diff --git a/e2e/dashboard.spec.js b/e2e/dashboard.spec.js index 83c6669f..137b790c 100644 --- a/e2e/dashboard.spec.js +++ b/e2e/dashboard.spec.js @@ -4,7 +4,7 @@ its Content-Security-Policy in force. */ const { test, expect } = require('@playwright/test'); -const { seedConfig, dismissSetupPrompt, app } = require('./helpers'); +const { seedConfig, dismissSetupPrompt, app, widgetFramesLoaded } = require('./helpers'); /* The dashboard shows the first-run prompt until it is dismissed, and builds no tiles behind it. */ @@ -138,8 +138,7 @@ test('one rotation into the phone layout loads each widget once', async ({ page, await stubPolls(page); await page.setViewportSize({ width: 844, height: 390 }); await page.goto('/'); - await page.locator('iframe').first().waitFor({ state: 'attached' }); - await page.waitForTimeout(500); + await widgetFramesLoaded(page); let loads = 0; page.on('framenavigated', f => { if (f !== page.mainFrame() && f.url().includes('/widgets/')) loads++; @@ -157,8 +156,7 @@ test('a safe-area change rebuilds the phone layout once', async ({ page, request await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); await page.locator('body.is-mob').waitFor({ state: 'attached' }); - await page.locator('iframe').first().waitFor({ state: 'attached' }); - await page.waitForTimeout(500); + await widgetFramesLoaded(page); let loads = 0; page.on('framenavigated', f => { if (f !== page.mainFrame() && f.url().includes('/widgets/')) loads++; diff --git a/e2e/helpers.js b/e2e/helpers.js index c56a5fc6..6fb26aa0 100644 --- a/e2e/helpers.js +++ b/e2e/helpers.js @@ -200,6 +200,20 @@ async function animationsDone(page) { }); } +/** Resolve once every widget frame on the page has finished loading its widget. + @param {import('@playwright/test').Page} page */ +async function widgetFramesLoaded(page) { + await page.waitForFunction(() => { + const frames = [...document.querySelectorAll('iframe')]; + return ( + frames.length > 0 && + frames.every( + f => f.contentWindow?.location.pathname.includes('/widgets/') && f.contentDocument?.readyState === 'complete', + ) + ); + }); +} + /** The colour at one point, once it has stopped changing. A panel that fades in is still moving when it first becomes visible. @param {import('@playwright/test').Locator} locator @@ -232,5 +246,6 @@ module.exports = { pixelAt, settledPixelAt, animationsDone, + widgetFramesLoaded, contrast, }; diff --git a/e2e/tile-names.spec.js b/e2e/tile-names.spec.js index 53ba5e2c..a07ac6ec 100644 --- a/e2e/tile-names.spec.js +++ b/e2e/tile-names.spec.js @@ -7,7 +7,7 @@ tile. A badge is also not a live region. */ const { test, expect } = require('@playwright/test'); -const { seedConfig, dismissSetupPrompt, app } = require('./helpers'); +const { seedConfig, dismissSetupPrompt, app, animationsDone } = require('./helpers'); /* A host that cannot resolve, so the check fails the same way on every run. */ const DEAD = 'http://does-not-resolve.invalid/'; @@ -77,13 +77,13 @@ test.describe('tile names', () => { await seedConfig(request, { items: [app('plain', 'Plain')], settings: { showLabels: { desktop: false } } }); await page.goto('/'); await page.locator('.icon').first().waitFor({ state: 'visible' }); - await page.waitForTimeout(500); + await animationsDone(page); const without = await readLive(); await seedConfig(request, { items: ITEMS, settings: { showLabels: { desktop: false } } }); await page.goto('/'); await page.locator('.badge.on').nth(2).waitFor({ state: 'visible' }); - await page.waitForTimeout(500); + await animationsDone(page); const with3 = await readLive(); expect( diff --git a/e2e/wheel-paging.spec.js b/e2e/wheel-paging.spec.js index ade9d34d..4276636e 100644 --- a/e2e/wheel-paging.spec.js +++ b/e2e/wheel-paging.spec.js @@ -4,7 +4,7 @@ a wheel inside it never reaches the dashboard unless it is handed on. */ const { test, expect } = require('@playwright/test'); -const { seedConfig, dismissSetupPrompt, app } = require('./helpers'); +const { seedConfig, dismissSetupPrompt, app, widgetFramesLoaded } = require('./helpers'); const MANY = Array.from({ length: 48 }, (_, i) => app(`a${i}`, `App ${i}`)); const clock = { @@ -23,11 +23,7 @@ async function open(page, request) { await page.route('**/api/health', r => r.fulfill({ status: 200, contentType: 'application/json', body: '{}' })); await page.goto('/'); await page.locator('#dots .dot').nth(1).waitFor({ state: 'visible' }); - await page.locator('.widget iframe').first().waitFor({ state: 'attached' }); - await page.waitForFunction(() => { - const f = /** @type {HTMLIFrameElement|null} */ (document.querySelector('.widget iframe')); - return !!f && f.contentDocument?.readyState === 'complete'; - }); + await widgetFramesLoaded(page); } const current = page => page.locator('#dots .dot').evaluateAll(ds => ds.findIndex(d => d.classList.contains('on')));