evOo-local/apps/web/e2e/heute-brief.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

149 lines
6.3 KiB
TypeScript

import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import type { CoachRhythm, CoachToday, Goal, Habit, Routine } from "@rakazo/contracts";
import { captureScreenshot, completeOnboarding, openMainBotThread, rpc, signup } from "./helpers";
/** The nav button reads "Today" or "Today, N need you". */
function openHeute(page: Page) {
return page.getByRole("button", { name: /^Today(,|$)/ }).click();
}
function shiftDate(date: string, days: number) {
return new Date(Date.parse(`${date}T00:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10);
}
test("Heute shows native brief instead of Mission Control iframe", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `heute-${stamp}@rakazo.test`, "password12", "Heute");
await completeOnboarding(page);
await openHeute(page);
await expect(page).toHaveURL(/\/app\/heute$/);
await page.reload();
await expect(page.getByTestId("heute-brief")).toBeVisible();
await expect(page).toHaveURL(/\/app\/heute$/);
const brief = page.getByTestId("heute-brief");
await expect(brief).toBeVisible();
await expect(page.locator('iframe[src="http://127.0.0.1:3088/"]')).toHaveCount(0);
await expect(page.getByText("evOo Mission Control")).toHaveCount(0);
await expect(page.getByTestId("heute-mission-control-link")).toHaveCount(0);
await expect(page.getByTestId("heute-status")).toBeVisible();
// Nothing tracked yet: no coach sections.
await expect(page.getByTestId("heute-coach-brief")).toHaveCount(0);
await expect(page.getByTestId("heute-habits")).toHaveCount(0);
await captureScreenshot(page, testInfo, "heute-native-brief");
});
test("Heute shows the coach's brief, habits and goals, checks habits off and sets check-ins", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `heute-coach-${stamp}@rakazo.test`, "password12", "Heute Coach");
await completeOnboarding(page);
// The main bot writes today's brief through its tool.
const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("write today's brief: Two calm hours on the beta, then a walk.");
await page.keyboard.press("Enter");
await expect(page.getByText("today's brief is ready.").first()).toBeVisible({ timeout: 30_000 });
const { date } = await rpc<CoachToday>(page, "coach/today", {});
const goal = await rpc<Goal>(page, "coach/goals/create", {
title: "Ship the beta",
targetDate: shiftDate(date, 30),
});
const habit = await rpc<Habit>(page, "coach/habits/create", { title: "Walk 20 minutes" });
for (const day of [1, 2]) {
await rpc(page, "coach/checkIns/log", {
kind: "habit",
habitId: habit.id,
date: shiftDate(date, -day),
done: true,
});
}
await openHeute(page);
await expect(page.getByTestId("heute-coach-brief")).toHaveText(
"Two calm hours on the beta, then a walk.",
);
await expect(page.getByTestId(`heute-goal-${goal.id}`)).toContainText("Ship the beta");
const habitRow = page.getByTestId(`heute-habit-${habit.id}`);
await expect(habitRow).toContainText("Walk 20 minutes");
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("2 days");
const checkbox = habitRow.getByRole("checkbox");
await expect(checkbox).not.toBeChecked();
const logged = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/checkIns/log") && response.ok(),
);
await checkbox.click();
await logged;
await expect(checkbox).toBeChecked();
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days");
await captureScreenshot(page, testInfo, "heute-coach");
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/app\/heute$/);
await expect(page.getByTestId("heute-brief")).toBeVisible();
await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked();
// Check-in times live behind one compact control.
const rhythm = page.getByTestId("heute-rhythm");
await expect(page.getByTestId("heute-coach-brief")).toBeVisible();
await rhythm.scrollIntoViewIfNeeded();
await expect(rhythm).toBeVisible({ timeout: 30_000 });
const rhythmLoaded = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/rhythm/get") && response.ok(),
);
await rhythm.click();
await rhythmLoaded;
const panel = page.getByTestId("heute-rhythm-panel");
await expect(panel).toBeVisible();
const morning = panel.getByTestId("heute-rhythm-morning");
await expect(morning.getByRole("switch")).not.toBeChecked();
const enabled = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/rhythm/set") && response.ok(),
);
await morning.getByRole("switch").click();
await enabled;
const time = morning.locator('input[type="time"]');
await expect(time).toHaveValue("08:00");
const moved = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/rhythm/set") && response.ok(),
);
await time.fill("07:30");
await moved;
await expect(time).toHaveValue("07:30");
const weekly = panel.getByTestId("heute-rhythm-weekly");
const weeklyOn = page.waitForResponse(
(response) => response.url().includes("/rpc/coach/rhythm/set") && response.ok(),
);
await weekly.getByRole("switch").click();
await weeklyOn;
await expect(weekly.getByRole("combobox")).toHaveValue("0");
await captureScreenshot(page, testInfo, "heute-rhythm");
expect(await rpc<CoachRhythm>(page, "coach/rhythm/get", {})).toEqual({
morning: "07:30",
evening: null,
weekly: { day: 0, time: "18:00" },
});
const main = await rpc<{ id: string }>(page, "bots/ensureMain", {});
const routines = await rpc<Routine[]>(page, "routines/list", { botId: main.id });
expect(routines.map(({ name, coachKey }) => ({ name, coachKey }))).toEqual(
expect.arrayContaining([
{ name: "Morning brief", coachKey: "morning" },
{ name: "Weekly review", coachKey: "weekly" },
]),
);
expect(routines).toHaveLength(2);
// The main bot's routine list names the check-ins by their key, in the UI language.
await openMainBotThread(page);
await page.getByTitle("Agent computer").click();
await expect(page.getByText("Morning brief", { exact: true })).toBeVisible();
await expect(page.getByText("Weekly review", { exact: true })).toBeVisible();
});