diff --git a/tests/browser/test-media.ts b/tests/browser/test-media.ts index 2555718f..7cc1f2bf 100644 --- a/tests/browser/test-media.ts +++ b/tests/browser/test-media.ts @@ -14,6 +14,10 @@ const TEST_MEDIA_PATH = '/test-media'; // page runs one test file, so nothing here is shared with another file. const requestCounts = new Map(); +// Tests waiting for a token to reach a request count, resolved by the worker +// below as it counts. +const requestWaiters = new Map void }[]>(); + // Whether this page's tests asked for the worker. Recorded so that a URL only // the worker can answer cannot be built without it. let inUse = false; @@ -75,15 +79,49 @@ export const createUnansweredMediaURL = (): string => createTestMediaURL([]); export const createStallingMediaURL = (filename?: string): string => 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 * the card actually fetched rather than only what it displayed. */ 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; }; +/** + * 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 => { + 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((resolve) => { + requestWaiters.set(token, [ + ...(requestWaiters.get(token) ?? []), + { count, resolve }, + ]); + }); +}; + /** * Serves a fixture at `/test-media/`, behaving as the query asks: * @@ -120,7 +158,17 @@ const worker = setupWorker( .map(Number); 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; if (isPastEnd && url.searchParams.get('repeat') !== 'true') { diff --git a/tests/card-controller/issues/issues/media-unavailable.browser.test.ts b/tests/card-controller/issues/issues/media-unavailable.browser.test.ts index 92092e0a..c34f6040 100644 --- a/tests/card-controller/issues/issues/media-unavailable.browser.test.ts +++ b/tests/card-controller/issues/issues/media-unavailable.browser.test.ts @@ -19,6 +19,7 @@ import { createUnansweredMediaURL, getTestMediaRequestCount, useTestMedia, + waitForTestMediaRequestCount, } from '../../../browser/test-media'; import { CAMERA_ENTITY, @@ -446,7 +447,8 @@ describe('MediaUnavailableIssue', () => { cameras: [createStillImageCameraConfig(CAMERA_ENTITY, mediaURL)], }); - await card.waitForSelector('img'); + await waitForTestMediaRequestCount(mediaURL, 1); + await card.advanceSeconds(MEDIA_LOADING_TIMEOUT_SECONDS); 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 // replaces it. await card.advanceSeconds(retrySeconds + 1); + await waitForTestMediaRequestCount(mediaURL, 2); + expect(getTestMediaRequestCount(mediaURL)).toBeGreaterThan(1); });