Track changes within DOM element with MutationObserver Python - javascript

I found a website that pushes darts scores. Each time a new score is published, I would like to be notified.
import time
import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
URL = 'https://live.dartsdata.com/'
driver = webdriver.Chrome('/Users/hjam/downloads/chromedriver')
driver.get(URL)
time.sleep(1)
matches = driver.find_elements(By.XPATH, ".//*[#class='sr-match__wrapper srt-base-1 sr-ml-list__match']")
matches[0].click()
I want to retrieve the seconds until the match starts (there are no live matches atm, but idea is the same). I see that this data point is located in
seconds = driver.find_elements(By.XPATH, ".//*[#class='sr-lmt-0-ms-countdown__time srt-primary-7 srm-large']")[-1]
Now I want to use a MutationObserver to track the changes of this element. Each time the element changes, I want it to be printed. Using the example of docs I write the following
driver.execute_script("""
const targetNode = document.querySelector('#content1 > div.sr-lmt-plus__comp.srm-double.srm-isLmt > div > div > div > div > div > div > div.sr-lmt-wrap > div > div.sr-lmt-22-state > div.sr-bb.sr-lmt-matchstatus.sr-ltr.sr-lmt-matchstatus--small > div > div > div.sr-lmt-matchstatus__slider.sr-slider-flex__slider > div > div > div.sr-lmt-setsports-ms-matchstatus__row.sr-lmt-setsports-ms-matchstatus__countdown-wrapper > div > div.sr-lmt-0-ms-countdown__row > div:nth-child(4)');
const config = { attributes: true, childList: true, subtree: true };
const callback = function(mutationsList, observer) {
for(const mutation of mutationsList) {
console.log('Time is ticking');
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
""")
This yields no error, but nothing happens. It should print continuously 'time is ticking' in the Console right?
What am I doing wrong? And is there also a possibility that the output is printed in my python-script?

querySelectorAll is not live. It's returns representations of what the dom was and you're monitoring that snapshot. You need to use getElementsByClassName to hook onto a live element.

Related

Strange error with asynchronous code in Mutation Observer

