Puppeteer Constantly Check for Selector to Click - javascript

I have been working on a Puppeteer system to help me handle my automation testing. However, my page would prompt a random pop-up to notify customers of on-going promotions - This prevents my test from proceeding. My initial thought was to run a endless loop in the background during the test to waitForSelector and click if it exists. However, I was thinking that this approach doesn't sound too smart, and I couldn't find anything suitable in the API.
Has anyone encountered the similar problem and has come out with a brilliant solution?

If the popup is always going to appear towards the beginning of the session, you can use page.waitForSelector():
await page.waitForSelector('#popup', {visible: true});
await page.click('#popup'); // Close Popup
Alternatively, if the element is dynamically added to the page and might not appear, you can use the MutationObserver interface to watch for the element to be added to the DOM tree and click it:
await page.evaluate(() => {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
for (let i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].id === 'popup' && window.getComputedStyle(mutation.addedNodes[i]).display !== 'none') {
mutation.addedNodes[i].click(); // Close Popup
}
}
});
});
observer.observe(document, {subtree: true});
});

Related

How to code a special endless scroll website?

I checked a lot of endless scroll scripts and was not happy with any of them.
What I want to have is in general this:
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
//same HTML document starts again without reloading
}
};
How is it possible to code this?
You can use an IntersectionObserver to get notified when your view is reaching the end to continue loading more content i.e. infinite scroll, what you want to achieve.
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#listItem');
observer.observe(target);
To get the complete guide, check out the very detailed documentation
ONLY If you are using React JS or planning to use it and your concern is performance
You can take a look on this react-window library so it will efficiently only render the ones on the window view:
https://codesandbox.io/s/x70ly749rq - if you inspect element the list on this sandbox, you will see that the previous set of list are remove from the dom making your site not slow despite of so much data that you previously loaded.
It goes hand in hand with react-window-infinite-loader

How to check if element is never visible in Cypress e2e testing?

Is there any way to assert that an element is never visible at any point when routing within Cypress?
I have a server-rendered web app that is sometimes showing a "loading" state when it shouldn't. So when I navigate between pages, a "loading" indicator is showing for a few seconds and then disappearing.
I know that Cypress's assertions will sometimes "wait" -- in this case Cypress waits until loading indicator goes away and that makes the test think that it has passed. But I want the test to fail because the loading indicator was visible at some point.
I'm using these two assertions:
cy.get('[data-test="loading"]').should('not.exist');
cy.get('[data-test="loading"]').should('not.be.visible');
But both of them are passing because the loading indicator goes away.
I've checked through all the documentation but there doesn't seem to be some kind of method for checking that an element is never visible. Is there some method I'm missing or some hack to test this a different way?
I might be crazy, and i have not tested this yet, but I wanted to throw this out there
I assume you are testing that there should NEVER be a loading indicator and it is waiting the default 4 seconds and the indicator goes away, and thus your test pass. So below I set the wait to zero, so it does not wait. I am also confused as to why you don't fix the actual code so you don't see the indicator if you are not supposed to. Perhaps you don't have access to the code..
cy.get('[data-test="loading"]',{ timeout: 0 }).should('not.exist');
cy.get('[data-test="loading"]',{ timeout: 0 }).should('not.be.visible');
Cypress has a lite version of jQuery, so we can watch for changes to the parent of the element that should not exist.
#Maccurt's tests are applied whenever a change occurs.
You want to keep watch firing to a minimum, so find the immediate (or nearest) parent of tested element.
Note this covers exists tests, but should not be necessary for visible tests if the element is present all the time but is just not visible.
In this example a button is added to body.
The first test watches for a span (which is never added so the test succeeds).
The 2nd test watches for the button and fails.
describe('watching for an element to not appear', () => {
const watchAndTest = function(parentSelector, assert) {
Cypress.$(parentSelector).bind('DOMNodeInserted', function(event) {
assert()
});
}
it('should succeed because "span" does not exist', () => {
const parentSelector = 'body'
const watchForSelector = 'span'
watchAndTest(parentSelector,
() => {
// Place your 'assert' code here
cy.get(`${parentSelector} ${watchForSelector}`,{ timeout: 0 })
.should('not.exist');
}
)
// Place your 'act' code here
cy.get(parentSelector).then(parent => {
var newElement = document.createElement('button');
parent[0].appendChild(newElement)
})
Cypress.$(parentSelector).unbind('DOMNodeInserted')
})
it('should fail because "button" exists', () => {
const parentSelector = 'body'
const watchForSelector = 'button'
watchAndTest(parentSelector,
() => {
// Place your 'assert' code here
cy.get(`${parentSelector} ${watchForSelector}`,{ timeout: 0 })
.should('not.exist');
}
)
// Place your 'act' code here
cy.get(parentSelector).then(parent => {
var newElement = document.createElement('button');
parent[0].appendChild(newElement)
})
Cypress.$(parentSelector).unbind('DOMNodeInserted')
})
})

