While onboarding wrote the main conversation, its sidebar row, project rows and Heute showed the server preview: nothing for a pending card (a card has no text of its own, so the row fell back to "Ready") and the English fallback of the closing note, even for a German user. Groups now carry `previewBlock` next to `preview`: the latest message when it is an onboarding card or note, validated with the same block schemas the messages use (now named exports). The web words it with the helpers that render the card and the note, so no copy is duplicated: the question while a card waits, the answer once given, and the closing note in the user's language and clock. A dismissed card keeps the plain preview, as the conversation no longer shows it. The field is left out for every other message, and mobile keeps using the plain `preview`. The chosen-answer lookup moves from the card into onboarding-copy so the card and the preview share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
92 lines
3.9 KiB
TypeScript
92 lines
3.9 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 type { Page } from "@playwright/test";
|
||
import { expect, 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?");
|
||
// The sidebar previews the main conversation as the web words it: the pending question.
|
||
const mainEntry = page.locator("aside").first().locator("[data-main-conversation]");
|
||
await expect(mainEntry).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(mainEntry).toContainText(/All set\. I’ll check in (today|tomorrow) at 8:00\sAM\./);
|
||
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",
|
||
}),
|
||
);
|
||
});
|