I'm working on an third party addon for a WordPress page builder for which the code is based on Vue. Concretely, I'm working on an Emmet styled command line to add new elements to the UI with commands like: > section > container > button.
So far, so good. Since this Page Builder does not currently provide an API to interact inside the Vue context / state, I need to be creative. Currently it is a mixture between event dispatching and mutation observers. I manually go down each click path and thus create the elements as I need them.
This works, but only to a limited extent. I can't get any further at one point. The code you find below only goes up to the second layer.
section > container will work. The code will produce a section element with a nested container element inside. But: section > container > button will not work. The result here also is the same as before. The third element (button) is missing.
Here is the code:
async addElements(command) {
try {
// Split elements by one or more spaces
let elements = command.split(">");
// Remove items from array which are empty strings
elements = elements.filter((item) => item);
// Remove whitespace from all elements in the array
elements = elements.map((item) => item.replace(/\s+/, ""));
for (let i = 0; i < elements.length; i++) {
// Remove whitespace
elements[i] = elements[i].replace(" ", "");
// Add Element
await this.addElement({
element: elements[i],
index: i,
elementsArray: elements,
});
}
} catch (e) {
console.log(e);
}
}
async addElement({ element, elementsArray, index }) {
// if is empty string, skip
if (element == "") {
return;
}
let newCreatedElement;
let newCreatedElements = [];
let mutationCountMax = elementsArray.length;
// Add Mutation Observer to wait for the element to be created
let observer = new MutationObserver(async (mutations) => {
if (this.mutationCount >= mutationCountMax) {
observer.disconnect();
return;
}
this.mutationCount++;
// Create a list from all li elements from the mutation.addedNodes
let liList = [];
mutations.forEach((mutation) => {
if (mutation.addedNodes && mutation.addedNodes.length) {
liList.push(
...mutation.addedNodes[0].querySelectorAll("li")
);
}
});
// Remove mutations which have no added nodes
mutations = mutations.filter(
(mutation) => mutation.addedNodes.length > 0
);
// Remove mutations which have #builder-panel-header as parent
mutations = mutations.filter(
(mutation) =>
mutation.addedNodes[0].parentElement.id !=
"builder-panel-header"
);
// Remove mutations which target is not tag UL or tag LI
mutations = mutations.filter(
(mutation) =>
mutation.addedNodes[0].tagName == "UL" ||
mutation.addedNodes[0].tagName == "LI"
);
let addedNode = mutations[0].addedNodes[0];
newCreatedElement = [addedNode];
// if newCreatedElement is UL, get the first LI
newCreatedElement = newCreatedElement[0].querySelectorAll("li");
newCreatedElement.forEach((element) => {
// Add click event to the element
element
.querySelector(".builder-draggable-handle")
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
/**
* Wait for the nested LI Elements to be added to the page
*/
let liObserver = new MutationObserver(async (liMutations) => {
// Wait until the nested li elements have been added to the page
if (
liMutations.some(
(liMutation) => liMutation.addedNodes.length > 0
)
) {
// Get all of the nested li elements
let liElements = addedNode.querySelectorAll("li li");
console.log(liElements);
console.log("Detected Nested LI Elements");
liElements.forEach((element) => {
// Add click event to the element
element
.querySelector(".builder-draggable-handle")
.dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
});
// Disconnect the observer once the li elements have been found
liObserver.disconnect();
}
});
// Observe the addedNode element for changes
liObserver.observe(addedNode, {
childList: true,
subtree: true,
});
/**
* Create Element
*/
if (this.mutationCount != 1) {
await this.createElement(element);
}
observer.disconnect();
});
// Observe the body
observer.observe(document.querySelector("#builder-structure"), {
childList: true,
// Not observing attributes
attributes: false,
// Also deep elements
subtree: true,
});
if (index == 0) {
this.createElement(element);
}
}
async createElement(element) {
console.log("Create Element: " + element);
// Code to add the element to the UI structure
// ...
}
The console.log output for the command section > container > button is the following:
Create Element: section
Create Element: container
Create Element: button
NodeList [li#element-aqtufh
Detected Nested LI Elements
NodeList [li#element-aqtufh
Detected Nested LI Elements
NodeList [li#element-aqtufh
Detected Nested LI Elements
I think the problem lies in the asynchrony. The output in the console also seems to be not quite correct.
In that case I need the second mutation observer, because in the first one strangely the nested LI elements can't be found - I don't understand why. So it works, but only with the above mentioned problem.
Where is the error in my code?

Grab itemName data attribute from html element

I was trying to test a few novice tricks from a project tutorial. Wanted to create a small scale task app and ran into a weird problem. The last document.addEventListener below should theoretically call the closest element with the class name of ".name" should be detected since its in the same parent div with the button. However it is returning NULL. Am I applying the .closest() method wrong?
The event listener detects the button after everytime a task is created. Not sure why it returns NULL when, after creating the task via addTaskButton, the task with the class name of ".name". I even tried to create a data attribute id based off of the taskName itself to see if it'll detect, but still NULL / erroring.
const list = []
const container = document.querySelector('.container');
const itemContainer = document.querySelector('.item');
const addTaskButton = document.querySelector('.add-task');
const taskInput = document.querySelector('#task-name');
function renderTasks(){
itemContainer.innerHTML = ''
list.forEach(task => {
const itemElement = document.createElement('div')
itemElement.innerHTML = `
<div class="name">
${task.taskName}
</div>
<button class="retrieval">Retrieve ID</button>
`
itemElement.dataset.itemName = task.taskName
itemContainer.appendChild(itemElement);
})
}
addTaskButton.addEventListener('click', (e)=>{
e.preventDefault();
list.push({ taskName: taskInput.value})
renderTasks()
})
document.addEventListener('click', (e)=>{
if(e.target.matches('.retrieval')){
const taskName = e.target.closest('.name');
console.log(taskName)
}
})
Ok, I double checked the mdn article it says:
closestElement is the Element which is the closest ancestor of the
selected element. It may be null.
That means it only looks for parents and parents of parents and so on, not 'siblings'.

Simulating a click on dynamically rendered React element

I am creating a geography game where you are supposed to click on a specific country on a world map - if you click on the right one, the country changes color and the game presents a new country to be clicked at. If the player doesn't know, he can click on a button which will show him the correct answer. For this, I want to simulate a click event, so that the same onClick() function is called as if you clicked on the correct country.
I am using D3, and the world map is made up of svg paths. Below is the code I thought would work, using the HTMLElement.click() method:
function simulateClick() {
// for each index in the nodelist,
// if the properties are equal to the properties of currentTargetUnit,
// simulate a click on the path of that node
let nodelist = d3.selectAll(".unit")
for (let i = 0; i < nodelist._groups[0].length; i++) {
if (nodelist._groups[0].item(i).__data__.properties.filename === currentTargetUnit.properties.filename) {
console.log(nodelist._groups[0][i])
// logs the correct svg path element
nodelist._groups[0][i].click()
// logs TypeError: nodelist._groups[0][i].click is not a function
}
}
}
I then looked at some tutorials which say that, for some reason I don't fully understand, you rather need to use React.useRef for this - but in all their examples, they put a "ref" value on an element which is returned from the beginning in the React component, like so:
import React, { useRef } from "react";
const CustomTextInput = () => {
const textInput = useRef();
focusTextInput = () => textInput.current.focus();
return (
<>
<input type="text" ref={textInput} />
<button onClick={focusTextInput}>Focus the text input</button>
</>
);
}
This obviously doesn't work because my svg path elements aren't returned initially. So my question is - how can I achieve this, whether using useRef or not?
Below are some previous questions I looked at which also did not help.
Simulate click event on react element
React Test Renderer Simulating Clicks on Elements
Simulating click on react element
I finally solved it - instead of calling the onClick() which was set inside the node I created a new clickevent with the help of the following code:
function simulateClick() {
let nodelist = d3.selectAll(".unit")
for (let i = 0; i < nodelist._groups[0].length; i++) {
if (nodelist._groups[0].item(i).__data__.properties.filename === currentTargetUnit.properties.filename) {
var event = document.createEvent("SVGEvents");
event.initEvent("click",true,true);
nodelist._groups[0].item(i).dispatchEvent(event);
}
}
}

Access element whose parent is hidden - cypress.io

The question is as given in the title, ie, to access element whose parent is hidden. The problem is that, as per the cypress.io docs :
An element is considered hidden if:
Its width or height is 0.
Its CSS property (or ancestors) is visibility: hidden.
Its CSS property (or ancestors) is display: none.
Its CSS property is position: fixed and it’s offscreen or covered up.
But the code that I am working with requires me to click on an element whose parent is hidden, while the element itself is visible.
So each time I try to click on the element, it throws up an error reading :
CypressError: Timed out retrying: expected
'< mdc-select-item#mdc-select-item-4.mdc-list-item>' to be 'visible'
This element '< mdc-select-item#mdc-select-item-4.mdc-list-item>' is
not visible because its parent
'< mdc-select-menu.mdc-simple-menu.mdc-select__menu>' has CSS property:
'display: none'
The element I am working with is a dropdown item, which is written in pug. The element is a component defined in angular-mdc-web, which uses the mdc-select for the dropdown menu and mdc-select-item for its elements (items) which is what I have to access.
A sample code of similar structure :
//pug
mdc-select(placeholder="installation type"
'[closeOnScroll]'="true")
mdc-select-item(value="false") ITEM1
mdc-select-item(value="true") ITEM2
In the above, ITEM1 is the element I have to access. This I do in cypress.io as follows :
//cypress.io
// click on the dropdown menu to show the dropdown (items)
cy.get("mdc-select").contains("installation type").click();
// try to access ITEM1
cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click();
Have tried with {force:true} to force the item click, but no luck. Have tried to select the items using {enter} keypress on the parent mdc-select, but again no luck as it throws :
CypressError: cy.type() can only be called on textarea or :text. Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
Also tried using the select command, but its not possible because the Cypress engine is not able to identify the element as a select element (because its not, inner workings are different). It throws :
CypressError: cy.select() can only be called on a . Your
subject is a: < mdc-select-label
class="mdc-select__selected-text">Select ...< /mdc-select-label>
The problem is that the mdc-select-menu that is the parent for the mdc-select-item has a property of display:none by some internal computations upon opening of the drop-down items.
This property is overwritten to display:flex, but this does not help.
All out of ideas. This works in Selenium, but does not with cypress.io. Any clue what might be a possible hack for the situation other than shifting to other frameworks, or changing the UI code?
After much nashing-of-teeth, I think I have an answer.
I think the root cause is that mdc-select-item has display:flex, which allows it to exceed the bounds of it's parents (strictly speaking, this feels like the wrong application of display flex, if I remember the tutorial correctly, however...).
Cypress does a lot of parent checking when determining visibilty, see visibility.coffee,
## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
...
when $parent = parentHasDisplayNone($el.parent())
parentNode = $elements.stringify($parent, "short")
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
...
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
parentNode = $elements.stringify($parent, "short")
width = elOffsetWidth($parent)
height = elOffsetHeight($parent)
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."
But, when using .should('be.visible'), we are stuck with parent properties failing child visibility check, even though we can actually see the child.
We need an alternate test.
The work-around
Ref jquery.js, this is one definition for visibility of the element itself (ignoring parent properties).
jQuery.expr.pseudos.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
so we might use that as the basis for an alternative.
describe('Testing select options', function() {
// Change this function if other criteria are required.
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
it('checks select option is not visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.document().then(function(document) {
const item1 = document.querySelectorAll('mdc-select-item')[0]
item1.style.display = 'none'
cy.get('mdc-select-item').contains("ITEM1").then (item => {
expect(isVisible(item[0])).to.be.false
})
})
});
it('checks select option is clickable', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").click() // this will fail
cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.true //visible when list is first dropped
});
item1.click();
cy.wait(500)
cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
expect(isVisible(item2[0])).to.be.false // not visible after item1 selected
});
});
})
Footnote - Use of 'then' (or 'each')
The way you normally use assertion in cypress is via command chains, which basically wraps the elements being tested and handles things like retry and waiting for DOM changes.
However, in this case we have a contradiction between the standard visibility assertion .should('be.visible') and the framework used to build the page, so we use then(fn) (ref) to get access to the unwrapped DOM. We can then apply our own version of the visibility test using stand jasmine expect syntax.
It turns out you can also use a function with .should(fn), this works as well
it('checks select option is visible - 2', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
expect(isVisible(item1[0])).to.be.true
});
});
Using should instead of then makes no difference in the visibility test, but note the should version can retry the function multiple times, so it can't be used with click test (for example).
From the docs,
What’s the difference between .then() and .should()/.and()?
Using .then() simply allows you to use the yielded subject in a callback function and should be used when you need to manipulate some values or do some actions.
When using a callback function with .should() or .and(), on the other hand, there is special logic to rerun the callback function until no assertions throw within it. You should be careful of side affects in a .should() or .and() callback function that you would not want performed multiple times.
You can also solve the problem by extending chai assertions, but the documentation for this isn't extensive, so potentially it's more work.
For convenience and reusability I had to mix the answer of Richard Matsen and Josef Biehler.
Define the command
// Access element whose parent is hidden
Cypress.Commands.add('isVisible', {
prevSubject: true
}, (subject) => {
const isVisible = (elem) => !!(
elem.offsetWidth ||
elem.offsetHeight ||
elem.getClientRects().length
)
expect(isVisible(subject[0])).to.be.true
})
You can now chain it from contains
describe('Testing select options', function() {
it('checks select option is visible', function() {
const doc = cy.visit('http://localhost:4200')
cy.get("mdc-select").contains("installation type").click()
//cy.get('mdc-select-item').contains("ITEM1").should('be.visible') // this will fail
cy.get('mdc-select-item').contains("ITEM1").isVisible()
});
});
I came across this topic but was not able to run your example. So I tried a bit and my final solution is this. maybe someone other also needs this. Please note that I use typescript.
First: Define a custom command
Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
cy.get(p1).should((jq: JQuery<HTMLElement>) => {
if (!jq || jq.length === 0) {
//assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
return;
}
const elem: HTMLElement = jq[0];
const doc: HTMLElement = document.documentElement;
const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
const pageTop: number = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
let elementLeft: number;
let elementTop: number;
let elementHeight: number;
let elementWidth: number;
const length: number = elem.getClientRects().length;
if (length > 0) {
// TODO: select correct border box!!
elementLeft = elem.getClientRects()[length - 1].left;
elementTop = elem.getClientRects()[length - 1].top;
elementWidth = elem.getClientRects()[length - 1].width;
elementHeight = elem.getClientRects()[length - 1].height;
}
const val: boolean = !!(
elementHeight > 0 &&
elementWidth > 0 &&
elem.getClientRects().length > 0 &&
elementLeft >= pageLeft &&
elementLeft <= window.outerWidth &&
elementTop >= pageTop &&
elementTop <= window.outerHeight
);
assert.isTrue(val);
});
});
Please note the TODO. In my case I was targeting a button which has two border boxes. The first with height and width 0. So i must select the second one. Please adjust this to your needs.
Second: Use it
cy.wrap("#some_id_or_other_locator").isVisible();
I could solve it by calling scrollIntoView after getting an element. See this answer.
A related problem:
Cypress was unable to find a tab element because it had a style of display: none (even though it was visible on the page)
My workaround:
Cypress could target the tab by matching text and clicking
cy.get("[data-cy=parent-element]").contains("target text").click();
To expand a bit the answer of BTL, if anyone faced an error - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> in Typescript, following is what I added at the top of commands.ts in cypress to get away with it -
declare global {
namespace Cypress {
interface Chainable {
isVisible;
}
}
}
And may be replacing expect(isVisible(subject[0])).to.be.true with assert.True(isVisible(subject[0])); if you see any chai assertion error with expect and don't want to import it - as in Josef Biehler answer..
I was facing the same error that parent is hidden so Cypress is unable to click the child element, I handled this by handling the visibility of parent from hidden to visible by this code
cy.get('div.MuiDrawer-root.MuiDrawer-docked').invoke('css', 'overflow-x', 'visible').should('have.css', 'overflow-x', 'visible')
Note: You can apply any css you want in the invoke function like I have
Remove the flex and try. If it is solved then use the flex standard way

