Skip to content

Instantly share code, notes, and snippets.

@rinogo
Last active July 29, 2022 21:45
Show Gist options
  • Save rinogo/7370cfd10f0290a01c773221b26994ad to your computer and use it in GitHub Desktop.
Save rinogo/7370cfd10f0290a01c773221b26994ad to your computer and use it in GitHub Desktop.
Wait for an element to remain unchanged for a period of time. Useful for waiting for asynchronous (AJAX) updates. Playwright, ES6, Promise, Mutation, MutationObserver
///////
//To test this code, execute it locally or copy/paste it at https://try.playwright.tech/
//Usage example: `await waitForMutationToStop(await page.$("#container"));`
///////
// @ts-check
const playwright = require("playwright");
//Wait for `elem` to have no mutations for a period of `noMutationDuration` ms. If `timeout` ms elapse without a "no mutation" period of sufficient length, throw an error. If `waitForFirstMutation` is true, wait until the first mutation before starting to wait for the `noMutationDuration` period.
const waitForMutationToStop = async (elem, noMutationDuration = 3000, timeout = 60000, waitForFirstMutation = true) => {
return elem.evaluate(async (elem, [noMutationDuration, timeout, waitForFirstMutation]) => {
//Resolve when a mutation occurs on elem.
const waitForMutation = async (elem) => {
let html = elem.innerHTML;
return new Promise((resolve, reject) => {
new MutationObserver((mutationRecords, observer) => {
if(elem.innerHTML != html) {
console.log("Mutation detected.");
resolve();
observer.disconnect();
}
})
.observe(document.documentElement, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
});
})
};
let timeoutId, noMutationTimeoutId;
return Promise.race([
//Reject when `timeout` ms have passed
new Promise((resolve, reject) => timeoutId = setTimeout(reject, timeout, `Reached timeout of ${timeout} ms while waiting for mutation to stop.`)),
//Resolve when `noMutationDuration` ms have passed since the last mutation.
new Promise(async (resolve, reject) => {
//If requested, wait for the first mutation to occur. This allows the function to wait longer than `noMutationDuration` ms for the first mutation to occur. Once the first mutation occurs, the function will resume "normal" behavior - that is, it will wait until no mutations occur for `noMutationDuration` ms before resolving.
if(waitForFirstMutation) {
console.log("Waiting for first mutation.");
await waitForMutation(elem);
}
while(true) {
noMutationTimeoutId = setTimeout(resolve, noMutationDuration) //We reset this timer every time a mutation occurs. So, when it finally "executes", we know that `noMutationDuration` has passed since the last mutation.
console.log(`Waiting ${noMutationDuration} ms for mutation.`);
await waitForMutation(elem);
if(!noMutationTimeoutId) {
break;
}
clearTimeout(noMutationTimeoutId);
}
}),
])
.then(
(value) => {
console.log(`${noMutationDuration} ms have elapsed since this function was called or the last mutation was detected. Fulfilling.`);
}, (reason) => {
console.log(`${timeout} ms have elapsed without a ${noMutationDuration} ms period devoid of mutation. Rejecting.`);
throw new Error(reason);
}
)
//Clear timeouts - if we don't, Node will refuse to exit until active timeouts expire
.finally(() => {
clearTimeout(timeoutId);
clearTimeout(noMutationTimeoutId);
noMutationTimeoutId = null;
})
},
[noMutationDuration, timeout, waitForFirstMutation]);
}
//Test harness
(async () => {
const browser = await playwright.chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://rinogo.github.io/playwright-mutation-test-site/static-delays.html");
await waitForMutationToStop(await page.$("#container"));
await page.screenshot({ path: `example.png` });
await browser.close();
})();
@rinogo
Copy link
Author

rinogo commented Mar 11, 2021

Released under the MIT License - please let me know if you appreciate this and share your improvements! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment