evOo-local/apps/web/e2e/onboarding-conversation.spec.ts
cynarAI 0e1036b2ff
First-run onboarding UX: naming, locale, voices, Heute route, mobile drawer (#171)
* Fix first-run onboarding UX: naming, locale, voices, Heute route, mobile drawer

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome import order and formatting

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix biome formatting on OoStage and E2E specs

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix E2E specs for onboarding name step and Heute reload route

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German onboarding heading matcher in E2E

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German signup E2E, Escape menu stacking, and Heute rhythm click

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Make onboarding E2E helpers locale-aware for German UI

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix completeOnboarding language skip for Skip · locale label

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Use language-step Skip · locale button in completeOnboarding

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix German E2E: Nachricht composer, voice loading wait, Heute rhythm

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Stabilize Heute reload URL and German voice E2E assertions

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix Heute bootstrap redirect and German onboarding voices

When Kokoro lists no locale-matched voices, onboarding now uses offline
presets instead of showing English-only options. Shell bootstrap skips
bot redirects while the user is on /app/heute.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Relax German voice E2E name matching for voice labels

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Harden Heute route and German voice onboarding E2E

Rebased onto main with Expo SDK 57 typecheck fixes. Voice options expose
stable test ids; Shell keeps /app/heute across bootstrap and redirects
legacy /app/heute bot URLs. Heute coach E2E asserts URL after reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Format Shell after Heute route helper

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 20:15:36 +02:00

120 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, ooName: "Robin" });
await expect(
page.getByText("Hey Robin. Fresh start on my side, so I’ll keep this short."),
).toHaveCount(0);
await expect(
page.getByTestId("transcript").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 must not rename the bot: onboarding keeps the chosen name.
await expect(
page.getByTestId("bot-settings-trigger").getByText("Robin", { exact: true }),
).toBeVisible();
expect((await rpc<{ name: string }>(page, "bots/ensureMain", {})).name).toBe("Robin");
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, ooName: "Robin" });
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();
});