test: Avoid test flakiness by waiting for request count (#2740)

This commit is contained in:
Dermot Duffy
2026-08-30 16:39:39 -07:00
committed by GitHub
parent bb6794ac49
commit fe7ac89982
2 changed files with 55 additions and 3 deletions
+50 -2
View File
@@ -14,6 +14,10 @@ const TEST_MEDIA_PATH = '/test-media';
// page runs one test file, so nothing here is shared with another file. // page runs one test file, so nothing here is shared with another file.
const requestCounts = new Map<string, number>(); const requestCounts = new Map<string, number>();
// Tests waiting for a token to reach a request count, resolved by the worker
// below as it counts.
const requestWaiters = new Map<string, { count: number; resolve: () => void }[]>();
// Whether this page's tests asked for the worker. Recorded so that a URL only // Whether this page's tests asked for the worker. Recorded so that a URL only
// the worker can answer cannot be built without it. // the worker can answer cannot be built without it.
let inUse = false; let inUse = false;
@@ -75,15 +79,49 @@ export const createUnansweredMediaURL = (): string => createTestMediaURL([]);
export const createStallingMediaURL = (filename?: string): string => export const createStallingMediaURL = (filename?: string): string =>
createTestMediaURL([HTTP_OK], false, filename); createTestMediaURL([HTTP_OK], false, filename);
const getToken = (url: string): string | null =>
new URL(url, window.location.href).searchParams.get('token');
/** /**
* How many requests a media URL has been asked for, so a test can count what * How many requests a media URL has been asked for, so a test can count what
* the card actually fetched rather than only what it displayed. * the card actually fetched rather than only what it displayed.
*/ */
export const getTestMediaRequestCount = (url: string): number => { export const getTestMediaRequestCount = (url: string): number => {
const token = new URL(url, window.location.href).searchParams.get('token'); const token = getToken(url);
return (token ? requestCounts.get(token) : null) ?? 0; return (token ? requestCounts.get(token) : null) ?? 0;
}; };
/**
* Wait until a media URL has been asked for the given number of times.
*
* A request is put on the page by the browser and answered by a service worker
* on a thread of its own, so how long it takes to be counted is real time that
* running the card's fake clock forward does not cover.
*/
export const waitForTestMediaRequestCount = async (
url: string,
count: number,
): Promise<void> => {
const token = getToken(url);
if (!token) {
throw new Error(
'Requests are only counted for a media URL created by this file, ' +
'e.g. createUnansweredMediaURL().',
);
}
if (getTestMediaRequestCount(url) >= count) {
return;
}
await new Promise<void>((resolve) => {
requestWaiters.set(token, [
...(requestWaiters.get(token) ?? []),
{ count, resolve },
]);
});
};
/** /**
* Serves a fixture at `/test-media/<file>`, behaving as the query asks: * Serves a fixture at `/test-media/<file>`, behaving as the query asks:
* *
@@ -120,7 +158,17 @@ const worker = setupWorker(
.map(Number); .map(Number);
const answered = requestCounts.get(token) ?? 0; const answered = requestCounts.get(token) ?? 0;
requestCounts.set(token, answered + 1); const counted = answered + 1;
requestCounts.set(token, counted);
const waiters = requestWaiters.get(token) ?? [];
requestWaiters.set(
token,
waiters.filter((waiter) => waiter.count > counted),
);
waiters
.filter((waiter) => waiter.count <= counted)
.forEach((waiter) => waiter.resolve());
const isPastEnd = answered >= responses.length; const isPastEnd = answered >= responses.length;
if (isPastEnd && url.searchParams.get('repeat') !== 'true') { if (isPastEnd && url.searchParams.get('repeat') !== 'true') {
@@ -19,6 +19,7 @@ import {
createUnansweredMediaURL, createUnansweredMediaURL,
getTestMediaRequestCount, getTestMediaRequestCount,
useTestMedia, useTestMedia,
waitForTestMediaRequestCount,
} from '../../../browser/test-media'; } from '../../../browser/test-media';
import { import {
CAMERA_ENTITY, CAMERA_ENTITY,
@@ -446,7 +447,8 @@ describe('MediaUnavailableIssue', () => {
cameras: [createStillImageCameraConfig(CAMERA_ENTITY, mediaURL)], cameras: [createStillImageCameraConfig(CAMERA_ENTITY, mediaURL)],
}); });
await card.waitForSelector('img'); await waitForTestMediaRequestCount(mediaURL, 1);
await card.advanceSeconds(MEDIA_LOADING_TIMEOUT_SECONDS); await card.advanceSeconds(MEDIA_LOADING_TIMEOUT_SECONDS);
expect(isIssueReported(card)).toBe(true); expect(isIssueReported(card)).toBe(true);
@@ -460,6 +462,8 @@ describe('MediaUnavailableIssue', () => {
// Past the grace period the attempt has had long enough, and the next retry // Past the grace period the attempt has had long enough, and the next retry
// replaces it. // replaces it.
await card.advanceSeconds(retrySeconds + 1); await card.advanceSeconds(retrySeconds + 1);
await waitForTestMediaRequestCount(mediaURL, 2);
expect(getTestMediaRequestCount(mediaURL)).toBeGreaterThan(1); expect(getTestMediaRequestCount(mediaURL)).toBeGreaterThan(1);
}); });