Wait for transition to end in puppeteer

I'm trying to test a website using Puppeteer. Unfortunately, I'm having trouble clicking elements in a toolbar. The toolbar is using a CSS transition to gracefully slide into the page. My code is failing because I'm clicking where the element will appear while the page is still animating. I'm using a timeout as a workaround, but there's got to be a more elegant solution. Here's an example:
await page.click("#showMeetings"); //Toolbar slides in
await page.waitFor(3000); //Can I do this without a timeout?
await page.click("#mtgfind"); //Click button in toolbar
I think I need to wait on the transitionend event, but I'm unsure of how to do that in Puppeteer. Any help would be appreciated.
In case of Grant solution, you shouldn't forget to remove event listener and to wait for it. You can try this solution, it works for me. I had similar problem.
async waitForTransitionEnd(element) {
await page.evaluate((element) => {
return new Promise((resolve) => {
const transition = document.querySelector(element);
const onEnd = function () {
transition.removeEventListener('transitionend', onEnd);
resolve();
};
transition.addEventListener('transitionend', onEnd);
});
}, element);
}
And call it:
await page.click('#showMeetings');
await waitForTransitionEnd('#elementWithTransition');
await page.click("#mtgfind");
I came up with a fairly dumb solution. I looked up how long the transition was supposed to take (in this case 0.2 seconds) and just modified the waitFor statement to wait that long. Final code:
await page.click("#showMeetings"); //Toolbar slides in
await page.waitFor(200);
await page.click("#mtgfind"); //Click button in toolbar
This wouldn't work if the timing was variable, but the website reuses the same transition everywhere, so it's fine for my use case.
You can use page.evaluate() to and the transitionend event to accomplish your goal:
await page.evaluate(() => {
const show_meetings = document.getElementById('showMeetings');
const mtg_find = document.getElementById('mtgfind');
mtg_find.addEventListener('transitionend', event => {
mtg_find.click();
}, false);
show_meetings.click();
});

Window.document not updated

I open a new tab in the browser with const w = window.open('www.example.com'); and I get the DOM with let d = w.document; Then I go to another webpage with an click-event: d.querySelector('a').click(); The new webpage opens, in the same window, and I want to grap the DOM of the just openend page by running d = w.document again. And this is the point where I get stuck.
I aspect the DOM of the currently openend window, but instead I get the DOM of the previous window back. When console.log(w) (in the .js itself not in the console), with the new webpage open, I get the window object of the current page and the window.document matches the DOM of the openend page. But as I said in reality I get the DOM of the previous page back when I run d = w.document on the new page.
The following code is the whole function I use. Sidenote: I didn't(/couldn't) use window.document.onload fom I reason I don't understand, but It seems I can't attach an function to the window onload event. Also to make things clear my code is inside of a ES6 Class so I didn't use the constas in the example.
The Problem:
When I run the getDOM method on the new openend webpage it returns the DOM of the previous page.
async foo() {
await this.getDOM()
.then(w => this.d = w.document);
const x = this.d.querySelector('button.x');
console.log(send); //null
}
getDOM () {
return new Promise(resolve => {
const interval = setInterval(() => {
if (this.w.document.readyState === 'complete') {
clearInterval(interval);
resolve(this.w);
}
}, 500);
});
}
I hope someone can help, thanks in advance!

Confirm beforeunload in Electron

