Compare commits

...

10 Commits

Author SHA1 Message Date
Cursor Agent
22000ad2ad
style(web): format chart-links E2E
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 12:57:48 +00:00
Cursor Agent
51b456409d
test(web): run chart-links E2E serially to reuse warmed Plot module
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 12:35:34 +00:00
Cursor Agent
7f75dfc2bc
fix(web): format empty-project E2E; stabilize chart-links cold Plot load
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 12:09:00 +00:00
Cursor Agent
8c948050b9
fix(web): empty project E2E simulates pre-start bootstrap; relax module recovery wait
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 11:48:31 +00:00
Cursor Agent
2e4d7d3835
fix(web): stabilize Phase 2 E2E (settings i18n, empty project, chart plot load)
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 11:23:53 +00:00
Cursor Agent
04d28aa803
style(web): format desktop header control test
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 10:59:41 +00:00
Cursor Agent
5d35ac7598
test(web): align Shell header control assertions with touch targets
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 10:49:08 +00:00
Cursor Agent
d8b2d6428e
fix(api): use FocusOptionId union for fact lookup
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 10:23:46 +00:00
Cursor Agent
65df4df348
fix(api): narrow focus fact locale key
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 10:23:04 +00:00
Cursor Agent
90c24c1a56
fix(api): type-safe focusFactValue lookup
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 10:22:48 +00:00
6 changed files with 141 additions and 101 deletions

View File

