From 4ad17f9de3fc223398905d8dbd40704ccf1c6375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 10 Jun 2026 21:32:48 +0200 Subject: [PATCH] test(e2e): pin the coach launch spec to the new chat contract The floating button only appears while a coach nudge is unread, the inline pill is the default launcher on every viewport, and the conversation list navigates to the full view instead of a tray. --- e2e/v1427-coach-launch.spec.ts | 151 ++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 50 deletions(-) diff --git a/e2e/v1427-coach-launch.spec.ts b/e2e/v1427-coach-launch.spec.ts index 3ca8c9604..7ad75be77 100644 --- a/e2e/v1427-coach-launch.spec.ts +++ b/e2e/v1427-coach-launch.spec.ts @@ -3,36 +3,40 @@ import { expect, test } from "@playwright/test"; import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** - * v1.4.27 R3d MB4 — Coach reachability on `/insights/{slug}` sub-pages - * plus the Coach drawer bottom-sheet branch on `=sm` it mounts as a - * right-side sheet (`data-variant="side-sheet"`). + * 2. Clicking the inline launch opens the Coach drawer — as a + * bottom-sheet (`data-variant="bottom-sheet"`) on `=sm`. + * + * 3. With an unread nudge the bubble mounts and clicking it + * navigates to the full-page chat at `/insights/coach`. + * + * 4. The drawer's "Conversations" button no longer opens an in-panel + * tray; it hands off to the full-page route `/insights/coach`. * * The three sub-pages covered (blutdruck, gewicht, schlaf) span the * empty-state branch (no data → CTA + Coach launch) and the populated - * branch (with data → full chart + Coach launch beneath). + * branch (with data → full chart + Coach launch in the header). */ -test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { +test.describe("Coach launch surfaces on insights sub-pages", () => { test.use({ storageState: STORAGE_STATE_PATH }); test.beforeEach(async ({ page }) => { // Populated analytics so each sub-page paints the data branch - // (`hasMetricData` true) — the Coach button is mounted at the - // bottom of the data tree. - // v1.4.39.3 — regex form matches the slim slice the v1.4.39.2 - // dashboard split fires alongside the thick request. + // (`hasMetricData` true) — the inline Coach launch is mounted in + // the sub-page header by the shell. + // Regex form matches the slim slice the dashboard split fires + // alongside the thick request. await page.route(/\/api\/analytics(\?|$)/, (route) => route.fulfill({ status: 200, @@ -103,35 +107,50 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { }), }), ); + + // Default nudge state: nothing unread. Registered AFTER the + // generic `**-status*` mock so it wins for the nudge endpoint + // (Playwright matches routes in reverse registration order). + await page.route("**/api/insights/coach/nudge-status*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { nudgedAt: null, unread: false }, + error: null, + }), + }), + ); }); for (const slug of ["blutdruck", "gewicht", "schlaf"] as const) { - test(`/insights/${slug} mounts the Coach launch button on Pixel 5 (FAB branch)`, async ({ + test(`/insights/${slug} mounts the inline Coach launch and no bubble by default on Pixel 5`, async ({ page, }, testInfo) => { test.skip( testInfo.project.name !== "chromium-mobile", - "Pixel 5 FAB branch", + "mobile inline-launch branch", ); await page.goto(`/insights/${slug}`, { waitUntil: "domcontentloaded" }); - // The FAB slot must be present and CSS-visible at Pixel 5. - const fab = page - .locator('[data-slot="coach-launch-fab"]') - .first(); - await expect(fab).toBeVisible({ timeout: 10_000 }); + // The header launch icon is the everyday entry point — visible + // on mobile too since the layout bubble is nudge-gated. + const icon = page.locator('[data-slot="coach-launch-icon"]').first(); + await expect(icon).toBeVisible({ timeout: 10_000 }); + + // No unread nudge → no floating bubble in the DOM. + await expect( + page.locator('[data-slot="coach-nudge-bubble"]'), + ).toHaveCount(0); }); - test(`/insights/${slug} mounts the header Coach icon on Desktop Chrome`, async ({ + test(`/insights/${slug} mounts the inline Coach launch and no bubble by default on Desktop Chrome`, async ({ page, }, testInfo) => { - // Desktop Chrome viewport is 1280 — `lg` (1024) is matched so the - // layout FAB is hidden by `lg:hidden`; the header icon the shell - // mounts is the Coach entry on the page itself. test.skip( testInfo.project.name !== "chromium-desktop", - "desktop header-icon branch", + "desktop inline-launch branch", ); await page.goto(`/insights/${slug}`, { waitUntil: "domcontentloaded" }); @@ -139,12 +158,13 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { const icon = page.locator('[data-slot="coach-launch-icon"]').first(); await expect(icon).toBeVisible({ timeout: 10_000 }); - const fab = page.locator('[data-slot="coach-launch-fab"]').first(); - await expect(fab).toBeHidden(); + await expect( + page.locator('[data-slot="coach-nudge-bubble"]'), + ).toHaveCount(0); }); } - test("clicking the FAB opens the Coach drawer as a bottom-sheet on Pixel 5", async ({ + test("clicking the inline Coach launch opens the drawer as a bottom-sheet on Pixel 5", async ({ page, }, testInfo) => { test.skip( @@ -154,16 +174,16 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { await page.goto("/insights/blutdruck", { waitUntil: "domcontentloaded" }); - const fab = page.locator('[data-slot="coach-launch-fab"]').first(); - await expect(fab).toBeVisible({ timeout: 10_000 }); - await fab.click(); + const icon = page.locator('[data-slot="coach-launch-icon"]').first(); + await expect(icon).toBeVisible({ timeout: 10_000 }); + await icon.click(); const drawer = page.locator('[data-slot="coach-drawer"]'); await expect(drawer).toBeVisible({ timeout: 10_000 }); await expect(drawer).toHaveAttribute("data-variant", "bottom-sheet"); }); - test("clicking the header Coach icon opens the Coach drawer as a side-sheet on Desktop Chrome", async ({ + test("clicking the inline Coach launch opens the drawer as a side-sheet on Desktop Chrome", async ({ page, }, testInfo) => { test.skip( @@ -182,12 +202,41 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { await expect(drawer).toHaveAttribute("data-variant", "side-sheet"); }); - test("conversation history is collapsed behind a toggle that opens the tray on Desktop Chrome", async ({ + test("an unread Coach nudge mounts the floating bubble and clicking it opens the full-page chat", async ({ + page, + }) => { + // The bubble contract is viewport-independent — the test runs on + // both projects. Override the default nudge mock with an unread + // one (later registration wins). + await page.route("**/api/insights/coach/nudge-status*", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { nudgedAt: "2026-06-09T06:00:00.000Z", unread: true }, + error: null, + }), + }), + ); + + await page.goto("/insights/blutdruck", { waitUntil: "domcontentloaded" }); + + const bubble = page.locator('[data-slot="coach-nudge-bubble"]'); + await expect(bubble).toBeVisible({ timeout: 10_000 }); + await bubble.click(); + + await page.waitForURL("**/insights/coach", { timeout: 10_000 }); + await expect( + page.locator('[data-slot="coach-page"]'), + ).toBeVisible({ timeout: 10_000 }); + }); + + test("the drawer's Conversations button hands off to the full-page chat on Desktop Chrome", async ({ page, }, testInfo) => { - // v1.12.0 — the history rail is no longer an always-on left column; - // it lives behind the "Conversations" toggle on every viewport and - // opens as a tray on demand so the thread keeps the full width. + // The in-panel history tray is gone from the drawer; the button + // navigates to `/insights/coach` where the conversation list + // renders inline on lg+. test.skip( testInfo.project.name !== "chromium-desktop", "exercises the desktop drawer layout", @@ -202,22 +251,24 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { const drawer = page.locator('[data-slot="coach-drawer"]'); await expect(drawer).toBeVisible({ timeout: 10_000 }); - // No inline history column — only the toggle. + // The drawer renders no inline history column. await expect( page.locator('[data-slot="coach-drawer-history"]'), ).toHaveCount(0); - const historyToggle = page.locator( + + const historyButton = page.locator( '[data-slot="coach-drawer-history-tray-trigger"]', ); - await expect(historyToggle).toBeVisible({ timeout: 10_000 }); + await expect(historyButton).toBeVisible({ timeout: 10_000 }); + await historyButton.click(); - // The history rail surfaces only after the toggle is pressed. + // No in-panel tray opens — the click navigates to the full view. + await page.waitForURL("**/insights/coach", { timeout: 10_000 }); + await expect( + page.locator('[data-slot="coach-page"]'), + ).toBeVisible({ timeout: 10_000 }); await expect( page.locator('[data-slot="coach-drawer-history-tray"]'), ).toHaveCount(0); - await historyToggle.click(); - await expect( - page.locator('[data-slot="coach-drawer-history-tray"]'), - ).toBeVisible({ timeout: 10_000 }); }); });