import { expect, test, type Page } from "@playwright/test"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; const E2E_IDENTITY_OVERRIDE_STORAGE_KEY = "sprout:e2e-identity-override.v1"; const HOME_SEEN_STORAGE_KEY_PREFIX = "sprout-home-feed-seen.v1:"; const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8); const BLANK_TYLER_IDENTITY = { ...TEST_IDENTITIES.tyler, username: "", }; type TestIdentity = { privateKey: string; pubkey: string; username: string; }; async function seedActiveIdentity(page: Page, identity: TestIdentity) { await page.addInitScript( ({ identity: nextIdentity, storageKey }) => { window.localStorage.setItem(storageKey, JSON.stringify(nextIdentity)); }, { identity, storageKey: E2E_IDENTITY_OVERRIDE_STORAGE_KEY, }, ); } async function seedOnboardingCompletion(page: Page, pubkey: string) { await page.addInitScript( ({ storageKey }) => { window.localStorage.setItem(storageKey, "true"); }, { storageKey: `sprout-onboarding-complete.v1:${pubkey}`, }, ); } async function readHomeSeenStorageKeys(page: Page) { return page.evaluate((prefix) => { return Object.keys(window.localStorage).filter((key) => key.startsWith(prefix), ); }, HOME_SEEN_STORAGE_KEY_PREFIX); } async function expectNoHomeSeenEntries(page: Page) { await expect.poll(async () => readHomeSeenStorageKeys(page)).toEqual([]); } async function expectHomeSeenCount(page: Page, expectedCount: number) { await expect .poll(async () => { return page.evaluate((prefix) => { const seenEntries = Object.entries(window.localStorage).filter( ([key]) => key.startsWith(prefix), ); if (seenEntries.length === 0) { return 0; } const [, rawValue] = seenEntries[0]; const parsed = JSON.parse(rawValue ?? "[]"); return Array.isArray(parsed) ? parsed.length : 0; }, HOME_SEEN_STORAGE_KEY_PREFIX); }) .toBe(expectedCount); } async function expectShellHidden(page: Page) { await expect(page.getByTestId("app-sidebar")).toHaveCount(0); await expect(page.getByTestId("chat-title")).toHaveCount(0); } async function expectIncompleteOnboarding(page: Page) { await expect(page.getByTestId("onboarding-gate")).toBeVisible(); await expectShellHidden(page); await expect(page.getByTestId("onboarding-page-1")).toBeVisible(); await expect(page.getByTestId("onboarding-display-name")).toHaveValue(""); } async function continueToSetupPage(page: Page) { await page.getByTestId("onboarding-next").click(); await expect(page.getByTestId("onboarding-page-2")).toBeVisible(); } test("completed users skip the loading gate while profile is still settling", async ({ page, }) => { await seedOnboardingCompletion(page, DEFAULT_MOCK_PUBKEY); await installMockBridge(page, { profileReadDelayMs: 3_000, }); await page.goto("/"); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); await expect(page.getByTestId("chat-title")).toHaveText("Home"); }); test("identity fallback text does not count as a real onboarding name", async ({ page, }) => { await installMockBridge(page, undefined, { skipOnboardingSeed: true }); await page.goto("/"); await expectIncompleteOnboarding(page); await expect(page.getByTestId("onboarding-avatar-upload")).toHaveText( "Upload photo", ); await expect(page.getByTestId("onboarding-avatar-url")).toHaveValue(""); await expect(page.getByTestId("onboarding-next")).toBeDisabled(); }); test("page 1 accepts an avatar URL as the secondary avatar path", async ({ page, }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await installMockBridge(page, undefined, { skipOnboardingSeed: true }); await page.goto("/"); await page.getByTestId("onboarding-display-name").fill("Morty QA"); await page .getByTestId("onboarding-avatar-url") .fill("https://example.com/morty.png"); const preview = page.getByTestId("onboarding-avatar-preview"); await expect(preview).toBeVisible(); const box = await preview.boundingBox(); expect(box?.width).toBeCloseTo(80, 0); expect(box?.height).toBeCloseTo(80, 0); await continueToSetupPage(page); await expect(page.getByTestId("onboarding-provider-goose")).toBeVisible(); }); test("first-run onboarding keeps the shell hidden through both pages and only marks Home seen after finish", async ({ page, }) => { await seedActiveIdentity(page, TEST_IDENTITIES.alice); await installMockBridge(page, undefined, { skipOnboardingSeed: true }); await page.goto("/"); await expect(page.getByTestId("onboarding-gate")).toBeVisible(); await expect(page.getByTestId("onboarding-page-1")).toBeVisible(); await expect(page.getByTestId("onboarding-display-name")).toHaveValue( "alice", ); await expectNoHomeSeenEntries(page); await continueToSetupPage(page); await expectShellHidden(page); await expect(page.getByTestId("onboarding-provider-goose")).toBeVisible(); await expectNoHomeSeenEntries(page); await page.getByTestId("onboarding-finish").click(); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); await expect(page.getByTestId("chat-title")).toHaveText("Home"); await expectHomeSeenCount(page, 2); }); test("finishing onboarding auto-joins the #general channel for a new member", async ({ page, }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await installMockBridge(page, undefined, { skipOnboardingSeed: true }); await page.goto("/"); await page.getByTestId("onboarding-display-name").fill("Morty QA"); await continueToSetupPage(page); await page.getByTestId("onboarding-finish").click(); await expect(page.getByTestId("chat-title")).toHaveText("Home"); await expect(page.getByTestId("channel-general")).toBeVisible(); }); test("page 2 falls back to Doctor guidance when ACP tools are not installed", async ({ page, }) => { await seedActiveIdentity(page, TEST_IDENTITIES.alice); await installMockBridge( page, { acpProviders: [], }, { skipOnboardingSeed: true }, ); await page.goto("/"); await continueToSetupPage(page); await expect(page.getByTestId("onboarding-acp-empty")).toBeVisible(); await expect( page.getByText("Settings > Doctor", { exact: false }), ).toBeVisible(); }); test("initial profile read failures still hold incomplete users in onboarding", async ({ page, }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await installMockBridge( page, { profileReadError: "Temporary profile read failure.", }, { skipOnboardingSeed: true }, ); await page.goto("/"); await expectIncompleteOnboarding(page); }); test("failed first profile saves can be skipped for the current session", async ({ page, }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await installMockBridge( page, { profileUpdateError: "Temporary profile sync failure.", }, { skipOnboardingSeed: true }, ); await page.goto("/"); await expect(page.getByTestId("onboarding-gate")).toBeVisible(); await expect(page.getByTestId("onboarding-display-name")).toHaveValue(""); await page.getByTestId("onboarding-display-name").fill("Morty QA"); await page.getByTestId("onboarding-next").click(); await expect(page.getByText("Temporary profile sync failure.")).toBeVisible(); await page.getByTestId("onboarding-skip").click(); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); await expect(page.getByTestId("chat-title")).toHaveText("Home"); });