test(onboarding): walk the onboarding questions end to end; mobile reads cards as text

- onboarding-intake.spec walks focus, goal (after typing into the composer, which still
  works), check-ins, quiet hours and the notifications card when the browser offers it,
  with the onboarding-intake-goal, -rhythm and -done screenshots.
- completeOnboarding and the wizard specs drop the preferences step; the scan step is
  optional (skipped when it asks, otherwise it advances by itself).
- The user-profile spec now checks the browser-filled profile and the focus fact; the
  conversation spec checks the shorter app suggestion and the following goal question.
- The hover spec measures a reply that fills its row (a bot row's rail sits at the row's
  end, so the old long onboarding line was what made it pass) and scopes Send to the
  composer, since an open goal card has its own.
- Mobile shows a choice card as its question and answer instead of an empty bubble.
- The focus options drop their unused summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
Claude 2026-10-03 14:09:56 +00:00
parent 3db0f0cfbd
commit 97210805be
No known key found for this signature in database
11 changed files with 172 additions and 98 deletions

View File

@ -52,7 +52,6 @@ export interface FocusOption {
id: string; id: string;
letter: string; letter: string;
label: string; label: string;
summary: string;
apps: string[]; apps: string[];
/** Stored as a `goals` fact, so it stays a plain English statement. */ /** Stored as a `goals` fact, so it stays a plain English statement. */
fact: string; fact: string;
@ -63,7 +62,6 @@ export const FOCUS_OPTIONS: FocusOption[] = [
id: "day", id: "day",
letter: "A", letter: "A",
label: "Day-to-day work", label: "Day-to-day work",
summary: "Slack, calendar, and email",
apps: ["slack", "gmail", "googlecalendar"], apps: ["slack", "gmail", "googlecalendar"],
fact: "Wants help with day-to-day work first", fact: "Wants help with day-to-day work first",
}, },
@ -71,7 +69,6 @@ export const FOCUS_OPTIONS: FocusOption[] = [
id: "inbox", id: "inbox",
letter: "B", letter: "B",
label: "Inbox & email", label: "Inbox & email",
summary: "email and calendar",
apps: ["gmail", "googlecalendar", "slack"], apps: ["gmail", "googlecalendar", "slack"],
fact: "Wants help with inbox and email first", fact: "Wants help with inbox and email first",
}, },
@ -79,7 +76,6 @@ export const FOCUS_OPTIONS: FocusOption[] = [
id: "research", id: "research",
letter: "C", letter: "C",
label: "Research & writing", label: "Research & writing",
summary: "the web, notes, and docs",
apps: ["hackernews", "notion", "googledocs"], apps: ["hackernews", "notion", "googledocs"],
fact: "Wants help with research and writing first", fact: "Wants help with research and writing first",
}, },
@ -87,7 +83,6 @@ export const FOCUS_OPTIONS: FocusOption[] = [
id: "crew", id: "crew",
letter: "D", letter: "D",
label: "Specialist crew", label: "Specialist crew",
summary: "a coordinated team of focused Oos",
apps: ["slack", "gmail", "googlecalendar"], apps: ["slack", "gmail", "googlecalendar"],
fact: "Wants a coordinated crew of specialist bots", fact: "Wants a coordinated crew of specialist bots",
}, },

View File