i know that there are hundreds of questions like: "How can i prevent close event in electron" or something like that.
After implementing a confirmation box (electron message box) in the beforeunload event i was able to close my app and cancel the close event. Since the dev tools are always open, i didn't recognize that it doesn't work while the dev tools are closed...
window.onbeforeunload = e =>
{
// show a message box with "save", "don't save", and "cancel" button
let warning = remote.dialog.showMessageBox(...)
switch(warning)
{
case 0:
console.log("save");
return;
case 1:
console.log("don't save");
return;
case 2:
console.log("cancel");
return false;
// e.returnValue = "false";
// e.returnValue = false;
}
};
So, when the dev tools are opened, i can close the app with saving, without saving and cancel the event.
When the dev tools are closed, the cancel button doesn't work anymore.
Btw.:
window.onbeforeunload = e =>
{
return false;
alert("foo");
};
will cancel the close event and obviously wouldn't show the message (doesn't matter if dev tools are open or closed)
window.onbeforeunload = e =>
{
alert("foo");
return false;
};
will cancel the close event after pressing ok if dev tools are open and will close the app after pressing ok if dev tools are closed
Intentionally i'm using the synchronous api of the message box and while i'm writing this question i figured out that a two windowed app (new remote.BrowserWindow()) will behave exactly like with the dev tools.
Has anyone an idea how i can resolve this problem?
Many thanks in advance
Instead of onbeforeunload prefer working with the event close. From this event, you'll be able to catch the closing event before the whole closure process is completed (event closed). With close, you'll be able to take the control and stop whenever you need the completion of the closure.
This is possible when you create your BrowserWindow, preferably in the main process:
// Create the browser window.
window = new BrowserWindow({});
// Event 'close'
window.on('close', (e) => {
// Do your control here
if (bToStop) {
e.preventDefault();
}
})
// Event 'closed'
window.on('closed', (e) => {
// Fired only if you didn't called e.preventDefault(); above!
})
In addition, be aware that the function e.preventDefault() is spreading in the whole code. If you need to be back to the natural behaviour of Electron, you need to toggle the variable e.defaultPrevented to false.
Actually, it seems e.preventDefault() function is handling the variable e.defaultPrevented to true until any change on it.
Maybe this will help someone with similar needs as i had, i have a react app wrapped in an electron app, the react app is agnostic to electron and can also run in the browser and the requirements i had was to show the default browser prompt, the infamous Leave Site? alert.
In the browser this is easy, for example with react i just do this:
useEffect(() => {
window.onbeforeunload = promptOnProjectLeave ? () => true : undefined;
return () => {
window.onbeforeunload = undefined;
}
}, [promptOnProjectLeave]);
Which will show the default browser Leave Site? prompt, but in electron this will only prevent the window from being closed without any action prompt asking you if you are sure, so my approach was a mix of this post and another post.
This is the solution
mainWindow.webContents.on('will-prevent-unload', (event) => {
const options = {
type: 'question',
buttons: ['Cancel', 'Leave'],
message: 'Leave Site?',
detail: 'Changes that you made may not be saved.',
};
const response = dialog.showMessageBoxSync(null, options)
if (response === 1) event.preventDefault();
});
This will allow me to use window.onbeforeunload in my react code as i would in the browser, in the browser i will get the default browser prompt and in electron i will get a message box :)
This is my first time working with electron so might be some ways to improve this but either way hope this helps someone, i know it would have helped me when i started with this task.
Update:
As I mentioned above in the comments of the accepted answer, the preventDefault was ignored on Windows. To be precise, I had it placed in the callback of a native electron dialog that opened when the user closed the app.
Therefore I have implemented a different approach:
let close: boolean = false
win.on('close', (ev: any) => {
if (close === false) {
ev.preventDefault()
dialog.showMessageBox({
type: 'warning',
buttons: ['Cancel', 'Ok'],
title: 'Do not forget to safe your changes',
cancelId: 0,
defaultId: 1,
noLink: true
}).then((val) => {
if (val.response === 0) {
// Cancel the close process
} else if (win) {
close = true
app.quit()
}
})
}
})
You can simply use 'pagehide' event. It seems to be working fine for electron apps. It works slightly different from 'beforeunload' as it can't prevent closing window/tab, but if you only need to do something before the page is closed(send some async request with navigator.sendBeacon(), etc) then this event might suit your needs.
You can read more info about it here, here and in the docs
Example of usage:
window.addEventListener('pagehide', () => {
window.navigator.sendBeacon(url, data);
}

Categories

Resources