I am interacting with a webpage whereby some of the deeper nested elements within sub-frames load even after the document is ready - i.e. they rely on pending ajax/api requests that are yet to be completed when the document is loaded.
My intention is to wait until these elements exist prior to doing anything with them. I can do this using setTimeout and wait for an arbitrary amount of time before doing anything e.g.
setTimeout(function() {
$("#better_content .binder__toc").append(
"<h4>Experiments</h4><ul><li>This</li><li>is</li><li>a</li><li>test</li></ul>"
);
}, 5000);
However, it would be nice in a recursive and asynchronous (non-blocking) manner to keep checking for said element ("#better_content .binder__toc") until undefined or null is not returned i.e. the element exists. I have tried to do this using Promises. A simple example with a counter is as follows:
static waitForElement = counter => {
counter++
return new Promise((res, rej) => {
if (counter < 5) {
this.waitForElement(counter)
.then(function() {
res("complete");
})
.catch(rej);
}
res("complete");
});
};
this.waitForElement(counter)
.then(a => console.log(a))
.catch(a => console.log(a));
The above resolves successfully and appears to be non-blocking. However, if I replace the counter for the element selector as below:
static waitForElement = selector => {
let found = $(document).find(selector)[0];
console.log(found);
return new Promise((res, rej) => {
if (found === undefined) {
this.waitForElement(selector)
.then(function() {
res("found");
})
.catch(rej);
}
res("found");
});
};
this.waitForElement("#better_content .binder__toc")
.then(a => {
console.log(a);
$("#better_content .binder__toc").append(
"<h4>Experiments</h4><ul><li>This</li><li>is</li><li>a</li><li>test</li></ul>"
);
})
.catch(a => console.log(a));
Then this never seems to resolve successfully and really slows down the webpage - I assume as it is blocking the main thread still.
Any suggestions on how I can correct or understand what is going on here would be more than welcome.
You can check for changes in an element, or children of an element, with the Mutation Observer API. The API fires a callback in which you can assert your logic to act when a certain change in the element is happened. In your case you'll want to listen on the container where the element is appended to with the fetch request.
It is possible to listen for certain changes, like attribute changes, or in your case changes in the children. In the configuration add childList: true which will indicate that you want to do something whenever a child is added or removed.
This way you don't have to check in if the element exists.
Check out the example below to see your example in action.
const target = document.getElementById('container');
const config = { childList: true };
const observer = new MutationObserver((mutations, observer) => {
/**
* Loop through all changes
*/
mutations.forEach(mutation => {
/**
* Only act if it is a change in the children.
*/
if (mutation.type !== 'childList') {
return;
}
/**
* Loop through the added elements and check if
* it is the correct element. Then add HTML to
* the newly added element.
*/
mutation.addedNodes.forEach(node => {
if (node.id === 'content') {
const html = `
<h4>Experiments</h4>
<ul>
<li>This</li>
<li>is</li>
<li>a</li
><li>test</li>
</ul>`;
node.innerHTML = html;
}
});
// Stop observing.
observer.disconnect();
});
});
// Observe the children in container.
observer.observe(target, config);
// Add content element after 2 seconds.
setTimeout(function() {
const content = document.createElement('div');
content.id = 'content';
target.append(content);
}, 2000);
<div id="container"></div>
Related
I am working on a simple Chrome Extension with Manifest V3, where I am using Mutation Observer to check if the element is visible or not. It checks when the page loads and every time when there is a change in the page. If element is visible then I am injecting a button in the page using a function.
I am either using Query Selector or Class Name, which are both separate functions to get the element as different website requirements.
Here is the code:
function findSelectorPlaceholder(selector) {
const observerS = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
for (let i = 0; i < mutation.addedNodes.length; i++) {
const elm = mutation.addedNodes[i].querySelector(selector);
if (elm && window.getComputedStyle(elm).display !== "none") {
console.log("Element is visible!");
placeBtn();
}
}
}
});
});
observerS.observe(document.body, {
childList: true,
subtree: true,
});
}
function findClassPlaceholder(cName) {
const mutationObserver1 = new MutationObserver((entries) => {
entries.forEach((entries) => {
if (entries.addedNodes.length > 0) {
for (let i = 0; i < entries.addedNodes.length; i++) {
if (entries.addedNodes[i].className == cName) {
console.log("Class Found!");
placeBtn();
}
}
}
});
});
mutationObserver1.observe(document.body, {
childList: true,
subtree: true,
});
}
Now, the extension runs smoothly as expected for both functions. However every time Mutation Observer is triggered with the Query Selector, I get mutation.addedNodes[i].querySelector is not a function error in the extension debugger.
I tried to look through the error and even tried to implement the function in different ways. However, if the function changes, it not working as expected with either more errors or unexpected results.
I would appreciate if someone shade a light into how to best implement this. I would also like to know if there is better way to merge both the functions to achieve the same results. I tried but it's breaking the code.
Thank you!
window.addEventListener("load" , () => {
setInterval(() => {
let xhttp = new XMLHttpRequest();
xhttp.open("GET", "./messagedUsers.php", true);
xhttp.onload = () => {
if(xhttp.readyState === XMLHttpRequest.DONE) {
if(xhttp.status === 200) {
let data = xhttp.response;
sideActiveUsers.innerHTML = data;
}
}
}
xhttp.send();
} , 500);
messageInputContainer.focus();
})
let userInfo = document.querySelectorAll(".user-info");
userInfoClick(userInfo); // function that does operation using the nodeList userInfo
In the above code , what I'm trying to do is send a HttpRequest to the php and get a div container as a response from there. As a response , I do get the div container. The div I got from there has a class name "user-info". And I want to click it and further , apply some css to the all the elements it holds(as there will be lot of "user-info" container coming from the response.). What I'm unable to figure out is "user-info" container can be applied styling through css(not JS). But if I try to use some JS for the node with className "user-info" , nothing happens as the nodeList seems to be empty.
And I've also tried using querySelector inside the setInterval for "user-info". But that makes the function outside userInfoClick(userInfo) takes a userInfo that is not defined.
And if I use both querySelector and the function that takes the variable that holds that nodeList(userInfoClick), I am able to click the element coming back from php. But its effect is stopped after every 500 ms(as the setInterval is set for 500ms).
I want to be able to click the element coming back from php and click it whose effect doesn't change after every 500ms(along with the element getting from php every 500ms).
Help me out guys.
userInfoClick function:
const userInfoClick = (userInfo) => {
let userinfo = Array.from(userInfo);
userinfo.forEach((el , ind) => {
el.addEventListener("click" , () => {
for(let i=0 ; i<userInfo.length ; i++)
{
if(ind!==i)
{
userinfo[i].style.backgroundColor = "#dddddd";
}
}
el.style.backgroundColor = "#fff";
})
})
}
You cannot bind an event handler to an element that does not yet exist. The querySelectorAll call will return an empty collection, as the assignment to innerHTML will only happen in some future.
In this case, where you have the interval, it is better to use event delegation. This means that you listen to click events at a higher level in the DOM tree -- on an element that is always there and is not dynamically created. In your case this could be sideActiveUsers.
So change your code as follows:
Remove this line (as it will return a useless empty collection):
let userInfo = document.querySelectorAll(".user-info");
And change the userInfoClick function, which should be called only once, without any arguments:
function userInfoClick() {
sideActiveUsers.addEventListener("click" , (e) => {
const el = e.target;
// We're only interested in user-info clicks:
if (!el.classList.contains("user-info")) return;
// Since we clicked on a user-info, the following will now have matches:
for (const sibling of sideActiveUsers.querySelectorAll(".user-info")) {
sibling.style.backgroundColor = "#dddddd";
}
el.style.backgroundColor = "#fff";
});
}
Im creating an app with react native and face the problem that I create multiple firebase listeners troughout the app, listeners on different screens to be precise and also listeners that listen to the firebase-database and others listening to the firestore.
What I want to accomplish is to kill all those listeners with one call or if necessary with multiple lines but as compact as possible - and also from an entire different screen where the listeners arent even running, this is important.
I know that there is the possibility to use Firebase.goOffline() but this only disconnects me from the Firebase - it doesnt stop the listeners. As soon as I goOnline() again, the listeners are all back.
I didnt find any solution yet for this problem from google etc thats why I try to ask here now, I would appriciate if anybody would have an idea how maybe an approach how to handle this type of behavior.
The following code samples provide you with listeners I included inside my app, they are located in in the same screen but I have nearly identical ones in other screens.
Database listener:
const statusListener = () => {
var partnerRef = firebase.database().ref(`users/${partnerId}/onlineState`);
partnerRef.on('value', function(snapshot){
setPartnerState(snapshot.val())
})
};
Firestore Listener: (this one is very long, thats only because I filter the documents I retrieve from the listener)
const loadnewmessages = () =>{ firebase.firestore().collection("chatrooms").doc(`${chatId}`).collection(`${chatId}`).orderBy("timestamp").limit(50).onSnapshot((snapshot) => {
var newmessages = [];
var deletedmesssages = [];
snapshot.docChanges().forEach((change) => {
if(change.type === "added"){
newmessages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime)
})
};
if(change.type === "removed"){
deletedmesssages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime)
})
};
})
if(newmessages.length > 0){
setChatMessages(chatmessages => {
return chatmessages.concat(newmessages)
});
};
if(deletedmesssages.length > 0){
setChatMessages(chatmessages => {
var modifythisarray = chatmessages;
let index = chatmessages.map(e => e.timestamp).indexOf(`${deletedmesssages[0].timestamp}`);
let pasttime = Date.now() - parseInt(modifythisarray[index].timestamp);
modifythisarray.splice(index, 1);
if(pasttime > 300000){
return chatmessages
}else{
return modifythisarray
}
});
setRefreshFlatList(refreshFlatlist => {
//console.log("Aktueller Status von refresher: ", refreshFlatlist);
return !refreshFlatlist
});
}
newmessages = [];
deletedmesssages = [];
})
};
Both those listeners are called within a useEffect hook just like that: (useEffect with empty braces at the end makes sure those listeners are called only once and not multiple times.)
useEffect(() => {
loadnewmessages();
statusListener();
}, []);
All of the subscribe functions return the unsubscribe function
const unSubscriptions = [];
... Where you subscribe
const unSub = document.onSnapshot(listener);
subscriptions.push(unSub);
... Where you unsubscribe all
function unSubAll () {
unSubscriptions.forEach((unSub) => unSub());
// Clear the array
unSubscriptions.length = 0;
}
How to check if element is present or not, so that certain steps can be performed if element is present. Else certain different steps can be performed if element is not present.
I tried something like below but it didn't work:
Cypress.Commands.add('deleteSometheingFunction', () => {
cy.get('body').then($body => {
if ($body.find(selectors.ruleCard).length) {
let count = 0;
cy.get(selectors.ruleCard)
.each(() => count++)
.then(() => {
while (count-- > 0) {
cy.get('body')
// ...
// ...
}
});
}
});
});
I am looking for a simple solution, which can be incorporated with simple javascript
if else block or then() section of the promise
Something similar to Webdriver protocol's below implementions:
driver.findElements(By.yourLocator).size() > 0
check for presenece of element in wait
Kindly advise. Thanks
I'll just add that if you decide to do if condition by checking the .length property of cy.find command, you need to respect the asynchronous nature of cypress.
Example:
Following condition evaluates as false despite appDrawerOpener button exists
if (cy.find("button[data-cy=appDrawerOpener]").length > 0) //evaluates as false
But this one evaluates as true because $body variable is already resolved as you're in .then() part of the promise:
cy.get("body").then($body => {
if ($body.find("button[data-cy=appDrawerOpener]").length > 0) {
//evaluates as true
}
});
Read more in Cypress documentation on conditional testing
it has been questioned before: Conditional statement in cypress
Thus you can basically try this:
cy.get('header').then(($a) => {
if ($a.text().includes('Account')) {
cy.contains('Account')
.click({force:true})
} else if ($a.text().includes('Sign')) {
cy.contains('Sign In')
.click({force:true})
} else {
cy.get('.navUser-item--account .navUser-action').click({force:true})
}
})
cypress all steps are async ,, so that you should make common function in commands file or page object file,,..
export function checkIfEleExists(ele){
return new Promise((resolve,reject)=>{
/// here if ele exists or not
cy.get('body').find( ele ).its('length').then(res=>{
if(res > 0){
//// do task that you want to perform
cy.get(ele).select('100').wait(2000);
resolve();
}else{
reject();
}
});
})
}
// here check if select[aria-label="rows per page"] exists
cy.checkIfEleExists('select[aria-label="rows per page"]')
.then(e=>{
//// now do what if that element is in ,,..
})
.catch(e=>{
////// if not exists...
})
I found a solution, hope it helps!
You can use this:
cy.window().then((win) => {
const identifiedElement = win.document.querySelector(element)
cy.log('Object value = ' + identifiedElement)
});
You can add this to your commands.js file in Cypress
Cypress.Commands.add('isElementExist', (element) => {
cy.window().then((win) => {
const identifiedElement = win.document.querySelector(element)
cy.log('Object value = ' + identifiedElement)
});
})
Cypress official document has offered a solution addressing the exact issue.
How to check Element existence
// click the button causing the new
// elements to appear
cy.get('button').click()
cy.get('body')
.then(($body) => {
// synchronously query from body
// to find which element was created
if ($body.find('input').length) {
// input was found, do something else here
return 'input'
}
// else assume it was textarea
return 'textarea'
})
.then((selector) => {
// selector is a string that represents
// the selector we could use to find it
cy.get(selector).type(`found the element by selector ${selector}`)
})
For me the following command is working for testing a VS code extension inside Code server:
Cypress.Commands.add('elementExists', (selector) => {
return cy.window().then($window => $window.document.querySelector(selector));
});
And I'm using it like this in my E2E test for a Code Server extension:
cy.visit("http://localhost:8080");
cy.wait(10000); // just an example here, better use iframe loaded & Promise.all
cy.elementExists("a[title='Yes, I trust the authors']").then((confirmBtn) => {
if(confirmBtn) {
cy.wrap(confirmBtn).click();
}
});
Just ensure that you're calling this check once everything is loaded.
If you're using Tyepscript, add the following to your global type definitions:
declare global {
namespace Cypress {
interface Chainable<Subject> {
/**
* Check if element exits
*
* #example cy.elementExists("#your-id").then($el => 'do something with the element');
*/
elementExists(element: string): Chainable<Subject>
}
}
}
Aside
VS Code server relies heavily on Iframes which can be hard to test. The following blog post will give you an idea - Testing iframes with Cypress.
The above code is needed to dismiss the "trust modal" if it's shown. Once the feature disable-workspace-trust is released it could be disabled as CLI option.
This command throws no error if element does not exist. If it does, it returns the actual element.
cypress/support/commands.js
elementExists(selector) {
cy.get('body').then(($body) => {
if ($body.find(selector).length) {
return cy.get(selector)
} else {
// Throws no error when element not found
assert.isOk('OK', 'Element does not exist.')
}
})
},
Usage:
cy.elementExists('#someSelectorId').then(($element) => {
// your code if element exists
})
In case somebody is looking for a way to use cy.contains to find an element and interact with it based on the result. See this post for more details about conditional testing.
Use case for me was that user is prompted with options, but when there are too many options, an extra click on a 'show more' button needs to be done before the 'desired option' could be clicked.
Command:
Cypress.Commands.add('clickElementWhenFound', (
content: string,
) => {
cy.contains(content)
// prevent previous line assertion from triggering
.should((_) => {})
.then(($element) => {
if (!($element || []).length) {
/** Do something when element was not found */
} else {
cy.contains(content).click();
}
});
});
Usage:
// Click button with 'Submit' text if it exists
cy.clickElementWhenFound('Submit');
Using async/await gives a clean syntax:
const $el = await cy.find("selector")
if ($el.length > 0) {
...
More info here: https://medium.com/#NicholasBoll/cypress-io-using-async-and-await-4034e9bab207
I had the same issue like button can appear in the webpage or not. I fixed it using the below code.
export function clickIfExist(element) {
cy.get('body').then((body) => {
cy.wait(5000).then(() => {
if (body.find(element).length > 0) {
cy.log('Element found, proceeding with test')
cy.get(element).click()
} else {
cy.log('Element not found, skipping test')
}
})
})
}
I'm using rxjs.
I have a Browser that's responsible for a number of Page objects. Each page has an Observable<Event> that yields a stream of events.
Page objects are closed and opened at various times. I want to create one observable, called TheOneObservable that will merge all the events from all the currently active Page objects, and also merge in custom events from the Browser object itself.
Closing a Page means that the subscription to it should be closed so it doesn't prevent it from being GC'd.
My problem is that Pages can be closed at any time, which means that the number of Observables being merged is always changing. I've thought of using an Observable of Pages and using mergeMap, but there are problems with this. For example, a subscriber will only receive events of Pages that are opened after it subscribes.
Note that this question has been answered here for .NET, but using an ObservableCollection that isn't available in rxjs.
Here is some code to illustrate the problem:
class Page {
private _events = new Subject<Event>();
get events(): Observable<Event> {
return this._events.asObservable();
}
}
class Browser {
pages = [] as Page[];
private _ownEvents = new Subject<Event>();
addPage(page : Page) {
this.pages.push(page);
}
removePage(page : Page) {
let ixPage = this.pages.indexOf(page);
if (ixPage < 0) return;
this.pages.splice(ixPage, 1);
}
get oneObservable() {
//this won't work for aforementioned reasons
return Observable.from(this.pages).mergeMap(x => x.events).merge(this._ownEvents);
}
}
It's in TypeScript, but it should be understandable.
You can switchMap() on a Subject() linked to array changes, replacing oneObservable with a fresh one when the array changes.
pagesChanged = new Rx.Subject();
addPage(page : Page) {
this.pages.push(page);
this.pagesChanged.next();
}
removePage(page : Page) {
let ixPage = this.pages.indexOf(page);
if (ixPage < 0) return;
this.pages.splice(ixPage, 1);
this.pagesChanged.next();
}
get oneObservable() {
return pagesChanged
.switchMap(changeEvent =>
Observable.from(this.pages).mergeMap(x => x.events).merge(this._ownEvents)
)
}
Testing,
const page1 = { events: Rx.Observable.of('page1Event') }
const page2 = { events: Rx.Observable.of('page2Event') }
let pages = [];
const pagesChanged = new Rx.Subject();
const addPage = (page) => {
pages.push(page);
pagesChanged.next();
}
const removePage = (page) => {
let ixPage = pages.indexOf(page);
if (ixPage < 0) return;
pages.splice(ixPage, 1);
pagesChanged.next();
}
const _ownEvents = Rx.Observable.of('ownEvent')
const oneObservable =
pagesChanged
.switchMap(pp =>
Rx.Observable.from(pages)
.mergeMap(x => x.events)
.merge(_ownEvents)
)
oneObservable.subscribe(x => console.log('subscribe', x))
console.log('adding 1')
addPage(page1)
console.log('adding 2')
addPage(page2)
console.log('removing 1')
removePage(page1)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
You will need to manage the subscriptions to the pages yourself and feed its events into the resulting subject yourself:
const theOneObservable$ = new Subject<Event>();
function openPage(page: Page): Subscription {
return page.events$.subscribe(val => this.theOneObservable$.next(val));
}
Closing the page, i.e. calling unsubscribe on the returned subscription, will already do everything it has to do.
Note that theOneObservable$ is a hot observable here.
You can, of course, take this a bit further by writing your own observable type which encapsulates all of this API. In particular, this would allow you to unsubscribe all inner observables when it is being closed.
A slightly different approach is this:
const observables$ = new Subject<Observable<Event>>();
const theOneObservable$ = observables$.mergeMap(obs$ => obs$);
// Add a page's events; note that takeUntil takes care of the
// unsubscription process here.
observables$.next(page.events$.takeUntil(page.closed$));
This approach is superior in the sense that it will unsubscribe the inner observables automatically when the observable is unsubscribed.