Mutation Observer or DOMNodeInserted

I have a script where I´ve use on the first slide of Adobe Captivate, to automate the task ok creating, courses, the script create the UX, navigation elements, intro/end motions, a game, insert spritesheets with characters, etc...
I´ve used DOMNodeInserted until know to check the modifications on the slide, when the user go to the next slide, the elements are added to the DOM and the page content is changed I´ve used this timer until now to call the function:
function detectChange(){
var slideName = document.getElementById('div_Slide')
slideName.addEventListener("DOMNodeInserted", detectChange, false);
updateSlideElements();
setTimeout(updateSlideElements, 100);
}
So I´m trying to use mutation Observer now:
var observer = new MutationObserver(function(mutations, observer) {
updateSlideElements();
});
observer.observe(document.getElementById('div_Slide').firstChild, {
attributes: true,
childList:true
});
But this is what´s happening, before with setTimeout I could reach the following element:
var motionText2 = document.querySelectorAll('div[id*=motion][class=cp-accessibility]');
This element is the firstChild of:
And the element can be found:
But now with mutationObserver the console returns empty:
I´ve just use a setTimeout inside the observer and watch the parent container not the firstChild:
var observer = new MutationObserver(function(mutations, observer) {
setTimeout(updateSlideElements, 100);
});
observer.observe(document.getElementById('div_Slide'), {
attributes: true,
childList:true
//subtree:true
});

Categories