From 28307ee1ad7efb379b9d19e894768301ea2ff4f1 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Tue, 27 Oct 2026 01:19:39 +0100 Subject: [PATCH] test(webapp): wait for a navigation before asking which one the viewport draws Five library-cache tests and both navigateWithin helpers chose between the phone's bottom bar and the sidebar with isVisible(), which answers at once. Asked while the shell was still mounting, it saw neither: on a phone the tests then looked for their links in the sidebar until they timed out, which is how 'takes the current tab back to its plain address' failed once in a full run on merged main (16/16 alone). onPhoneNavigation waits for whichever navigation is drawn, then asks. Signed-off-by: InstaZDLL --- webapp/e2e/fixtures/navigation.ts | 21 +++++++++++++++++++ webapp/e2e/fixtures/radio.ts | 3 ++- webapp/e2e/library-cache.spec.ts | 11 +++++----- .../e2e/smart-playlists-and-podcasts.spec.ts | 4 +++- 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 webapp/e2e/fixtures/navigation.ts diff --git a/webapp/e2e/fixtures/navigation.ts b/webapp/e2e/fixtures/navigation.ts new file mode 100644 index 00000000..8de1cf44 --- /dev/null +++ b/webapp/e2e/fixtures/navigation.ts @@ -0,0 +1,21 @@ +// Which of the two navigations the viewport draws, asked once it has drawn one. + +import type { Page } from "@playwright/test"; + +/** + * Whether the page shows the phone's bottom navigation rather than the + * sidebar. + * + * `isVisible()` answers at once, without waiting. Asked while the shell was + * still mounting, it saw neither navigation, and a test on a phone then looked + * for its links in the sidebar a phone never draws, until it timed out — on a + * loaded machine, now and then. This waits for whichever one the viewport + * draws, then asks. + */ +export async function onPhoneNavigation(page: Page): Promise { + await page + .locator(".mobile-navigation:visible, .primary-navigation:visible") + .first() + .waitFor(); + return page.locator(".mobile-navigation").isVisible(); +} diff --git a/webapp/e2e/fixtures/radio.ts b/webapp/e2e/fixtures/radio.ts index 07d53366..e73ebebe 100644 --- a/webapp/e2e/fixtures/radio.ts +++ b/webapp/e2e/fixtures/radio.ts @@ -3,6 +3,7 @@ import type { Page } from "@playwright/test"; import { silentWav } from "./media"; +import { onPhoneNavigation } from "./navigation"; import { mock } from "./state"; /** @@ -12,7 +13,7 @@ import { mock } from "./state"; */ export async function navigateWithin(page: Page, name: string) { const sidebar = page.locator(".sidebar"); - if (await sidebar.isVisible()) { + if (!(await onPhoneNavigation(page))) { await sidebar.getByRole("link", { name, exact: true }).click(); return; } diff --git a/webapp/e2e/library-cache.spec.ts b/webapp/e2e/library-cache.spec.ts index 825a9eab..0cda5411 100644 --- a/webapp/e2e/library-cache.spec.ts +++ b/webapp/e2e/library-cache.spec.ts @@ -4,6 +4,7 @@ import { expect, test } from "@playwright/test"; import { resetAndMockApi } from "./fixtures/api"; import { albums, session, track } from "./fixtures/catalogue"; +import { onPhoneNavigation } from "./fixtures/navigation"; import { mock } from "./fixtures/state"; test.beforeEach(resetAndMockApi); @@ -31,7 +32,7 @@ test("keeps the current tab visible while the next tab loads", async ({ await page.goto("/"); await expect(page.getByRole("heading", { name: "Albums" })).toBeVisible(); - const mobile = await page.locator(".mobile-navigation").isVisible(); + const mobile = await onPhoneNavigation(page); const genresLink = mobile ? page.locator('.more-sheet a[href="/genres"]') : page.locator(".primary-navigation").getByRole("link", { name: "Genres" }); @@ -76,7 +77,7 @@ test("shows the next tab with its wait when the answer is slow", async ({ await page.goto("/"); await expect(page.getByRole("heading", { name: "Albums" })).toBeVisible(); - const mobile = await page.locator(".mobile-navigation").isVisible(); + const mobile = await onPhoneNavigation(page); if (mobile) { await page .locator(".mobile-navigation") @@ -102,7 +103,7 @@ test("shows the next tab with its wait when the answer is slow", async ({ test("takes the current tab back to its plain address", async ({ page }) => { await page.goto("/playing?view=lyrics"); await expect(page).toHaveURL(/\/playing\?view=lyrics$/); - const navigation = (await page.locator(".mobile-navigation").isVisible()) + const navigation = (await onPhoneNavigation(page)) ? page.locator(".mobile-navigation") : page.locator(".primary-navigation"); await navigation.getByRole("link", { name: "Now playing" }).click(); @@ -128,7 +129,7 @@ test("draws the random tab once when switching to it", async ({ page }) => { await page.goto("/"); await expect(page.getByRole("heading", { name: "Albums" })).toBeVisible(); - if (await page.locator(".mobile-navigation").isVisible()) { + if (await onPhoneNavigation(page)) { await page.locator(".mobile-navigation").getByRole("button", { name: "More" }).click(); await page.locator('.more-sheet a[href="/random"]').click(); } else { @@ -170,7 +171,7 @@ test("leaves a tab switch behind when the visitor moves on while it loads", asyn await page.goto("/"); await expect(page.getByRole("heading", { name: "Albums" })).toBeVisible(); - const mobile = await page.locator(".mobile-navigation").isVisible(); + const mobile = await onPhoneNavigation(page); if (mobile) { await page.locator(".mobile-navigation").getByRole("button", { name: "More" }).click(); } diff --git a/webapp/e2e/smart-playlists-and-podcasts.spec.ts b/webapp/e2e/smart-playlists-and-podcasts.spec.ts index 61565773..b0c3d716 100644 --- a/webapp/e2e/smart-playlists-and-podcasts.spec.ts +++ b/webapp/e2e/smart-playlists-and-podcasts.spec.ts @@ -1,6 +1,8 @@ import AxeBuilder from "@axe-core/playwright"; import { expect, type Page, test } from "@playwright/test"; +import { onPhoneNavigation } from "./fixtures/navigation"; + /** * Smart playlists and podcasts (RFC-025), against a mock that keeps what it is * sent: an editor that saved nothing, or a download that changed nothing, @@ -646,7 +648,7 @@ function episodeElement(page: Page, ticket: string) { */ async function navigateWithin(page: Page, name: string) { const sidebar = page.locator(".sidebar"); - if (await sidebar.isVisible()) { + if (!(await onPhoneNavigation(page))) { await sidebar.getByRole("link", { name, exact: true }).click(); return; }