@ -43,14 +43,16 @@ interface IntakeStep {
save: (effects: IntakeEffects, answer: IntakeAnswer) => Promise<void>; save: (effects: IntakeEffects, answer: IntakeAnswer) => Promise<void>;
} }
export type FocusOptionId = "day" | "inbox" | "research" | "crew";
export interface FocusOption { export interface FocusOption {
id: string; id: FocusOptionId;
letter: string; letter: string;
label: string; label: string;
apps: string[]; apps: string[];
} }
const FOCUS_FACTS: Record<string, Record<FocusOption["id"], string>> = { const FOCUS_FACTS = {
en: { en: {
day: "Wants help with day-to-day work first", day: "Wants help with day-to-day work first",
inbox: "Wants help with inbox and email first", inbox: "Wants help with inbox and email first",
@ -63,14 +65,17 @@ const FOCUS_FACTS: Record<string, Record<FocusOption["id"], string>> = {
research: "Möchte zuerst bei Recherche und Schreiben unterstützt werden", research: "Möchte zuerst bei Recherche und Schreiben unterstützt werden",
crew: "Möchte eine koordinierte Crew aus Spezialisten-Bots", crew: "Möchte eine koordinierte Crew aus Spezialisten-Bots",
}, },
}; } as const satisfies Record<string, Record<FocusOptionId, string>>;
/** Profile fact text for a focus answer, in the user's locale (defaults to English). */ /** Profile fact text for a focus answer, in the user's locale (defaults to English). */
function isFocusOptionId(id: string): id is FocusOptionId {
return FOCUS_OPTIONS.some((option) => option.id === id);
}
export function focusFactValue(focusId: string, locale: string): string { export function focusFactValue(focusId: string, locale: string): string {
const id = focusId as FocusOption["id"]; if (!isFocusOptionId(focusId)) return focusId;
const key = locale.toLowerCase().startsWith("de") ? "de" : "en"; const key: keyof typeof FOCUS_FACTS = locale.toLowerCase().startsWith("de") ? "de" : "en";
const table = FOCUS_FACTS[key]; return FOCUS_FACTS[key][focusId];
return table[id] ?? FOCUS_FACTS.en[id] ?? focusId;
} }
export const FOCUS_OPTIONS: FocusOption[] = [ export const FOCUS_OPTIONS: FocusOption[] = [

View File

@ -3,7 +3,9 @@ import { captureScreenshot } from "./helpers";
const fixture = "/e2e/fixtures/chart-links.html"; const fixture = "/e2e/fixtures/chart-links.html";
test("chart links cannot execute script when clicked or expanded", async ({ page }, testInfo) => { test.describe
.serial("chart links", () => {
test("cannot execute script when clicked or expanded", async ({ page }, testInfo) => {
test.setTimeout(120_000); test.setTimeout(120_000);
// ChartCanvas lazy-loads Observable Plot; pay the import cost once before the matrix below. // ChartCanvas lazy-loads Observable Plot; pay the import cost once before the matrix below.
await page.goto(`${fixture}?${new URLSearchParams({ href: "#details", inspect: "0" })}`); await page.goto(`${fixture}?${new URLSearchParams({ href: "#details", inspect: "0" })}`);
@ -49,9 +51,9 @@ test("chart links cannot execute script when clicked or expanded", async ({ page
} }
} }
await captureScreenshot(page, testInfo, "chart-with-inert-link"); await captureScreenshot(page, testInfo, "chart-with-inert-link");
}); });
test("chart links retain supported URLs and browser encoding semantics", async ({ page }) => { test("retain supported URLs and browser encoding semantics", async ({ page }) => {
test.setTimeout(120_000); test.setTimeout(120_000);
await page.route("https://example.test/**", (route) => await page.route("https://example.test/**", (route) =>
route.fulfill({ body: "Chart destination" }), route.fulfill({ body: "Chart destination" }),
@ -59,9 +61,8 @@ test("chart links retain supported URLs and browser encoding semantics", async (
await page.route("**/e2e/fixtures/java*", (route) => await page.route("**/e2e/fixtures/java*", (route) =>
route.fulfill({ body: "Chart destination" }), route.fulfill({ body: "Chart destination" }),
); );
// Each test gets a fresh browser context, so this one pays the cold Plot import too.
await page.goto(`${fixture}?${new URLSearchParams({ href: "#details" })}`); await page.goto(`${fixture}?${new URLSearchParams({ href: "#details" })}`);
await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 90_000 }); await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 30_000 });
for (const href of [ for (const href of [
"https://example.test/chart?a=1&b=2", "https://example.test/chart?a=1&b=2",
"mailto:chart@example.test", "mailto:chart@example.test",
@ -88,4 +89,5 @@ test("chart links retain supported URLs and browser encoding semantics", async (
await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed"); await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed");
} }
} }
}); });
});

View File

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { completeOnboarding, rpc, signup } from "./helpers"; import { completeOnboarding, routePatchRpc, rpc, signup } from "./helpers";
test("empty project space offers Start before the main conversation exists", async ({ page }) => { test("empty project space offers Start before the main conversation exists", async ({ page }) => {
const stamp = Date.now(); const stamp = Date.now();
@ -7,15 +7,41 @@ test("empty project space offers Start before the main conversation exists", asy
await completeOnboarding(page); await completeOnboarding(page);
const side = await rpc<{ id: string }>(page, "spaces/create", { name: "Empty project" }); const side = await rpc<{ id: string }>(page, "spaces/create", { name: "Empty project" });
// spaces/create provisions a main conversation server-side; strip it from bootstrap so the
// shell shows the pre-start empty project UI.
await routePatchRpc(page, "bootstrap", (json) => {
const groups = (json.groups as Array<{ spaceId: string }>).filter(
(group) => group.spaceId !== side.id,
);
const bots = (json.bots as Array<{ spaceId: string }>).filter((bot) => bot.spaceId !== side.id);
const spaces = (
json.spaces as Array<{
id: string;
bots: unknown[];
groups: unknown[];
hasContent: boolean;
}>
).map((space) =>
space.id === side.id ? { ...space, bots: [], groups: [], hasContent: false } : space,
);
return { ...json, groups, bots, spaces };
});
await page.evaluate((spaceId) => { await page.evaluate((spaceId) => {
window.localStorage.setItem("rakazo:space-id", spaceId); window.localStorage.setItem("rakazo:space-id", spaceId);
}, side.id); }, side.id);
await page.reload({ waitUntil: "domcontentloaded" }); await page.goto("/app", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", { await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
timeout: 60_000, timeout: 60_000,
}); });
const sidebarStart = page.getByTestId(`project-start-${side.id}`); const mainStart = page.getByTestId("project-start-main");
await expect(mainStart).toBeVisible({ timeout: 30_000 });
await expect(mainStart).toContainText(/Start|Starten/i);
const projekte = page.locator(".sidebar-projekte");
const sidebarStart = projekte.getByTestId(`project-start-${side.id}`);
await expect(sidebarStart).toBeVisible({ timeout: 30_000 }); await expect(sidebarStart).toBeVisible({ timeout: 30_000 });
await expect(sidebarStart).toContainText(/Start|Starten/i); await expect(sidebarStart).toContainText(/Start|Starten/i);
}); });

View File

@ -291,7 +291,7 @@ export async function openUserSettings(
await expect(menuTrigger).toBeVisible({ timeout: 30_000 }); await expect(menuTrigger).toBeVisible({ timeout: 30_000 });
await menuTrigger.scrollIntoViewIfNeeded(); await menuTrigger.scrollIntoViewIfNeeded();
await menuTrigger.click(); await menuTrigger.click();
await page.getByRole("button", { name: "Settings", exact: true }).click(); await page.getByRole("button", { name: /^Settings$|^Einstellungen$/i }).click();
const settings = page.getByTestId("user-settings"); const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible(); await expect(settings).toBeVisible();
if (section && section !== "general") { if (section && section !== "general") {

View File

@ -20,7 +20,9 @@ test("the app reloads itself once when a module request aborts while booting", a
await page.goto("/sign-in", { waitUntil: "commit" }); await page.goto("/sign-in", { waitUntil: "commit" });
await expect(page.getByRole("heading", { name: "Sign in to evOo" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Sign in to evOo" })).toBeVisible({
timeout: 60_000,
});
expect(counter.aborted).toBe(1); expect(counter.aborted).toBe(1);
expect(await page.evaluate(() => sessionStorage.getItem("rakazo:boot-reload"))).toBeNull(); expect(await page.evaluate(() => sessionStorage.getItem("rakazo:boot-reload"))).toBeNull();
}); });
@ -30,7 +32,9 @@ test("a lazy route recovers when its module request aborts", async ({ page }) =>
await page.goto("/sign-in"); await page.goto("/sign-in");
await expect(page.getByRole("heading", { name: "Sign in to evOo" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Sign in to evOo" })).toBeVisible({
timeout: 60_000,
});
expect(counter.aborted).toBe(1); expect(counter.aborted).toBe(1);
expect( expect(
await page.evaluate(() => sessionStorage.getItem("rakazo:module-reload:pages/Auth")), await page.evaluate(() => sessionStorage.getItem("rakazo:module-reload:pages/Auth")),

View File

@ -67,12 +67,15 @@ describe("window chrome", () => {
expect(shell).toContain( expect(shell).toContain(
'className="app-drag flex items-center justify-between border-b border-sidebar-border', 'className="app-drag flex items-center justify-between border-b border-sidebar-border',
); );
expect(shell).toContain('className="app-no-drag grid size-control-lg'); expect(shell).toContain(
'className="app-no-drag rk-touch-target grid size-control-lg min-h-control-lg min-w-control-lg',
);
expect(shell).toContain('className="app-no-drag flex min-w-0 items-center gap-3"'); expect(shell).toContain('className="app-no-drag flex min-w-0 items-center gap-3"');
// Ensure header sidebar toggle controls stay clickable (not part of drag region) // Ensure header sidebar toggle controls stay clickable (not part of drag region)
expect( expect(
toggles.match(/app-no-drag .* size-control-lg .* place-items-center/g)?.length ?? 0, toggles.match(/app-no-drag rk-touch-target .* size-control-lg .* place-items-center/g)
).toBeGreaterThanOrEqual(2); ?.length ?? 0,
).toBeGreaterThanOrEqual(1);
}); });
it("moves window chrome into the conversation header when the bots sidebar is collapsed", () => { it("moves window chrome into the conversation header when the bots sidebar is collapsed", () => {