From 53e9c72364812e645cf0b22a7c5ebd433da0ce07 Mon Sep 17 00:00:00 2001
From: Ryan Dombrowski
diff --git a/e2e/tour-first-run.spec.ts b/e2e/tour-first-run.spec.ts new file mode 100644 index 0000000..8842209 --- /dev/null +++ b/e2e/tour-first-run.spec.ts @@ -0,0 +1,41 @@ +/** + * First-visit tour auto-start, against the static artifact. + * + * Runs ONLY in the "first-run" Playwright project, which (unlike "studio") + * seeds no storageState: every test here starts as a genuine first-time + * visitor. The rest of the suite runs as a returning visitor by construction. + */ +import { expect, test } from "@playwright/test"; + +test("a first-time visitor lands in the tour, on a real recorded state", async ({ page }) => { + await page.goto("/"); + + // Step 1 of the tour: the failed gate moment of the real repair run — + // the same state tour-xray-wire.spec.ts asserts after a manual start. + await expect(page.getByTestId("tour-bar")).toBeVisible(); + await expect(page.getByTestId("tour-title")).toContainText("argues back"); + await expect(page.getByTestId("gate-ticker")).toContainText("S3✗"); +}); + +test("the tour never recurs after dismissal, and stays restartable", async ({ page }) => { + await page.goto("/"); + await expect(page.getByTestId("tour-bar")).toBeVisible(); + await page.getByTestId("tour-skip").click(); + await expect(page.getByTestId("tour-bar")).toHaveCount(0); + + await page.reload(); + // A dismissed tour is remembered: the studio loads directly, with the + // manual entry point still present. + await expect(page.getByTestId("scenario-project-deletion")).toBeVisible(); + await expect(page.getByTestId("tour-bar")).toHaveCount(0); + await expect(page.getByTestId("tour-start")).toBeVisible(); +}); + +test("deep-linked first-time visitors are never hijacked by the tour", async ({ page }) => { + await page.goto("/#s=project-deletion&f=clean&e=11"); + // The permalink resolves to its own state... + await expect(page.getByTestId("gate-ticker")).toBeVisible(); + await expect(page.getByTestId("scenario-project-deletion")).toBeVisible(); + // ...and the tour stays out of the way. + await expect(page.getByTestId("tour-bar")).toHaveCount(0); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 6441a9b..aa67334 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,6 +9,8 @@ import { defineConfig } from "@playwright/test"; * Run `pnpm --filter web build` first (CI does; the webServer commands below * do not rebuild to keep local iteration fast). */ +const BASE_URL = "http://localhost:3311"; + export default defineConfig({ testDir: "e2e", timeout: 30_000, @@ -18,16 +20,40 @@ export default defineConfig({ // so parallel workers can race each other through it. One worker keeps // every run deterministic; the static production suite stays parallel. workers: 1, - // prod-smoke asserts the DEPLOYED site's properties (no local agent, - // injected analytics); it runs via playwright.production.config.ts only. - testIgnore: ["prod-smoke.spec.ts"], use: { - baseURL: "http://localhost:3311", + baseURL: BASE_URL, // Deterministic CI: the homepage is alive by default (auto-plays the // first recording); under reduced motion it jumps to the finished // surface instead, which is also the stable state tests start from. contextOptions: { reducedMotion: "reduce" }, }, + // The tour auto-starts for first-time visitors, so visitor history is part + // of every test's premise. Two projects make it explicit: "first-run" + // starts with clean storage (the auto-start spec), "studio" is seeded as a + // returning visitor so every other spec keeps asserting the plain studio. + projects: [ + { + name: "first-run", + testMatch: "tour-first-run.spec.ts", + }, + { + name: "studio", + // prod-smoke asserts the DEPLOYED site's properties (no local agent, + // injected analytics); it runs via playwright.production.config.ts only. + testIgnore: ["prod-smoke.spec.ts", "tour-first-run.spec.ts"], + use: { + storageState: { + cookies: [], + origins: [ + { + origin: BASE_URL, + localStorage: [{ name: "dspack-studio-tour-done", value: "1" }], + }, + ], + }, + }, + }, + ], webServer: [ { command: "node e2e/serve-static.mjs", diff --git a/playwright.production.config.ts b/playwright.production.config.ts index 04180c7..7df7b55 100644 --- a/playwright.production.config.ts +++ b/playwright.production.config.ts @@ -8,34 +8,59 @@ import { defineConfig } from "@playwright/test"; * * npx playwright test --config playwright.production.config.ts */ +const PROD = process.env.PROD_URL ?? "https://studio.aesthetic-function.com"; + export default defineConfig({ testDir: "e2e", - testMatch: [ - "replay.spec.ts", - "alive.spec.ts", - "permalinks.spec.ts", - "tour-xray-wire.spec.ts", - "receipts.spec.ts", - "fork.spec.ts", - "fixture-006.spec.ts", - "inspector.spec.ts", - "wire.spec.ts", - "a11y.spec.ts", - "studio-shell.spec.ts", - // Replay + restyle only; agent-free by construction. - "design-swap.spec.ts", - // The deployed site has no agent, which is exactly the state this suite - // reproduces locally by blocking the port — it runs as-is in production. - "break-offline.spec.ts", - // Agent-free by construction (blocks the agent port itself); the - // validator, config, and downloads are static-site capabilities. - "take-home.spec.ts", - "prod-smoke.spec.ts", - ], timeout: 30_000, retries: 1, use: { - baseURL: process.env.PROD_URL ?? "https://studio.aesthetic-function.com", + baseURL: PROD, contextOptions: { reducedMotion: "reduce" }, }, + // Mirrors playwright.config.ts: "first-run" asserts the deployed + // first-visit tour auto-start with clean storage; "studio" runs everything + // else as a returning visitor (seeded tour-done) so no spec meets the tour. + projects: [ + { + name: "first-run", + testMatch: "tour-first-run.spec.ts", + }, + { + name: "studio", + testMatch: [ + "replay.spec.ts", + "alive.spec.ts", + "permalinks.spec.ts", + "tour-xray-wire.spec.ts", + "receipts.spec.ts", + "fork.spec.ts", + "fixture-006.spec.ts", + "inspector.spec.ts", + "wire.spec.ts", + "a11y.spec.ts", + "studio-shell.spec.ts", + // Replay + restyle only; agent-free by construction. + "design-swap.spec.ts", + // The deployed site has no agent, which is exactly the state this suite + // reproduces locally by blocking the port — it runs as-is in production. + "break-offline.spec.ts", + // Agent-free by construction (blocks the agent port itself); the + // validator, config, and downloads are static-site capabilities. + "take-home.spec.ts", + "prod-smoke.spec.ts", + ], + use: { + storageState: { + cookies: [], + origins: [ + { + origin: new URL(PROD).origin, + localStorage: [{ name: "dspack-studio-tour-done", value: "1" }], + }, + ], + }, + }, + }, + ], });