@ -1857,6 +1857,25 @@ describe("mobile thread event reduction", () => {
).toBe("Slack · Alex: Hello from the group"); ).toBe("Slack · Alex: Hello from the group");
}); });
it("reads a choice card as its question and answer", () => {
const card = {
kind: "choice" as const,
step: "goal" as const,
question: "What’s one thing you want to get done in the next few weeks?",
textInput: { placeholder: "Your goal" },
options: [{ id: "skip", letter: "", label: "Skip" }],
};
expect(blockText(mobileMessage("choice-1", [card]))).toBe(card.question);
expect(
blockText(
mobileMessage("choice-2", [{ ...card, answerId: "_text", answerText: "Run a 10K" }]),
),
).toBe(`${card.question}\nRun a 10K`);
expect(blockText(mobileMessage("choice-3", [{ ...card, answerId: "skip" }]))).toBe(
`${card.question}\nSkip`,
);
});
it("deduplicates durable messages and replaces matching transient subagent state", () => { it("deduplicates durable messages and replaces matching transient subagent state", () => {
const initial = snapshot([ const initial = snapshot([
mobileMessage("message-1", [{ kind: "text", text: "old" }]), mobileMessage("message-1", [{ kind: "text", text: "old" }]),

View File

@ -874,6 +874,12 @@ export function blockText(message: MobileMessage) {
if (block.kind === "child_bot") { if (block.kind === "child_bot") {
return `${block.status === "archived" ? "Archived" : block.status === "deleted" ? "Deleted" : "Bot"} ${block.name ?? ""}`; return `${block.status === "archived" ? "Archived" : block.status === "deleted" ? "Deleted" : "Bot"} ${block.name ?? ""}`;
} }
// Choice cards are answered on the web; here they read as the question and its answer.
if (block.kind === "choice") {
const answer =
block.answerText ?? block.options.find((option) => option.id === block.answerId)?.label;
return answer ? `${block.question}\n${answer}` : block.question;
}
if (block.kind === "chart") return `[chart: ${block.name ?? "chart"}]`; if (block.kind === "chart") return `[chart: ${block.name ?? "chart"}]`;
if (block.kind === "image") return `[image: ${block.name ?? "attachment"}]`; if (block.kind === "image") return `[image: ${block.name ?? "attachment"}]`;
if (block.kind === "file") { if (block.kind === "file") {

View File

@ -88,8 +88,9 @@ async function visible(locator: Locator, timeout: number) {
} }
/** /**
* Walks first-run onboarding, which lands in the main conversation. Unless `mainConversation` * Walks the first-run wizard, which lands in the main conversation with the main bot's first
* is set, it then opens the main bot's own thread, where most specs start. * onboarding question pending (the composer works regardless). Unless `mainConversation` is
* set, it then opens the main bot's own thread, where most specs start.
*/ */
export async function completeOnboarding( export async function completeOnboarding(
page: Page, page: Page,
@ -101,13 +102,12 @@ export async function completeOnboarding(
if (page.url().includes("/onboarding")) { if (page.url().includes("/onboarding")) {
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
"data-onboarding-step", "data-onboarding-step",
/^(emoji|preferences|language|voice|integrations|scan|bot)$/, /^(emoji|language|voice|integrations|scan|bot)$/,
{ timeout: 30_000 }, { timeout: 30_000 },
); );
} }
const design = page.getByRole("heading", { name: /Design your Oo/i }); const design = page.getByRole("heading", { name: /Design your Oo/i });
const preferences = page.getByRole("heading", { name: /Tell us about yourself/i });
const language = page.getByRole("heading", { name: /Choose your language/i }); const language = page.getByRole("heading", { name: /Choose your language/i });
const voice = page.getByRole("heading", { name: /Pick a voice/i }); const voice = page.getByRole("heading", { name: /Pick a voice/i });
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true }); const integrations = page.getByRole("heading", { name: "Server integrations", exact: true });
@ -119,8 +119,6 @@ export async function completeOnboarding(
await expect(entry.first()).toBeVisible({ timeout: 20_000 }); await expect(entry.first()).toBeVisible({ timeout: 20_000 });
if (await design.isVisible()) { if (await design.isVisible()) {
await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(preferences).toBeVisible();
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(language).toBeVisible(); await expect(language).toBeVisible();
await page.getByRole("button", { name: /Skip/i }).click(); await page.getByRole("button", { name: /Skip/i }).click();
@ -133,8 +131,15 @@ export async function completeOnboarding(
await page.getByRole("button", { name: "Skip", exact: true }).click(); await page.getByRole("button", { name: "Skip", exact: true }).click();
} }
// Scan step auto-advances to the main bot, then onboarding lands in the main conversation. // The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 }); // then lands in the main conversation.
const skipScan = page
.locator('[data-onboarding-step="scan"]')
.getByRole("button", { name: "Skip", exact: true });
await expect(async () => {
if (await skipScan.isVisible()) await skipScan.click();
expect(page.url()).toMatch(/\/app\/g\/[^/]+$/);
}).toPass({ timeout: 30_000 });
await expect(chief).toBeVisible({ timeout: 15_000 }); await expect(chief).toBeVisible({ timeout: 15_000 });
await waitForChatComposer(page); await waitForChatComposer(page);

View File

@ -4,7 +4,8 @@ import { captureScreenshot, completeOnboarding, signup, waitForChatComposer } fr
/** Submit from the Send control so an in-flight prior send cannot swallow Enter. */ /** Submit from the Send control so an in-flight prior send cannot swallow Enter. */
async function sendComposerMessage(page: Page, composer: Locator, text: string) { async function sendComposerMessage(page: Page, composer: Locator, text: string) {
await composer.fill(text); await composer.fill(text);
const send = page.getByRole("button", { name: "Send", exact: true }); // An open onboarding card in the transcript has its own Send.
const send = page.getByTestId("composer-bar").getByRole("button", { name: "Send", exact: true });
await expect(send).toBeEnabled(); await expect(send).toBeEnabled();
await send.click(); await send.click();
await expect(composer).toHaveValue(""); await expect(composer).toHaveValue("");
@ -72,13 +73,20 @@ test("message hover shows beside-bubble actions; reply links to parent", async (
const transcript = page.getByTestId("transcript"); const transcript = page.getByTestId("transcript");
// Empty start: no Fresh-start greeting. Answer the focus card so a plain // Empty start: no Fresh-start greeting. Answer the focus card, then ask something long: a bot
// bot text bubble exists for hover layout checks. // row's rail sits at the row's end, so the measured reply must wrap and fill its row.
await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible({ await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible({
timeout: 20_000, timeout: 20_000,
}); });
await page.getByTestId("focus-choice-day").click(); await page.getByTestId("focus-choice-day").click();
const botText = page.getByText(/Got it\./); await expect(page.getByText(/Got it\./)).toBeVisible({ timeout: 20_000 });
const composer = page.getByRole("combobox", { name: /^Message/ });
await sendComposerMessage(
page,
composer,
`hover-bot-${stamp} ${"please look into this ".repeat(7)}`,
);
const botText = page.getByText(/i handled: hover-bot-/);
await expect(botText).toBeVisible({ timeout: 20_000 }); await expect(botText).toBeVisible({ timeout: 20_000 });
const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first(); const botRow = transcript.locator(`[data-message-id]`).filter({ has: botText }).first();
await expect(botRow).toBeVisible(); await expect(botRow).toBeVisible();
@ -121,7 +129,6 @@ test("message hover shows beside-bubble actions; reply links to parent", async (
const parentText = `hover-parent-${stamp}`; const parentText = `hover-parent-${stamp}`;
const replyText = `hover-reply-${stamp}`; const replyText = `hover-reply-${stamp}`;
const composer = page.getByRole("combobox", { name: /^Message/ });
await expect(composer).toBeVisible(); await expect(composer).toBeVisible();
await sendComposerMessage(page, composer, parentText); await sendComposerMessage(page, composer, parentText);

View File

@ -21,6 +21,9 @@ test("focus choice suggests apps and preserves a completed connection", async ({
await expect(page.getByTestId("focus-choice-day")).toContainText("Day-to-day work"); await expect(page.getByTestId("focus-choice-day")).toContainText("Day-to-day work");
await expect(page.getByTestId("focus-choice-research")).toContainText("Research & writing"); await expect(page.getByTestId("focus-choice-research")).toContainText("Research & writing");
await expect(page.getByTestId("focus-choice-crew")).toContainText("Specialist crew"); await expect(page.getByTestId("focus-choice-crew")).toContainText("Specialist crew");
// Onboarding questions offer a quiet way out instead of a dismiss button.
await expect(page.getByTestId("focus-choice-skip")).toBeVisible();
await expect(page.getByRole("button", { name: "Dismiss" })).toHaveCount(0);
await page.mouse.move(1, 1); await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "01-focus-choice"); await captureScreenshot(page, testInfo, "01-focus-choice");
await captureScreenshot(page, testInfo, "choice-card-onboarding"); await captureScreenshot(page, testInfo, "choice-card-onboarding");
@ -33,6 +36,9 @@ test("focus choice suggests apps and preserves a completed connection", async ({
).toBeVisible(); ).toBeVisible();
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Chief"); expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Chief");
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await expect(
page.getByText("Got it. Slack, Gmail, and Google Calendar are a good place to start."),
).toBeVisible();
await expect(page.getByText("Slack", { exact: true })).toBeVisible(); await expect(page.getByText("Slack", { exact: true })).toBeVisible();
await expect(page.getByText("Gmail", { exact: true })).toBeVisible(); await expect(page.getByText("Gmail", { exact: true })).toBeVisible();
const connectionCards = page.getByRole("group", { name: / connection$/ }); const connectionCards = page.getByRole("group", { name: / connection$/ });
@ -45,10 +51,10 @@ test("focus choice suggests apps and preserves a completed connection", async ({
); );
expect(cardBoxes[1].top - cardBoxes[0].bottom).toBeGreaterThanOrEqual(8); expect(cardBoxes[1].top - cardBoxes[0].bottom).toBeGreaterThanOrEqual(8);
expect(cardBoxes[2].top - cardBoxes[1].bottom).toBeGreaterThanOrEqual(8); expect(cardBoxes[2].top - cardBoxes[1].bottom).toBeGreaterThanOrEqual(8);
await page // The next onboarding question follows the suggestions.
.getByTestId("transcript") const goal = page.locator('[data-testid="choice-card"][data-step="goal"]');
.getByText("Hit those three and I’ll start pulling the picture.") await expect(goal).toContainText("What’s one thing you want to get done in the next few weeks?");
.scrollIntoViewIfNeeded(); await slackCard(page).scrollIntoViewIfNeeded();
await page.mouse.move(1, 1); await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "02-app-suggestions"); await captureScreenshot(page, testInfo, "02-app-suggestions");
const authorizeButton = slackCard(page).getByRole("button", { name: "Authorize" }); const authorizeButton = slackCard(page).getByRole("button", { name: "Authorize" });

View File

@ -17,12 +17,11 @@ test.beforeEach(async ({ page }) => {
}); });
/** /**
* E2E test for the new emoji-first onboarding flow: * E2E test for the emoji-first onboarding flow:
* 1. Sign up → lands on onboarding * 1. Sign up → lands on onboarding
* 2. Design Oo: pick shape + color → live preview * 2. Design Oo: pick shape + color → live preview
* 3. Tell us about yourself: name, timezone, hobbies, work style, communication * 3. Choose language, skip the voice
* 4. Choose language * 4. Skip integrations → bot created → land in chat, where the main bot asks the rest
* 5. Skip integrations → bot created → land in chat
*/ */
test("emoji-first onboarding: shape + color selection with live preview", async ({ test("emoji-first onboarding: shape + color selection with live preview", async ({
@ -50,50 +49,13 @@ test("emoji-first onboarding: shape + color selection with live preview", async
if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview"); if (testInfo) await captureScreenshot(page, testInfo, "02-shape-color-preview");
// Click Continue → goes to preferences step // Click Continue → goes to the language step
await page await page
.getByRole("button", { name: /Continue/i }) .getByRole("button", { name: /Continue/i })
.first() .first()
.click(); .click();
// Step 2: Tell us about yourself // Step 2: Language selection
await expect(page.getByRole("heading", { name: /Tell us about yourself/i })).toBeVisible();
if (testInfo) await captureScreenshot(page, testInfo, "03-user-preferences");
// Fill in name
await page.getByPlaceholder(/Your name/i).fill("Alice");
// Fill in timezone
await page.getByLabel(/Timezone/i).fill("Europe/Vienna");
// Select a hobby
const hobbyButtons = page.locator("button[aria-pressed]");
if (await hobbyButtons.count()) {
await hobbyButtons.first().click(); // toggle first hobby
}
// Select work style
const workStyleButtons = page.locator("button[aria-pressed]:visible");
if ((await workStyleButtons.count()) >= 2) {
await workStyleButtons.nth(0).click();
}
// Select communication style
const commButtons = page.locator("button[aria-pressed]:visible");
if ((await commButtons.count()) >= 2) {
await commButtons.nth(0).click();
}
if (testInfo) await captureScreenshot(page, testInfo, "04-preferences-filled");
// Click Continue → goes to language step
await page
.getByRole("button", { name: /Continue/i })
.first()
.click();
// Step 3: Language selection
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
if (testInfo) await captureScreenshot(page, testInfo, "05-language-step"); if (testInfo) await captureScreenshot(page, testInfo, "05-language-step");
@ -135,13 +97,6 @@ test("onboarding: skip language defaults to English", async ({ page }) => {
.first() .first()
.click(); .click();
// Preferences step — just continue
await expect(page.getByRole("heading", { name: /Tell us about yourself/i })).toBeVisible();
await page
.getByRole("button", { name: /Continue/i })
.first()
.click();
// Language step — click Skip, then skip voice. // Language step — click Skip, then skip voice.
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
await page.getByRole("button", { name: /Skip/i }).click(); await page.getByRole("button", { name: /Skip/i }).click();

View File

@ -0,0 +1,83 @@
/**
* E2E: after the wizard the main bot gets to know the user in the main conversation, one card
* after each answer, without ever blocking the composer.
*/
import { expect, type Page, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, rpc, signup } from "./helpers";
function card(page: Page, step: string) {
return page.locator(`[data-testid="choice-card"][data-step="${step}"]`);
}
test("the main bot asks focus, goal, check-ins and quiet hours, then says what is next", async ({
page,
}, testInfo) => {
await signup(page, `intake-${Date.now()}@rakazo.test`, "password12", "Intake Tester");
await completeOnboarding(page, undefined, { mainConversation: true });
await page.getByTestId("focus-choice-day").click();
const goal = card(page, "goal");
await expect(goal).toContainText("What’s one thing you want to get done in the next few weeks?");
// Typing instead of answering goes to the main bot, and the card stays answerable.
const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("Quick question first");
await composer.press("Enter");
await expect(page.getByTestId("transcript")).toContainText(/on it|handled/i, {
timeout: 60_000,
});
const goalInput = goal.getByRole("textbox", {
name: "What’s one thing you want to get done in the next few weeks?",
});
await goalInput.fill("Run a 10K");
await goal.scrollIntoViewIfNeeded();
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "onboarding-intake-goal");
await goalInput.press("Enter");
await expect(goal.getByTestId("choice-answer")).toHaveText("Run a 10K");
const rhythm = card(page, "rhythm");
await expect(rhythm).toContainText("When should I check in with you?");
await rhythm.scrollIntoViewIfNeeded();
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "onboarding-intake-rhythm");
await rhythm.getByTestId("rhythm-choice-morning").click();
await expect(rhythm.getByTestId("choice-answer")).toContainText("Every morning");
const quiet = card(page, "quiet_hours");
await expect(quiet).toContainText("When should I stay quiet?");
await quiet.getByTestId("quiet_hours-choice-22-07").click();
// Only a browser that can still turn push on is asked about notifications.
const closing = page.getByText(/^All set\. I’ll check in (today|tomorrow) at 8:00\sAM\.$/);
const notifications = card(page, "notifications");
await expect(notifications.or(closing).first()).toBeVisible();
if (await notifications.isVisible()) {
await notifications.getByTestId("notifications-choice-later").click();
}
await expect(closing).toBeVisible();
await expect(
page.locator('[data-testid="choice-card"]:not(:has([data-testid="choice-answer"]))'),
).toHaveCount(0);
await closing.scrollIntoViewIfNeeded();
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "onboarding-intake-done");
const settings = await rpc<{ quietHours: { start: string; end: string } | null }>(
page,
"notifications/settings/get",
null,
);
expect(settings.quietHours).toEqual({ start: "22:00", end: "07:00" });
const profile = await rpc<{ facts: Array<{ value: string; source: string }> }>(
page,
"profile/get",
null,
);
expect(profile.facts).toContainEqual(
expect.objectContaining({
value: "Wants help with day-to-day work first",
source: "onboarding",
}),
);
});

View File

@ -37,7 +37,7 @@ test("onboarding shows the look picker when a model is still required", async ({
await page.waitForURL(/\/onboarding/, { timeout: 20_000 }); await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
"data-onboarding-step", "data-onboarding-step",
/^(emoji|preferences|language|voice|integrations)$/, /^(emoji|language|voice|integrations)$/,
{ timeout: 30_000 }, { timeout: 30_000 },
); );
await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({
@ -56,7 +56,7 @@ for (const unavailable of ["empty", "failed"] as const) {
await page.waitForURL(/\/onboarding/, { timeout: 20_000 }); await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute( await expect(page.locator('[data-rakazo-surface="onboarding"]')).toHaveAttribute(
"data-onboarding-step", "data-onboarding-step",
/^(emoji|preferences|language|voice|integrations)$/, /^(emoji|language|voice|integrations)$/,
{ timeout: 30_000 }, { timeout: 30_000 },
); );
await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({ await expect(page.getByRole("heading", { name: /Design your Oo/i })).toBeVisible({

View File

@ -77,5 +77,5 @@ test("mobile onboarding: category pick and swipe look previews", async ({ page }
.getByRole("button", { name: /Continue/i }) .getByRole("button", { name: /Continue/i })
.first() .first()
.click(); .click();
await expect(page.getByRole("heading", { name: /Tell us about yourself/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
}); });

View File

@ -1,6 +1,7 @@
/** /**
* E2E: onboarding answers land in the user profile, and Settings → About you shows every * E2E: onboarding fills the user profile without asking (time zone, locale and language from
* fact with its origin, confirms a pending one, and forgets one or all of them. * the browser) and records the focus answer as a fact; Settings → About you shows every fact
* with its origin, confirms a pending one, and forgets one or all of them.
*/ */
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { captureScreenshot, openUserSettings, rpc, signup, waitForChatComposer } from "./helpers"; import { captureScreenshot, openUserSettings, rpc, signup, waitForChatComposer } from "./helpers";
@ -8,7 +9,7 @@ import { captureScreenshot, openUserSettings, rpc, signup, waitForChatComposer }
test.use({ timezoneId: "Europe/Vienna" }); test.use({ timezoneId: "Europe/Vienna" });
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Walk the full emoji-first flow, including the preferences step. // Walk the full emoji-first flow.
await page.route("**/rpc/integrationSetup/get", (route) => await page.route("**/rpc/integrationSetup/get", (route) =>
route.fulfill({ route.fulfill({
json: { json: {
@ -34,27 +35,23 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
}); });
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.getByRole("heading", { name: /Tell us about yourself/i })).toBeVisible();
await expect(page.getByLabel("Timezone")).toHaveValue("Europe/Vienna");
await page.getByPlaceholder("Your name").fill("Pat Example");
await page.getByRole("button", { name: "Music", exact: true }).click();
await page.getByRole("button", { name: "Deep-dive focused" }).click();
await page.getByRole("button", { name: "Direct & concise" }).click();
await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Choose your language/i })).toBeVisible();
await page.getByRole("button", { name: "English", exact: true }).click(); await page.getByRole("button", { name: "English", exact: true }).click();
await expect(page.getByRole("heading", { name: /Pick a voice/i })).toBeVisible(); await expect(page.getByRole("heading", { name: /Pick a voice/i })).toBeVisible();
await page.getByRole("button", { name: "Skip", exact: true }).click(); await page.getByRole("button", { name: "Skip", exact: true }).click();
await page.waitForURL(/\/app\/[^/]+/, { timeout: 30_000 }); await page.waitForURL(/\/app\/g\/[^/]+/, { timeout: 30_000 });
await waitForChatComposer(page); await waitForChatComposer(page);
// The main bot asks the rest in the main conversation; the focus answer becomes a fact.
await page.getByTestId("focus-choice-day").click();
await expect(page.locator('[data-testid="choice-card"][data-step="goal"]')).toBeVisible();
// The answers no longer overwrite a bot's instructions. // The answers no longer overwrite a bot's instructions.
const bots = await rpc<Array<{ instructions: string }>>(page, "bots/list", null); const bots = await rpc<Array<{ instructions: string }>>(page, "bots/list", null);
expect(bots.length).toBeGreaterThan(0); expect(bots.length).toBeGreaterThan(0);
for (const bot of bots) expect(bot.instructions).not.toContain("User Context"); for (const bot of bots) expect(bot.instructions).not.toContain("User Context");
expect((await rpc<{ name: string }>(page, "me", null)).name).toBe("Pat Example"); // The account name is used as is; onboarding no longer asks for it again.
expect((await rpc<{ name: string }>(page, "me", null)).name).toBe("Profile Tester");
// Sensitive facts from anywhere but the user wait for confirmation. // Sensitive facts from anywhere but the user wait for confirmation.
await rpc(page, "profile/update", { await rpc(page, "profile/update", {
@ -67,14 +64,13 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
await expect(profile.getByTestId("profile-field-timeZone")).toContainText("Europe/Vienna"); await expect(profile.getByTestId("profile-field-timeZone")).toContainText("Europe/Vienna");
await expect(profile.getByTestId("profile-field-language")).toContainText("English"); await expect(profile.getByTestId("profile-field-language")).toContainText("English");
await expect(profile.getByTestId("profile-field-locale")).toContainText("United States"); await expect(profile.getByTestId("profile-field-locale")).toContainText("United States");
await expect(profile.getByTestId("profile-field-communicationStyle")).toContainText(
"Direct and concise",
);
const music = profile.getByTestId("user-fact").filter({ hasText: "Interested in music" }); const focus = profile
await expect(music).toContainText("Onboarding"); .getByTestId("user-fact")
await expect(profile.getByTestId("user-facts-habits")).toContainText( .filter({ hasText: "Wants help with day-to-day work first" });
"Prefers focused deep dives", await expect(focus).toContainText("Onboarding");
await expect(profile.getByTestId("user-facts-goals")).toContainText(
"Wants help with day-to-day work first",
); );
const allergy = profile.getByTestId("user-fact").filter({ hasText: "Allergic to peanuts" }); const allergy = profile.getByTestId("user-fact").filter({ hasText: "Allergic to peanuts" });
@ -84,8 +80,8 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
await expect(allergy).toHaveAttribute("data-status", "active"); await expect(allergy).toHaveAttribute("data-status", "active");
await expect(allergy.getByRole("button", { name: "Confirm" })).toHaveCount(0); await expect(allergy.getByRole("button", { name: "Confirm" })).toHaveCount(0);
await music.getByRole("button", { name: "Delete" }).click(); await focus.getByRole("button", { name: "Delete" }).click();
await expect(music).toHaveCount(0); await expect(focus).toHaveCount(0);
await captureScreenshot(page, testInfo, "user-profile-settings"); await captureScreenshot(page, testInfo, "user-profile-settings");
const afterDelete = await rpc<{ facts: Array<{ value: string; status: string }> }>( const afterDelete = await rpc<{ facts: Array<{ value: string; status: string }> }>(
page, page,
@ -93,7 +89,9 @@ test("onboarding feeds the profile; facts can be confirmed and forgotten", async
null, null,
); );
expect(afterDelete.facts).toEqual( expect(afterDelete.facts).toEqual(
expect.not.arrayContaining([expect.objectContaining({ value: "Interested in music" })]), expect.not.arrayContaining([
expect.objectContaining({ value: "Wants help with day-to-day work first" }),
]),
); );
expect(afterDelete.facts).toContainEqual( expect(afterDelete.facts).toContainEqual(
expect.objectContaining({ value: "Allergic to peanuts", status: "active" }), expect.objectContaining({ value: "Allergic to peanuts", status: "active" }),