* 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>
120 lines
5.6 KiB
TypeScript
120 lines
5.6 KiB
TypeScript
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();
|
||
});
|