Skip to content

Instantly share code, notes, and snippets.

@sebastianrothbucher
Last active November 11, 2023 16:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebastianrothbucher/29a0c126a296792c1061ce7b612da1ee to your computer and use it in GitHub Desktop.
Save sebastianrothbucher/29a0c126a296792c1061ce7b612da1ee to your computer and use it in GitHub Desktop.
Track fetch API for more stable tests
const __legacyFetch = window.fetch;
let __openFetchPromises = 0;
let __waitForFetchPromise = Promise.resolve();
let __waitForFetchResolve = null;
// need a function to instrument promise-ified methods from fetch down
function __instrumentPromise(legacyThis, legacyFct, legacyFctName) {
return function() {
if (__openFetchPromises === 0) {
__waitForFetchPromise = new Promise(resolve => __waitForFetchResolve = resolve);
}
const legacyRes = legacyFct.apply(legacyThis, arguments);
if (legacyRes instanceof Promise) {
__openFetchPromises++;
console.log('New ' + legacyFctName + ' - now: ' + __openFetchPromises);
return legacyRes.then((legacyPromiseRes) => {
// if there's a chance for new promises, instrument accordingly
Object.keys(legacyPromiseRes?.__proto__ || {})
.filter(name => typeof(legacyPromiseRes[name]) === 'function')
.forEach(name => {
legacyPromiseRes[name] = __instrumentPromise(legacyPromiseRes, legacyPromiseRes[name], name);
});
// handle this result itself
setTimeout(() => { // run ALL other handlers before declaring we're done
__openFetchPromises--;
console.log('Done handling ' + legacyFctName + ' - now: ' + __openFetchPromises);
if (__openFetchPromises === 0) {
console.log('Done with backend - firing our promise');
__waitForFetchResolve();
}
});
return legacyPromiseRes;
});
} else {
return legacyRes;
}
};
}
window.__waitForFetch = async function() {
await __waitForFetchPromise;
};
window.__hasNoOngoingFetch = function() {
return __openFetchPromises === 0;
};
// finally insert our hook
window.fetch = __instrumentPromise(window, window.fetch, 'fetch');
import { test, expect } from '@playwright/test';
test('use flaky API', async ({ page }) => {
await page.goto('/');
await page.locator('#bt').click();
//await page.locator('//span[contains(string(.), "content two")]').waitFor(); // a TON is wrong with that: locators are hard to write (and do we really want a page model just for that?) - and an app error waits the whole 30s before firing!
await page.waitForFunction(() => (window as any).__hasNoOngoingFetch()); // so much better
await expect(page.locator('#spn1')).toContainText('content one', {timeout: -1});
await expect(page.locator('#spn2')).toContainText(/content two/, {timeout: -1}); // try "three" here - it returns immediately
});
<html>
<body>
<div><button id="bt">Run action</button> <span id="spn1"></span> <span id="spn2"></span></div>
<script>
document.getElementById('bt').addEventListener('click', async () => {
console.log('Starting action one');
const serverRes1 = await fetch('http://localhost:8090/test?action=one').then(res => res.json());
document.getElementById('spn1').innerText = serverRes1.content;
console.log('Starting action two');
const serverRes2 = await fetch('http://localhost:8090/test?action=two').then(res => res.json());
document.getElementById('spn2').innerText = serverRes2.content;
console.log('Done with actions');
});
</script>
<script src="fetch-override.js"></script><!-- TODO: only insert during tests - like in build -->
</body>
</html>
Starting action one
New fetch - now: 1
New json - now: 2
Done handling fetch - now: 1
Starting action two
New fetch - now: 2
Done handling json - now: 1
New json - now: 2
Done handling fetch - now: 1
Done with actions
Done handling json - now: 0
Done with backend - firing our promise
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment