evOo-local/apps/web/e2e/onboarding-conversation.spec.ts
Claude d3689b88fc
feat(onboarding): show the onboarding cards and check-in names in the user's language
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
2026-10-03 14:51:59 +00:00

119 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, type Page, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, rpc, signup } from "./helpers";
function slackCard(page: Page) {
return page.getByRole("group", { name: "Slack connection" });
}
test("focus choice suggests apps and preserves a completed connection", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `onboarding-${stamp}@rakazo.test`, "password12", "Robin");
// The main bot greets the user in the main conversation, where onboarding lands.
await completeOnboarding(page, undefined, { mainConversation: true });
await expect(
page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."),
).toHaveCount(0);
await expect(page.getByText("What do you want me on first?", { exact: true })).toBeVisible();
await expect(page.getByTestId("focus-choice-day")).toBeVisible();
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-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 captureScreenshot(page, testInfo, "01-focus-choice");
await captureScreenshot(page, testInfo, "choice-card-onboarding");
await page.getByTestId("focus-choice-day").click();
// The focus step suggests apps but must not rename the bot: the name the
// user chose during creation ("Chief") is preserved.
await expect(
page.getByTestId("bot-settings-trigger").getByText("Chief", { exact: true }),
).toBeVisible();
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Chief");
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
// The app cards speak for themselves; onboarding adds no text around them.
await expect(page.getByText(/^Got it/)).toHaveCount(0);
await expect(page.getByText("Slack", { exact: true })).toBeVisible();
await expect(page.getByText("Gmail", { exact: true })).toBeVisible();
const connectionCards = page.getByRole("group", { name: / connection$/ });
await expect(connectionCards).toHaveCount(3);
const cardBoxes = await connectionCards.evaluateAll((cards) =>
cards.map((card) => {
const { bottom, top } = card.getBoundingClientRect();
return { bottom, top };
}),
);
expect(cardBoxes[1].top - cardBoxes[0].bottom).toBeGreaterThanOrEqual(8);
expect(cardBoxes[2].top - cardBoxes[1].bottom).toBeGreaterThanOrEqual(8);
// The next onboarding question follows the suggestions.
const goal = page.locator('[data-testid="choice-card"][data-step="goal"]');
await expect(goal).toContainText("What’s one thing you want to get done in the next few weeks?");
await slackCard(page).scrollIntoViewIfNeeded();
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "02-app-suggestions");
const authorizeButton = slackCard(page).getByRole("button", { name: "Authorize" });
const restingBackground = await authorizeButton.evaluate(
(button) => getComputedStyle(button).backgroundColor,
);
const accentBackground = await authorizeButton.evaluate((button) => {
const probe = document.createElement("span");
probe.style.backgroundColor = "var(--accent)";
button.append(probe);
const backgroundColor = getComputedStyle(probe).backgroundColor;
probe.remove();
return backgroundColor;
});
expect(accentBackground).not.toBe(restingBackground);
await authorizeButton.hover();
await expect
.poll(() => authorizeButton.evaluate((button) => getComputedStyle(button).backgroundColor))
.toBe(accentBackground);
await captureScreenshot(page, testInfo, "02-app-suggestions-authorize-hover");
await authorizeButton.click();
await expect(slackCard(page).getByText("Connected", { exact: true })).toBeVisible();
await expect(slackCard(page).getByText("Connected", { exact: true })).toHaveCSS("opacity", "1");
await expect
.poll(async () => {
const connections = await rpc<Array<{ provider: string; status: string }>>(
page,
"connections/list",
{},
);
return connections.some(
(connection) => connection.provider === "SLACK" && connection.status === "connected",
);
})
.toBe(true);
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "03-slack-connected");
await page.reload();
await expect(slackCard(page).getByText("Connected", { exact: true })).toBeVisible();
await page.mouse.move(1, 1);
await captureScreenshot(page, testInfo, "04-connected-after-reload");
});
test("choice refresh failures leave options available for retry", async ({ page }) => {
await signup(page, `choice-refresh-${Date.now()}@rakazo.test`, "password12", "Choice Retry");
await completeOnboarding(page, undefined, { mainConversation: true });
const choice = page.getByTestId("focus-choice-day");
await expect(choice).toBeEnabled();
// Keep the existing choice rendered while its save succeeds and navigation refresh fails.
await page.route("**/rpc/onboarding/choose", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ json: { ok: true } }),
}),
);
await page.route("**/rpc/spaces/list", (route) => route.abort());
await Promise.all([page.waitForRequest("**/rpc/spaces/list"), choice.click()]);
await expect(choice).toBeEnabled();
});