mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
752 lines
24 KiB
TypeScript
752 lines
24 KiB
TypeScript
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
import { nsecEncode } from "nostr-tools/nip19";
|
|
|
|
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: "",
|
|
};
|
|
const BLANK_AVATAR_PLACEHOLDER_IDENTITY = {
|
|
...TEST_IDENTITIES.tyler,
|
|
pubkey: "1".repeat(64),
|
|
username: "",
|
|
};
|
|
const BLANK_AVATAR_EMOJI_IDENTITY = {
|
|
...TEST_IDENTITIES.tyler,
|
|
pubkey: "2".repeat(64),
|
|
username: "",
|
|
};
|
|
const FIRST_RUN_ALICE = {
|
|
...TEST_IDENTITIES.alice,
|
|
username: "",
|
|
};
|
|
const FIRST_RUN_KENNY = {
|
|
...TEST_IDENTITIES.tyler,
|
|
username: "Kenny QA",
|
|
};
|
|
|
|
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 selectFirstEmojiFromPicker(page: Page) {
|
|
const picker = page.locator("em-emoji-picker");
|
|
await expect(picker).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
picker.evaluate((element) =>
|
|
Boolean(element.shadowRoot?.querySelector(".scroll button")),
|
|
),
|
|
)
|
|
.toBe(true);
|
|
await picker.evaluate((element) => {
|
|
const button = element.shadowRoot?.querySelector(".scroll button");
|
|
if (!(button instanceof HTMLElement)) {
|
|
throw new Error("Emoji picker did not render an emoji button.");
|
|
}
|
|
button.click();
|
|
});
|
|
}
|
|
|
|
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 expectHomeView(page: Page) {
|
|
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
|
}
|
|
|
|
async function getMockProfile(page: Page) {
|
|
return page.evaluate(async () => {
|
|
const invoke = (
|
|
window as Window & {
|
|
__SPROUT_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
}
|
|
).__SPROUT_E2E_INVOKE_MOCK_COMMAND__;
|
|
if (!invoke) {
|
|
throw new Error("Mock invoke bridge is unavailable.");
|
|
}
|
|
|
|
return (await invoke("get_profile")) as {
|
|
avatar_url: string | null;
|
|
display_name: string | null;
|
|
};
|
|
});
|
|
}
|
|
|
|
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-avatar")).toBeVisible();
|
|
await page
|
|
.getByTestId("onboarding-avatar-url")
|
|
.fill("https://example.com/onboarding-avatar.png");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-theme")).toBeVisible();
|
|
await page.getByTestId("onboarding-theme-option-github-light").click();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-theme")),
|
|
)
|
|
.toBe("github-light");
|
|
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 expectHomeView(page);
|
|
});
|
|
|
|
test("first-run default workspace handoff gives immediate stepper feedback", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, FIRST_RUN_KENNY);
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
profileReadDelayMs: 2_000,
|
|
},
|
|
{ skipOnboardingSeed: true, skipWorkspaceSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByText("Welcome to Sprout")).toBeVisible();
|
|
await page
|
|
.getByRole("button", { name: "Continue with Block Inc. workspace" })
|
|
.click();
|
|
|
|
await page.waitForTimeout(80);
|
|
await expect(page.getByRole("button", { name: "Connecting..." })).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(
|
|
page.getByRole("button", { name: "Continue with Block Inc. workspace" }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("progressbar")).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"2",
|
|
);
|
|
await page.waitForTimeout(240);
|
|
await expect(page.getByTestId("welcome-continue-nostr")).toBeVisible();
|
|
await expect(page.getByRole("progressbar")).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"2",
|
|
);
|
|
|
|
const nameInput = page.getByTestId("onboarding-display-name");
|
|
await expect(nameInput).toHaveValue("Kenny QA");
|
|
await expect(page.getByRole("progressbar")).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"2",
|
|
);
|
|
await expect(nameInput).toHaveAttribute("autocomplete", "off");
|
|
await expect(page.getByTestId("onboarding-back")).toBeVisible();
|
|
});
|
|
|
|
test("welcome can continue using an existing Nostr key", async ({ page }) => {
|
|
await installMockBridge(page, undefined, {
|
|
skipOnboardingSeed: true,
|
|
skipWorkspaceSeed: true,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("welcome-continue-nostr").click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Continue using Nostr" }),
|
|
).toBeVisible();
|
|
|
|
const importedNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.alice.privateKey));
|
|
await page.getByTestId("welcome-nostr-nsec-input").fill(importedNsec);
|
|
await expect(page.getByTestId("welcome-nostr-npub-preview")).toBeVisible();
|
|
await page.getByTestId("welcome-nostr-submit").click();
|
|
|
|
await expect(page.getByTestId("onboarding-display-name")).toHaveValue(
|
|
"alice",
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const rawWorkspaces = window.localStorage.getItem("sprout-workspaces");
|
|
const workspaces = rawWorkspaces
|
|
? (JSON.parse(rawWorkspaces) as Array<{ pubkey?: string }>)
|
|
: [];
|
|
return workspaces[0]?.pubkey ?? null;
|
|
}),
|
|
)
|
|
.toBe(TEST_IDENTITIES.alice.pubkey);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate((storageKey) => {
|
|
const rawIdentity = window.localStorage.getItem(storageKey);
|
|
const identity = rawIdentity
|
|
? (JSON.parse(rawIdentity) as { pubkey?: string })
|
|
: null;
|
|
return identity?.pubkey ?? null;
|
|
}, E2E_IDENTITY_OVERRIDE_STORAGE_KEY),
|
|
)
|
|
.toBe(TEST_IDENTITIES.alice.pubkey);
|
|
});
|
|
|
|
test("welcome presents custom workspace setup as joining a workspace", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, undefined, {
|
|
skipOnboardingSeed: true,
|
|
skipWorkspaceSeed: true,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await page.getByRole("button", { name: "Join a workspace" }).click();
|
|
|
|
await expect(
|
|
page.getByRole("heading", { name: "Join a workspace" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Join a workspace" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
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-next")).toBeDisabled();
|
|
});
|
|
|
|
test("avatar step uses an add-image placeholder before an avatar is chosen", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, BLANK_AVATAR_PLACEHOLDER_IDENTITY);
|
|
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
const preview = page.getByTestId("onboarding-avatar-preview");
|
|
await expect(preview).toBeVisible();
|
|
await expect(preview).toHaveAttribute("aria-label", "Add a display image");
|
|
await expect(preview).toHaveClass(/border-dashed/);
|
|
});
|
|
|
|
test("avatar step reveals preset backgrounds after the first emoji pick", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, BLANK_AVATAR_EMOJI_IDENTITY);
|
|
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
|
|
await page.getByRole("tab", { name: "Emoji" }).click();
|
|
|
|
const colorGridShell = page.getByTestId("onboarding-avatar-color-grid-shell");
|
|
await expect(colorGridShell).toHaveAttribute("aria-hidden", "true");
|
|
|
|
await selectFirstEmojiFromPicker(page);
|
|
|
|
await expect(colorGridShell).toHaveAttribute("aria-hidden", "false");
|
|
await expect(page.getByTestId("onboarding-avatar-color-grid")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-avatar-preview")).not.toHaveCSS(
|
|
"background-color",
|
|
"rgb(255, 255, 255)",
|
|
);
|
|
});
|
|
|
|
test("avatar step accepts an avatar URL before theme selection", 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-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
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(192, 0);
|
|
expect(box?.height).toBeCloseTo(192, 0);
|
|
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-theme")).toBeVisible();
|
|
await page.getByTestId("onboarding-theme-option-github-light").click();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-theme")),
|
|
)
|
|
.toBe("github-light");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-goose")).toBeVisible();
|
|
});
|
|
|
|
test("failed avatar saves can continue without saving the avatar", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
|
await installMockBridge(page, {}, { skipOnboardingSeed: true });
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
await page
|
|
.getByTestId("onboarding-avatar-url")
|
|
.fill("https://example.com/morty.png");
|
|
await page.evaluate(() => {
|
|
const testWindow = window as Window & {
|
|
__SPROUT_E2E__?: { mock?: { profileUpdateError?: string } };
|
|
};
|
|
if (testWindow.__SPROUT_E2E__?.mock) {
|
|
testWindow.__SPROUT_E2E__.mock.profileUpdateError =
|
|
"Temporary avatar sync failure.";
|
|
}
|
|
});
|
|
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByText("Temporary avatar sync failure.")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("onboarding-next-without-saving"),
|
|
).toBeVisible();
|
|
await page.getByTestId("onboarding-next-without-saving").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-theme")).toBeVisible();
|
|
});
|
|
|
|
test("theme step offers skip instead of going back", 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-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
await page
|
|
.getByTestId("onboarding-avatar-url")
|
|
.fill("https://example.com/morty.png");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-theme")),
|
|
)
|
|
.toBe("github-light-default");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-accent-color")),
|
|
)
|
|
.toBe("neutral");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => document.documentElement.classList.contains("light")),
|
|
)
|
|
.toBe(true);
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-theme")).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-theme")),
|
|
)
|
|
.toBe("github-light-default");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-accent-color")),
|
|
)
|
|
.toBe("neutral");
|
|
await expect(
|
|
page.getByTestId("onboarding-theme-option-github-light-default"),
|
|
).toHaveAttribute("aria-pressed", "true");
|
|
await expect(
|
|
page.getByTestId("onboarding-accent-color-neutral"),
|
|
).toHaveAttribute("aria-pressed", "true");
|
|
await page.getByTestId("onboarding-accent-color-blue").click();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-accent-color")),
|
|
)
|
|
.toBe("#3b82f6");
|
|
await page.getByTestId("onboarding-accent-color-neutral").click();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.localStorage.getItem("sprout-accent-color")),
|
|
)
|
|
.toBe("neutral");
|
|
await expect(page.getByTestId("onboarding-back")).toHaveCount(0);
|
|
await page.getByTestId("onboarding-skip").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
});
|
|
|
|
test("avatar upload rejects a file whose server-detected MIME is not an image", async ({
|
|
page,
|
|
}) => {
|
|
// Models a spoofed/blank picker MIME: the picked file claims to be an image
|
|
// (passes the browser-side accept filter) but the shared generic upload path
|
|
// returns a non-image descriptor. The post-upload backstop must reject it so
|
|
// a non-image can't become an avatar (regression guard — the shared upload
|
|
// path no longer rejects non-images server-side).
|
|
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
uploadDescriptors: [
|
|
{
|
|
url: `https://mock.relay/media/${"b".repeat(64)}.pdf`,
|
|
sha256: "b".repeat(64),
|
|
size: 4096,
|
|
type: "application/pdf",
|
|
uploaded: Math.floor(Date.now() / 1000),
|
|
filename: "not-an-image.pdf",
|
|
},
|
|
],
|
|
},
|
|
{ skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
await page.getByTestId("onboarding-avatar-input").setInputFiles({
|
|
name: "looks-like.png",
|
|
mimeType: "image/png",
|
|
buffer: Buffer.from("not really a png"),
|
|
});
|
|
|
|
await expect(page.getByRole("alert")).toContainText(
|
|
"Choose a PNG, JPG, GIF, or WebP image.",
|
|
);
|
|
await expect(page.getByTestId("onboarding-avatar-url")).toHaveValue("");
|
|
});
|
|
|
|
test("avatar upload accepts a file whose server-detected MIME is an image", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
|
const url = `https://mock.relay/media/${"c".repeat(64)}.png`;
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
uploadDescriptors: [
|
|
{
|
|
url,
|
|
sha256: "c".repeat(64),
|
|
size: 2048,
|
|
type: "image/png",
|
|
uploaded: Math.floor(Date.now() / 1000),
|
|
},
|
|
],
|
|
},
|
|
{ skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
await page.getByTestId("onboarding-avatar-input").setInputFiles({
|
|
name: "avatar.png",
|
|
mimeType: "image/png",
|
|
buffer: Buffer.from("png bytes"),
|
|
});
|
|
|
|
await expect(page.getByTestId("onboarding-avatar-url")).toHaveValue(url);
|
|
await expect(page.getByTestId("onboarding-avatar-error")).toHaveCount(0);
|
|
});
|
|
|
|
test("first-run onboarding keeps the shell hidden through setup and only marks Home seen after finish", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, FIRST_RUN_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("");
|
|
await expectNoHomeSeenEntries(page);
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Alice");
|
|
await continueToSetupPage(page);
|
|
await expectShellHidden(page);
|
|
await expect(page.getByTestId("onboarding-runtime-goose")).toBeVisible();
|
|
await expectNoHomeSeenEntries(page);
|
|
|
|
await page.getByTestId("onboarding-finish").click();
|
|
await expect(page.getByTestId("onboarding-gate")).toHaveCount(0);
|
|
await expectHomeView(page);
|
|
await expectHomeSeenCount(page, 2);
|
|
});
|
|
|
|
test("existing relay profile prefills the name step without localStorage completion", 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 expectShellHidden(page);
|
|
await expect(page.getByTestId("onboarding-display-name")).toHaveValue(
|
|
"alice",
|
|
);
|
|
await expect(page.getByTestId("onboarding-next")).toBeEnabled();
|
|
await expect(page.getByTestId("onboarding-back")).toHaveCount(0);
|
|
});
|
|
|
|
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 expectHomeView(page);
|
|
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, FIRST_RUN_ALICE);
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [],
|
|
},
|
|
{ skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Alice");
|
|
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 expectHomeView(page);
|
|
});
|
|
|
|
test("failed saved profile saves can continue without retrying the display name", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, TEST_IDENTITIES.alice);
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
profileUpdateError: "Temporary profile sync failure.",
|
|
},
|
|
{ skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("onboarding-display-name")).toHaveValue(
|
|
"alice",
|
|
);
|
|
await page.getByTestId("onboarding-display-name").fill("Alice Draft");
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByText("Temporary profile sync failure.")).toBeVisible();
|
|
await page.getByTestId("onboarding-next-without-saving").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
const avatarUrl = "https://example.com/alice-onboarding-avatar.png";
|
|
await page.getByTestId("onboarding-avatar-url").fill(avatarUrl);
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-theme")).toBeVisible();
|
|
await expect(await getMockProfile(page)).toMatchObject({
|
|
avatar_url: avatarUrl,
|
|
display_name: "alice",
|
|
});
|
|
});
|
|
|
|
test("membership denial can import a different invited key", async ({
|
|
page,
|
|
}) => {
|
|
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
relayRole: null,
|
|
},
|
|
{ skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
|
|
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByTestId("membership-denied")).toBeVisible();
|
|
await page.getByTestId("membership-denied-change-key").click();
|
|
|
|
const importedNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.alice.privateKey));
|
|
await page.getByTestId("membership-denied-nsec-input").fill(importedNsec);
|
|
await expect(
|
|
page.getByTestId("membership-denied-npub-preview"),
|
|
).toBeVisible();
|
|
await page.getByTestId("membership-denied-import-key").click();
|
|
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as Window & { __SPROUT_E2E_COMMANDS__?: string[] })
|
|
.__SPROUT_E2E_COMMANDS__ ?? [],
|
|
),
|
|
)
|
|
.toEqual(expect.arrayContaining(["plugin:websocket|disconnect"]));
|
|
await expect(page.getByTestId("onboarding-page-1")).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate((storageKey) => {
|
|
const rawIdentity = window.localStorage.getItem(storageKey);
|
|
const identity = rawIdentity
|
|
? (JSON.parse(rawIdentity) as { pubkey?: string })
|
|
: null;
|
|
return identity?.pubkey ?? null;
|
|
}, E2E_IDENTITY_OVERRIDE_STORAGE_KEY),
|
|
)
|
|
.toBe(TEST_IDENTITIES.alice.pubkey);
|
|
await page.getByTestId("onboarding-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-avatar")).toBeVisible();
|
|
});
|