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; }