The onboarding cards, the closing line and the coach routine names were English server text for every user. The server keeps the state and an English fallback; the web now words them through Lingui, so there is one translation source. - Choice cards: the web says the question, the option labels and the placeholder from the step and option ids, and keeps the server text for anything it does not know (quiet-hour ranges are already written in the user's locale). Another bot's focus card is recognized by its option ids. Mobile and agent history keep the server text. - The "Got it. … are a good place to start." line is gone: the app cards speak for themselves, and with no cards nothing is said. - The closing line becomes an intake_note block (note "done", nextCheckInAt, English text). The web words it with the first check-in in the user's clock; mobile blockText and blocksToAgentHistoryText read its text through their generic fallback. - Routines carry coachKey. The routine list and editor show a check-in under its label in the UI language until the user renames it; saving an untouched label keeps the stored name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
/**
|
||
* 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.
|
||
// The web words the closing note itself, from the time of the first check-in.
|
||
const closing = page
|
||
.getByTestId("transcript")
|
||
.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",
|
||
}),
|
||
);
|
||
});
|