feat(web): Phase 2 production readiness — locale, mobile, E2E matrix
Localize onboarding focus facts by profile locale (DE assertion). Add mobile overflow/touch audits, deep-link matrix, extended console checks, and E2E for empty project, session unavailable, and teaching composer. Safe-area and Heute header layout on 390px viewports. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
97787a0272
commit
a5ef5dc5ce
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
closingNote,
|
closingNote,
|
||||||
focusCard,
|
focusCard,
|
||||||
|
focusFactValue,
|
||||||
intakeContext,
|
intakeContext,
|
||||||
intakeStep,
|
intakeStep,
|
||||||
nextIntakeStep,
|
nextIntakeStep,
|
||||||
@ -14,6 +15,13 @@ const plain = (text: string) => text.replace(/\s/g, " ");
|
|||||||
const off = { morning: null, evening: null, weekly: null };
|
const off = { morning: null, evening: null, weekly: null };
|
||||||
|
|
||||||
describe("onboarding steps", () => {
|
describe("onboarding steps", () => {
|
||||||
|
it("writes focus facts in the user's locale", () => {
|
||||||
|
expect(focusFactValue("day", "en-US")).toBe("Wants help with day-to-day work first");
|
||||||
|
expect(focusFactValue("day", "de-DE")).toBe(
|
||||||
|
"Möchte zuerst bei der täglichen Arbeit unterstützt werden",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("asks in order and offers notifications only to a device that can still turn push on", () => {
|
it("asks in order and offers notifications only to a device that can still turn push on", () => {
|
||||||
expect(nextIntakeStep("focus", { canEnablePush: false })?.id).toBe("goal");
|
expect(nextIntakeStep("focus", { canEnablePush: false })?.id).toBe("goal");
|
||||||
expect(nextIntakeStep("goal", { canEnablePush: false })?.id).toBe("rhythm");
|
expect(nextIntakeStep("goal", { canEnablePush: false })?.id).toBe("rhythm");
|
||||||
|
|||||||
@ -48,8 +48,28 @@ export interface FocusOption {
|
|||||||
letter: string;
|
letter: string;
|
||||||
label: string;
|
label: string;
|
||||||
apps: string[];
|
apps: string[];
|
||||||
/** Stored as a `goals` fact, so it stays a plain English statement. */
|
}
|
||||||
fact: string;
|
|
||||||
|
const FOCUS_FACTS: Record<string, Record<FocusOption["id"], string>> = {
|
||||||
|
en: {
|
||||||
|
day: "Wants help with day-to-day work first",
|
||||||
|
inbox: "Wants help with inbox and email first",
|
||||||
|
research: "Wants help with research and writing first",
|
||||||
|
crew: "Wants a coordinated crew of specialist bots",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
day: "Möchte zuerst bei der täglichen Arbeit unterstützt werden",
|
||||||
|
inbox: "Möchte zuerst bei Posteingang und E-Mail unterstützt werden",
|
||||||
|
research: "Möchte zuerst bei Recherche und Schreiben unterstützt werden",
|
||||||
|
crew: "Möchte eine koordinierte Crew aus Spezialisten-Bots",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Profile fact text for a focus answer, in the user's locale (defaults to English). */
|
||||||
|
export function focusFactValue(focusId: string, locale: string): string {
|
||||||
|
const id = focusId as FocusOption["id"];
|
||||||
|
const key = locale.toLowerCase().startsWith("de") ? "de" : "en";
|
||||||
|
return FOCUS_FACTS[key][id] ?? FOCUS_FACTS.en[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FOCUS_OPTIONS: FocusOption[] = [
|
export const FOCUS_OPTIONS: FocusOption[] = [
|
||||||
@ -58,28 +78,24 @@ export const FOCUS_OPTIONS: FocusOption[] = [
|
|||||||
letter: "A",
|
letter: "A",
|
||||||
label: "Day-to-day work",
|
label: "Day-to-day work",
|
||||||
apps: ["slack", "gmail", "googlecalendar"],
|
apps: ["slack", "gmail", "googlecalendar"],
|
||||||
fact: "Wants help with day-to-day work first",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inbox",
|
id: "inbox",
|
||||||
letter: "B",
|
letter: "B",
|
||||||
label: "Inbox & email",
|
label: "Inbox & email",
|
||||||
apps: ["gmail", "googlecalendar", "slack"],
|
apps: ["gmail", "googlecalendar", "slack"],
|
||||||
fact: "Wants help with inbox and email first",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "research",
|
id: "research",
|
||||||
letter: "C",
|
letter: "C",
|
||||||
label: "Research & writing",
|
label: "Research & writing",
|
||||||
apps: ["hackernews", "notion", "googledocs"],
|
apps: ["hackernews", "notion", "googledocs"],
|
||||||
fact: "Wants help with research and writing first",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "crew",
|
id: "crew",
|
||||||
letter: "D",
|
letter: "D",
|
||||||
label: "Specialist crew",
|
label: "Specialist crew",
|
||||||
apps: ["slack", "gmail", "googlecalendar"],
|
apps: ["slack", "gmail", "googlecalendar"],
|
||||||
fact: "Wants a coordinated crew of specialist bots",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -117,9 +133,16 @@ export const INTAKE_STEPS: IntakeStep[] = [
|
|||||||
save: async ({ prisma, tx, userId }, answer) => {
|
save: async ({ prisma, tx, userId }, answer) => {
|
||||||
const option = FOCUS_OPTIONS.find((entry) => entry.id === answer.option?.id);
|
const option = FOCUS_OPTIONS.find((entry) => entry.id === answer.option?.id);
|
||||||
if (!option) return;
|
if (!option) return;
|
||||||
|
const profile = await tx.userProfile.findUnique({
|
||||||
|
where: { userId },
|
||||||
|
select: { locale: true, language: true },
|
||||||
|
});
|
||||||
|
const locale = intakeContext(profile, new Date()).locale;
|
||||||
await tx.userOnboarding.update({ where: { userId }, data: { focus: option.id } });
|
await tx.userOnboarding.update({ where: { userId }, data: { focus: option.id } });
|
||||||
await updateUserProfile(prisma, userId, {
|
await updateUserProfile(prisma, userId, {
|
||||||
addFacts: [{ category: "goals", value: option.fact, source: "onboarding" }],
|
addFacts: [
|
||||||
|
{ category: "goals", value: focusFactValue(option.id, locale), source: "onboarding" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
16
apps/web/e2e/empty-project-state.spec.ts
Normal file
16
apps/web/e2e/empty-project-state.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { completeOnboarding, rpc, signup } from "./helpers";
|
||||||
|
|
||||||
|
test("empty project space shows Start before the main conversation exists", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `empty-project-${stamp}@rakazo.test`, "password12", "Empty Project");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
const side = await rpc<{ id: string }>(page, "spaces/create", { name: "Empty project" });
|
||||||
|
await page.evaluate((spaceId) => {
|
||||||
|
window.localStorage.setItem("rakazo:space-id", spaceId);
|
||||||
|
}, side.id);
|
||||||
|
await page.goto("/app", { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page.getByTestId("project-start-main")).toBeVisible({ timeout: 30_000 });
|
||||||
|
await expect(page.getByRole("button", { name: /^Start$|^Starten$/i })).toBeVisible();
|
||||||
|
});
|
||||||
@ -320,3 +320,53 @@ export async function createNamedBot(
|
|||||||
await waitForChatComposer(page);
|
await waitForChatComposer(page);
|
||||||
return bot.id;
|
return bot.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ALLOWED_CONSOLE_ERRORS = [
|
||||||
|
/Failed to load resource.*404/i,
|
||||||
|
/favicon/i,
|
||||||
|
/ERR_ABORTED/i,
|
||||||
|
/Content Security Policy/i,
|
||||||
|
/net::ERR_/i,
|
||||||
|
/React Router Future Flag/i,
|
||||||
|
/DevTools failed to load SourceMap/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Collect unexpected console errors and page errors for the rest of the test. */
|
||||||
|
export function attachConsoleGuard(page: Page) {
|
||||||
|
const errors: string[] = [];
|
||||||
|
page.on("console", (msg) => {
|
||||||
|
if (msg.type() !== "error") return;
|
||||||
|
const text = msg.text();
|
||||||
|
if (ALLOWED_CONSOLE_ERRORS.some((pattern) => pattern.test(text))) return;
|
||||||
|
errors.push(text);
|
||||||
|
});
|
||||||
|
page.on("pageerror", (err) => errors.push(err.message));
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the document does not scroll horizontally (±1px tolerance). */
|
||||||
|
export async function assertNoHorizontalOverflow(page: Page) {
|
||||||
|
const ok = await page.evaluate(() => {
|
||||||
|
const doc = document.documentElement;
|
||||||
|
return doc.scrollWidth <= doc.clientWidth + 1;
|
||||||
|
});
|
||||||
|
expect(ok, `horizontal overflow on ${page.url()}`).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOBILE_TOUCH_MIN = 44;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary chrome controls must meet 44×44px on touch viewports, either by box size or `rk-touch-target`.
|
||||||
|
*/
|
||||||
|
export async function assertPrimaryTouchTargets(page: Page, selectors: string[]) {
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const locator = page.locator(selector).first();
|
||||||
|
await expect(locator, selector).toBeVisible();
|
||||||
|
const ok = await locator.evaluate((node, min) => {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
if (rect.width >= min && rect.height >= min) return true;
|
||||||
|
return node.classList.contains("rk-touch-target");
|
||||||
|
}, MOBILE_TOUCH_MIN);
|
||||||
|
expect(ok, `${selector} touch target`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,39 +1,68 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { completeOnboarding, openUserSettings, signup, waitForChatComposer } from "./helpers";
|
import {
|
||||||
|
attachConsoleGuard,
|
||||||
|
completeOnboarding,
|
||||||
|
openUserSettings,
|
||||||
|
rpc,
|
||||||
|
signup,
|
||||||
|
waitForChatComposer,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
const ALLOWED_CONSOLE = [
|
const SIGNED_IN_ROUTES = (groupId: string, botId: string) =>
|
||||||
/Failed to load resource.*404/i,
|
["/app/heute", `/app/g/${groupId}`, `/app/${botId}`, "/onboarding"] as const;
|
||||||
/favicon/i,
|
|
||||||
/ERR_ABORTED/i,
|
|
||||||
/Content Security Policy/i,
|
|
||||||
/net::ERR_/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
function attachConsoleGuard(page: import("@playwright/test").Page) {
|
const SETTINGS_SECTIONS = [
|
||||||
const errors: string[] = [];
|
"general",
|
||||||
page.on("console", (msg) => {
|
"integrations",
|
||||||
if (msg.type() !== "error") return;
|
"models",
|
||||||
const text = msg.text();
|
"memory",
|
||||||
if (ALLOWED_CONSOLE.some((pattern) => pattern.test(text))) return;
|
"voice",
|
||||||
errors.push(text);
|
"notifications",
|
||||||
});
|
"usage",
|
||||||
page.on("pageerror", (err) => errors.push(err.message));
|
"computer",
|
||||||
return errors;
|
"updates",
|
||||||
|
"privacy",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function exerciseSignedInRoutes(page: import("@playwright/test").Page, errors: string[]) {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `console-j-${stamp}@rakazo.test`, "password12", "Console QA");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
|
||||||
|
const groupId = page.url().match(/\/app\/g\/([^/]+)/)?.[1];
|
||||||
|
const bots = await rpc<Array<{ id: string }>>(page, "bots/list", {});
|
||||||
|
const botId = bots[0]?.id;
|
||||||
|
expect(groupId).toBeTruthy();
|
||||||
|
expect(botId).toBeTruthy();
|
||||||
|
|
||||||
|
for (const route of SIGNED_IN_ROUTES(groupId!, botId!)) {
|
||||||
|
await page.goto(route, { waitUntil: "domcontentloaded" });
|
||||||
|
if (route !== "/onboarding") {
|
||||||
|
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(errors, `after ${route}:\n${errors.join("\n")}`).toEqual([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await openUserSettings(page);
|
||||||
|
for (const section of SETTINGS_SECTIONS) {
|
||||||
|
const nav = settings.getByTestId(`settings-nav-${section}`);
|
||||||
|
if (section !== "general") {
|
||||||
|
if (!(await nav.isVisible().catch(() => false))) continue;
|
||||||
|
await nav.click();
|
||||||
|
}
|
||||||
|
expect(errors, `after settings ${section}:\n${errors.join("\n")}`).toEqual([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe("desktop journeys", () => {
|
test.describe("desktop journeys", () => {
|
||||||
test.use({ viewport: { width: 1440, height: 900 } });
|
test.use({ viewport: { width: 1440, height: 900 } });
|
||||||
|
|
||||||
test("sign-up through settings has no unexpected console errors", async ({ page }) => {
|
test("signed-in route matrix has no unexpected console errors", async ({ page }) => {
|
||||||
const errors = attachConsoleGuard(page);
|
const errors = attachConsoleGuard(page);
|
||||||
const stamp = Date.now();
|
await exerciseSignedInRoutes(page, errors);
|
||||||
await signup(page, `console-j-${stamp}@rakazo.test`, "password12", "Console QA");
|
|
||||||
await completeOnboarding(page);
|
|
||||||
await waitForChatComposer(page);
|
|
||||||
await page.goto("/app/heute");
|
|
||||||
await expect(page).toHaveURL(/\/app\/heute/);
|
|
||||||
await openUserSettings(page);
|
|
||||||
expect(errors, errors.join("\n")).toEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,12 +73,8 @@ test.describe("mobile journeys", () => {
|
|||||||
hasTouch: true,
|
hasTouch: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
test("onboarding and chat shell stay console-clean", async ({ page }) => {
|
test("signed-in route matrix has no unexpected console errors", async ({ page }) => {
|
||||||
const errors = attachConsoleGuard(page);
|
const errors = attachConsoleGuard(page);
|
||||||
const stamp = Date.now();
|
await exerciseSignedInRoutes(page, errors);
|
||||||
await signup(page, `console-m-${stamp}@rakazo.test`, "password12", "Mobile Console");
|
|
||||||
await completeOnboarding(page);
|
|
||||||
await waitForChatComposer(page);
|
|
||||||
expect(errors, errors.join("\n")).toEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
54
apps/web/e2e/mobile-layout-audit.spec.ts
Normal file
54
apps/web/e2e/mobile-layout-audit.spec.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
assertNoHorizontalOverflow,
|
||||||
|
assertPrimaryTouchTargets,
|
||||||
|
completeOnboarding,
|
||||||
|
openUserSettings,
|
||||||
|
rpc,
|
||||||
|
signup,
|
||||||
|
waitForChatComposer,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
||||||
|
|
||||||
|
test("main routes avoid horizontal overflow on a phone viewport", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `mobile-layout-${stamp}@rakazo.test`, "password12", "Mobile Layout");
|
||||||
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
|
||||||
|
const groupMatch = page.url().match(/\/app\/g\/([^/]+)/);
|
||||||
|
const groupId = groupMatch?.[1];
|
||||||
|
const bots = await rpc<Array<{ id: string }>>(page, "bots/list", {});
|
||||||
|
const botId = bots[0]?.id;
|
||||||
|
expect(groupId).toBeTruthy();
|
||||||
|
expect(botId).toBeTruthy();
|
||||||
|
|
||||||
|
const routes = [`/app/heute`, `/app/g/${groupId}`, `/app/${botId}`];
|
||||||
|
for (const route of routes) {
|
||||||
|
await page.goto(route, { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
await assertNoHorizontalOverflow(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await openUserSettings(page);
|
||||||
|
for (const section of ["models", "memory", "voice", "notifications", "privacy"] as const) {
|
||||||
|
await settings.getByTestId(`settings-nav-${section}`).click();
|
||||||
|
await assertNoHorizontalOverflow(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("primary shell controls meet touch target guidance", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `mobile-touch-${stamp}@rakazo.test`, "password12", "Mobile Touch");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
|
||||||
|
await assertPrimaryTouchTargets(page, [
|
||||||
|
"[data-testid='user-menu-trigger']",
|
||||||
|
"[data-testid='create-menu-trigger']",
|
||||||
|
"[data-testid='open-navigation']",
|
||||||
|
]);
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { completeOnboarding, signup } from "./helpers";
|
import { completeOnboarding, openUserSettings, rpc, signup } from "./helpers";
|
||||||
|
|
||||||
test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ test("German UI localizes goal placeholder and question in intake", async ({ pag
|
|||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
await signup(page, `intake-de-${stamp}@rakazo.test`, "password12", "Deutsch Intake");
|
await signup(page, `intake-de-${stamp}@rakazo.test`, "password12", "Deutsch Intake");
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
await rpc(page, "profile/update", { locale: "de-DE", language: "de" });
|
||||||
await page.getByTestId("focus-choice-day").click();
|
await page.getByTestId("focus-choice-day").click();
|
||||||
|
|
||||||
const goal = page.locator('[data-testid="choice-card"][data-step="goal"]');
|
const goal = page.locator('[data-testid="choice-card"][data-step="goal"]');
|
||||||
@ -20,4 +21,10 @@ test("German UI localizes goal placeholder and question in intake", async ({ pag
|
|||||||
name: "Was möchtest du in den nächsten Wochen erledigt bekommen?",
|
name: "Was möchtest du in den nächsten Wochen erledigt bekommen?",
|
||||||
}),
|
}),
|
||||||
).toHaveAttribute("placeholder", "Dein Ziel");
|
).toHaveAttribute("placeholder", "Dein Ziel");
|
||||||
|
|
||||||
|
const settings = await openUserSettings(page);
|
||||||
|
const profile = settings.getByTestId("user-profile-settings");
|
||||||
|
await expect(profile.getByTestId("user-facts-goals")).toContainText(
|
||||||
|
"Möchte zuerst bei der täglichen Arbeit unterstützt werden",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
66
apps/web/e2e/route-deep-link-matrix.spec.ts
Normal file
66
apps/web/e2e/route-deep-link-matrix.spec.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
attachConsoleGuard,
|
||||||
|
completeOnboarding,
|
||||||
|
rpc,
|
||||||
|
signup,
|
||||||
|
waitForChatComposer,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
const mobile = { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true };
|
||||||
|
|
||||||
|
test.describe("signed out deep links", () => {
|
||||||
|
test.use(mobile);
|
||||||
|
|
||||||
|
for (const { path, expectUrl, expectText } of [
|
||||||
|
{ path: "/", expectUrl: /\/($|\?)/, expectText: /evOo|Willkommen|Welcome/i },
|
||||||
|
{ path: "/sign-in", expectUrl: /\/sign-in/, expectText: /Sign in to evOo|Bei evOo anmelden/i },
|
||||||
|
{ path: "/sign-up", expectUrl: /\/sign-up/, expectText: /Create account|Konto erstellen/i },
|
||||||
|
{ path: "/forgot-password", expectUrl: /\/forgot-password/, expectText: /password|Passwort/i },
|
||||||
|
{ path: "/onboarding", expectUrl: /\/sign-in/, expectText: /Sign in|Anmelden/i },
|
||||||
|
{ path: "/app/heute", expectUrl: /\/sign-in/, expectText: /Sign in|Anmelden/i },
|
||||||
|
{ path: "/app/g/example-group", expectUrl: /\/sign-in/, expectText: /Sign in|Anmelden/i },
|
||||||
|
{ path: "/app/example-bot", expectUrl: /\/sign-in/, expectText: /Sign in|Anmelden/i },
|
||||||
|
]) {
|
||||||
|
test(`load ${path} without console errors`, async ({ page }) => {
|
||||||
|
const errors = attachConsoleGuard(page);
|
||||||
|
await page.goto(path, { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page).toHaveURL(expectUrl);
|
||||||
|
await expect(page.locator("body")).toContainText(expectText);
|
||||||
|
expect(errors, errors.join("\n")).toEqual([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("signed in reload matrix", () => {
|
||||||
|
test.use(mobile);
|
||||||
|
|
||||||
|
test("core app routes reload cleanly", async ({ page }) => {
|
||||||
|
const errors = attachConsoleGuard(page);
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `deeplink-${stamp}@rakazo.test`, "password12", "Deep Link");
|
||||||
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
|
||||||
|
const groupId = page.url().match(/\/app\/g\/([^/]+)/)?.[1];
|
||||||
|
const bots = await rpc<Array<{ id: string }>>(page, "bots/list", null);
|
||||||
|
const botId = bots[0]?.id;
|
||||||
|
expect(groupId).toBeTruthy();
|
||||||
|
expect(botId).toBeTruthy();
|
||||||
|
|
||||||
|
const routes = [`/app/heute`, `/app/g/${groupId}`, `/app/${botId}`, `/onboarding`];
|
||||||
|
for (const route of routes) {
|
||||||
|
await page.goto(route, { waitUntil: "domcontentloaded" });
|
||||||
|
if (route === "/onboarding") {
|
||||||
|
await expect(page).toHaveURL(/\/(app|onboarding)/);
|
||||||
|
} else {
|
||||||
|
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await page.reload({ waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page.locator("body")).not.toBeEmpty();
|
||||||
|
expect(errors, errors.join("\n")).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
17
apps/web/e2e/session-unavailable.spec.ts
Normal file
17
apps/web/e2e/session-unavailable.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { completeOnboarding, signup } from "./helpers";
|
||||||
|
|
||||||
|
test("shows SessionUnavailable when auth session cannot be loaded", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `session-down-${stamp}@rakazo.test`, "password12", "Session Down");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
await page.route("**/api/auth/get-session", (route) => route.abort("failed"));
|
||||||
|
await page.goto("/app/heute", { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page.getByText(/Can't reach the server|Server nicht erreichbar/i)).toBeVisible({
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: /Retry now|Jetzt erneut versuchen/i }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
30
apps/web/e2e/teaching-composer.spec.ts
Normal file
30
apps/web/e2e/teaching-composer.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { activeBotId, completeOnboarding, signup, waitForChatComposer } from "./helpers";
|
||||||
|
|
||||||
|
test("blocks the composer while a skill is being taught", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `teach-composer-${stamp}@rakazo.test`, "password12", "Teach Composer");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await waitForChatComposer(page);
|
||||||
|
|
||||||
|
await page.getByTitle("Agent computer").click();
|
||||||
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
|
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||||
|
const openButton = sidePanel.getByTestId("computer-preview-open");
|
||||||
|
await expect(openButton).toBeVisible();
|
||||||
|
await openButton.click();
|
||||||
|
const chrome = page.getByTestId("computer-chrome");
|
||||||
|
await chrome.getByTestId("teach-start-button").click();
|
||||||
|
await page.getByTestId("teach-goal-input").fill("Export weekly list");
|
||||||
|
await page.getByRole("button", { name: "Start recording" }).click();
|
||||||
|
await expect(page.getByTestId("teach-recording-overlay")).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText(
|
||||||
|
/Teaching in progress\. Stop teaching before sending a new message|Aufgabe wird angelernt\. Beende das Anlernen, bevor du eine neue Nachricht sendest\./i,
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
const composer = page.getByRole("combobox", { name: /^Message$|^Nachricht$/i });
|
||||||
|
await expect(composer).toBeDisabled();
|
||||||
|
expect(activeBotId(page)).toBeTruthy();
|
||||||
|
});
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta name="description" content="Deine Oos — immer da, immer wach." />
|
<meta name="description" content="Deine Oos — immer da, immer wach." />
|
||||||
<meta name="theme-color" content="#07080a" />
|
<meta name="theme-color" content="#07080a" />
|
||||||
<meta name="apple-mobile-web-app-title" content="evOo" />
|
<meta name="apple-mobile-web-app-title" content="evOo" />
|
||||||
|
|||||||
@ -158,9 +158,9 @@ export function HeuteBrief({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="heute-brief">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="heute-brief">
|
||||||
<div className="flex items-start justify-between gap-3 border-b border-border px-4 py-5 md:px-8 lg:px-10">
|
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border px-4 py-5 md:px-8 lg:px-10">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<h1 className="text-ui-title font-semibold tracking-tight text-foreground">
|
<h1 className="break-words text-ui-title font-semibold tracking-tight text-foreground">
|
||||||
{dateLabel}
|
{dateLabel}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-ui-body text-muted-foreground" data-testid="heute-status">
|
<p className="mt-1 text-ui-body text-muted-foreground" data-testid="heute-status">
|
||||||
@ -171,7 +171,9 @@ export function HeuteBrief({
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CoachRhythmControl />
|
<div className="min-w-0 max-w-full shrink-0">
|
||||||
|
<CoachRhythmControl />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rk-scroll flex-1 overflow-y-auto px-4 py-6 md:px-8 lg:px-10">
|
<div className="rk-scroll flex-1 overflow-y-auto px-4 py-6 md:px-8 lg:px-10">
|
||||||
|
|||||||
@ -2717,7 +2717,7 @@ export function ShellPage() {
|
|||||||
<div
|
<div
|
||||||
data-testid="shell-root"
|
data-testid="shell-root"
|
||||||
data-ready={shellReady}
|
data-ready={shellReady}
|
||||||
className="relative flex h-full min-w-0 overflow-hidden bg-background text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
|
className="relative flex h-full min-w-0 overflow-hidden bg-background text-foreground/90 p-0 max-md:pt-[env(safe-area-inset-top,0px)] max-md:pb-[env(safe-area-inset-bottom,0px)] md:p-3 gap-0 md:gap-3"
|
||||||
onTouchStartCapture={onMobileSidebarTouchStart}
|
onTouchStartCapture={onMobileSidebarTouchStart}
|
||||||
onTouchEndCapture={onMobileSidebarTouchEnd}
|
onTouchEndCapture={onMobileSidebarTouchEnd}
|
||||||
onTouchCancelCapture={onMobileSidebarTouchCancel}
|
onTouchCancelCapture={onMobileSidebarTouchCancel}
|
||||||
@ -3203,7 +3203,7 @@ export function ShellPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Back to timeline`}
|
aria-label={t`Back to timeline`}
|
||||||
onClick={closeProjectSession}
|
onClick={closeProjectSession}
|
||||||
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent"
|
className="app-no-drag rk-touch-target grid size-control-lg min-h-control-lg min-w-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent"
|
||||||
>
|
>
|
||||||
<ArrowLeft strokeWidth={1.8} aria-hidden="true" className="size-icon-lg" />
|
<ArrowLeft strokeWidth={1.8} aria-hidden="true" className="size-icon-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function SidebarToggleButtons({
|
|||||||
data-testid="open-navigation"
|
data-testid="open-navigation"
|
||||||
aria-label={t`Open navigation`}
|
aria-label={t`Open navigation`}
|
||||||
onClick={onOpenNavigation}
|
onClick={onOpenNavigation}
|
||||||
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
|
className="app-no-drag rk-touch-target grid size-control-lg min-h-control-lg min-w-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
|
||||||
>
|
>
|
||||||
<Menu strokeWidth={1.7} className="size-icon-lg" />
|
<Menu strokeWidth={1.7} className="size-icon-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# evOo-local production readiness (Phase 1)
|
# evOo-local production readiness
|
||||||
|
|
||||||
Audit of user journeys, automated coverage, known defects, and Phase 2 backlog. Scope: web app (`apps/web`), API, shared packages; mobile (`apps/mobile`) noted where CI does not cover it.
|
Audit of user journeys, automated coverage, known defects, and backlog. Scope: web app (`apps/web`), API, shared packages; mobile (`apps/mobile`) noted where CI does not cover it.
|
||||||
|
|
||||||
## Product surfaces
|
## Product surfaces
|
||||||
|
|
||||||
@ -23,11 +23,11 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| Look category & swipe | `/onboarding` emoji step | `onboarding-oo-swipe`, `onboarding-emoji-first` | swipe spec (desktop viewport) | — | Mobile-specific swipe gestures |
|
| Look category & swipe | `/onboarding` emoji step | `onboarding-oo-swipe`, `onboarding-emoji-first` | swipe spec (desktop viewport) | — | Mobile-specific swipe gestures |
|
||||||
| Name Oo | name step | `first-run-onboarding-ux` | partial | — | — |
|
| Name Oo | name step | `first-run-onboarding-ux` | partial | — | — |
|
||||||
| Language | language step | `first-run-onboarding-ux`, `ui-locale` | DE skip label | — | — |
|
| Language | language step | `first-run-onboarding-ux`, `ui-locale` | DE skip label | — | — |
|
||||||
| Voice | voice step | `first-run-onboarding-ux`, `voice` | DE voices | — | Kokoro offline errors |
|
| Voice | voice step | `first-run-onboarding-ux`, `voice` | DE voices | — | Offline voice errors (no vendor names in UI) |
|
||||||
| Integrations / scan | optional steps | `onboarding-discovery`, `onboarding-complete` | — | onboarding PG | — |
|
| Integrations / scan | optional steps | `onboarding-discovery`, `onboarding-complete` | — | onboarding PG | — |
|
||||||
| Reload mid-onboarding | `/onboarding` | **`onboarding-reload-persistence`** (this PR) | same | `onboarding-draft.test` | — |
|
| Reload mid-onboarding | `/onboarding` | `onboarding-reload-persistence` | same | `onboarding-draft.test` | — |
|
||||||
| Intake cards (focus, goal, rhythm, quiet hours) | `/app/g/…` | `onboarding-intake` | **`onboarding-intake-de`** | onboarding-intake PG | Full DE rhythm/quiet copy |
|
| Intake cards (focus, goal, rhythm, quiet hours) | `/app/g/…` | `onboarding-intake` | **`onboarding-intake-de`** | onboarding-intake PG | Full DE rhythm/quiet copy in cards (UI localized; server stores locale-aware focus facts) |
|
||||||
| Deep link `/onboarding` while signed in | redirect rules | partial via auth specs | — | — | Explicit deep-link matrix |
|
| Deep link `/onboarding` while signed in | redirect rules | **`route-deep-link-matrix`** | same | — | — |
|
||||||
|
|
||||||
### Session & auth
|
### Session & auth
|
||||||
|
|
||||||
@ -35,16 +35,16 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
|--------|--------|-----|------|
|
|--------|--------|-----|------|
|
||||||
| Sign in / out | `/sign-in`, `/` | `auth-lifecycle`, `browser-auth` | Tailscale / cookie edge cases |
|
| Sign in / out | `/sign-in`, `/` | `auth-lifecycle`, `browser-auth` | Tailscale / cookie edge cases |
|
||||||
| Password reset | `/forgot-password`, `/reset-password` | `auth-lifecycle` | — |
|
| Password reset | `/forgot-password`, `/reset-password` | `auth-lifecycle` | — |
|
||||||
| Session unavailable / retry | `/app` loading | `module-recovery` | — |
|
| Session unavailable / retry | `/app` loading | **`session-unavailable`**, `module-recovery` | — |
|
||||||
| Welcome (signed out) | `/` | `smoke` | — |
|
| Welcome (signed out) | `/` | `smoke`, **`route-deep-link-matrix`** | — |
|
||||||
|
|
||||||
### Heute (daily brief)
|
### Heute (daily brief)
|
||||||
|
|
||||||
| Journey | Route | E2E | Gaps |
|
| Journey | Route | E2E | Gaps |
|
||||||
|--------|-------|-----|------|
|
|--------|-------|-----|------|
|
||||||
| Open Heute | `/app/heute` | `heute-brief` | Mobile layout overflow audit |
|
| Open Heute | `/app/heute` | `heute-brief`, **`mobile-layout-audit`** | — |
|
||||||
| Coach / goals list | Heute UI | `heute-brief` | — |
|
| Coach / goals list | Heute UI | `heute-brief` | — |
|
||||||
| Deep link reload | `/app/heute` | partial | Console errors on cold load |
|
| Deep link reload | `/app/heute` | **`route-deep-link-matrix`**, **`journey-console-errors`** | — |
|
||||||
|
|
||||||
### 1:1 & group chat
|
### 1:1 & group chat
|
||||||
|
|
||||||
@ -53,12 +53,12 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| Main conversation | `/app/g/:groupId` | `main-conversation`, `first-conversation` | — |
|
| Main conversation | `/app/g/:groupId` | `main-conversation`, `first-conversation` | — |
|
||||||
| Bot thread | `/app/:botId` | many specs | — |
|
| Bot thread | `/app/:botId` | many specs | — |
|
||||||
| Send message | composer | `message-send` | — |
|
| Send message | composer | `message-send` | — |
|
||||||
| Streaming / progress | transcript | `chat-progress-messages` (fixture), **`chat-queued-progress`** | Real LLM latency (mocked) |
|
| Streaming / progress | transcript | `chat-progress-messages`, `chat-queued-progress` | Real LLM latency (mocked) |
|
||||||
| Queued / slow model UI | composer + transcript | **`chat-queued-progress`** | Multi-run queue depth |
|
| Queued / slow model UI | composer + transcript | `chat-queued-progress` | Multi-run queue depth |
|
||||||
| Errors & retry | composer banner | `run-failure`, `computer-screen-error` | — |
|
| Errors & retry | composer banner | `run-failure`, `computer-screen-error` | — |
|
||||||
| Stop run | composer | `golden` | — |
|
| Stop run | composer | `golden` | — |
|
||||||
| Peer / group | `/app/g/…` | `group-chats`, `peer-messages` | — |
|
| Peer / group | `/app/g/…` | `group-chats`, `peer-messages` | — |
|
||||||
| Console cleanliness | main paths | **`journey-console-errors`** | Full matrix all routes |
|
| Console cleanliness | main paths | **`journey-console-errors`** (full route + settings matrix) | — |
|
||||||
|
|
||||||
### Oos (bots)
|
### Oos (bots)
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| Create project space | `spaces`, `adaptive-workspace` | — |
|
| Create project space | `spaces`, `adaptive-workspace` | — |
|
||||||
| Bind folder + git | `project-main-agent` | — |
|
| Bind folder + git | `project-main-agent` | — |
|
||||||
| Timeline / goals | `project-main-agent` | Voice-first goal card |
|
| Timeline / goals | `project-main-agent` | Voice-first goal card |
|
||||||
| Empty project | partial | Dedicated empty-state spec |
|
| Empty project | **`empty-project-state`** | — |
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| General / account | `settings-shell`, `user-profile` | — |
|
| General / account | `settings-shell`, `user-profile` | — |
|
||||||
| Language (UI locale) | `ui-locale` | — |
|
| Language (UI locale) | `ui-locale` | — |
|
||||||
| Voice | `voice`, `media-settings` | — |
|
| Voice | `voice`, `media-settings` | — |
|
||||||
| Goal / intake copy (DE) | **`onboarding-intake-de`** | Profile fact *values* remain EN from server |
|
| Goal / intake copy (DE) | **`onboarding-intake-de`** (focus fact in profile) | Extend locale matrix beyond DE |
|
||||||
| Privacy / delete account | `settings-privacy` | Docker/network cleanup verified in adapters unit; PG journey extension |
|
| Privacy / delete account | `settings-privacy` | — |
|
||||||
| Model connection | `model-settings`, `models-managed` | Offline model banner |
|
| Model connection | `model-settings`, `models-managed` | Offline model banner |
|
||||||
| Notifications / quiet hours | `notification-settings` | — |
|
| Notifications / quiet hours | `notification-settings` | — |
|
||||||
| Messaging | `messaging-settings` | — |
|
| Messaging | `messaging-settings` | — |
|
||||||
@ -96,7 +96,7 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
|
|
||||||
| Journey | E2E / PG | Gaps |
|
| Journey | E2E / PG | Gaps |
|
||||||
|--------|----------|------|
|
|--------|----------|------|
|
||||||
| Delete account (API) | PG journey 11 | **Team sandbox + `data/homes/team-*`** cleanup added in API (this PR); PG assertion Phase 2 |
|
| Delete account (API) | PG journey 11, **`account-delete-sandbox.postgres`** | — |
|
||||||
| Delete empty space | authorization PG | — |
|
| Delete empty space | authorization PG | — |
|
||||||
|
|
||||||
### Empty, error, offline
|
### Empty, error, offline
|
||||||
@ -104,9 +104,9 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| Journey | Coverage | Gaps |
|
| Journey | Coverage | Gaps |
|
||||||
|--------|------------|------|
|
|--------|------------|------|
|
||||||
| No model connected | `model-settings`, composer banner | — |
|
| No model connected | `model-settings`, composer banner | — |
|
||||||
| Server unreachable | `SessionUnavailable` UI | E2E |
|
| Server unreachable | **`session-unavailable`** | — |
|
||||||
| Empty inbox / no bots | partial | — |
|
| Empty inbox / no bots | partial | — |
|
||||||
| Teaching blocks composer | Shell copy | E2E |
|
| Teaching blocks composer | **`teaching-composer`** | — |
|
||||||
|
|
||||||
### Navigation, keyboard, a11y
|
### Navigation, keyboard, a11y
|
||||||
|
|
||||||
@ -115,7 +115,9 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
| Escape closes mobile drawer | `first-run-onboarding-ux` mobile | — |
|
| Escape closes mobile drawer | `first-run-onboarding-ux` mobile | — |
|
||||||
| Command palette | `command-palette` | — |
|
| Command palette | `command-palette` | — |
|
||||||
| Mention picker keyboard | `mention-picker-keyboard` | — |
|
| Mention picker keyboard | `mention-picker-keyboard` | — |
|
||||||
| Collapsible sidebar | `collapsible-sidebar-sections`, `mobile-sidebar-swipe` | Touch target audit (44px) |
|
| Collapsible sidebar | `collapsible-sidebar-sections`, `mobile-sidebar-swipe` | — |
|
||||||
|
| Mobile 390px overflow | **`mobile-layout-audit`** | Broader route list |
|
||||||
|
| Touch targets ≥44px | **`mobile-layout-audit`** | Broader control list |
|
||||||
| Focus management on drawer close | mobile esc test | Broader focus trap review |
|
| Focus management on drawer close | mobile esc test | Broader focus trap review |
|
||||||
|
|
||||||
### Integrations & MCP
|
### Integrations & MCP
|
||||||
@ -130,30 +132,38 @@ Legend: **E2E** = Playwright under `apps/web/e2e/`; **Unit** = vitest in app/pac
|
|||||||
|
|
||||||
Severity: **P0** ship blocker · **P1** major UX/trust · **P2** polish · **P3** nice-to-have
|
Severity: **P0** ship blocker · **P1** major UX/trust · **P2** polish · **P3** nice-to-have
|
||||||
|
|
||||||
### Fixed in Phase 1 (this PR)
|
### Fixed in Phase 1 ([PR #175](https://github.com/cynarAI/evOo-local/pull/175))
|
||||||
|
|
||||||
| ID | Sev | Issue | Fix |
|
| ID | Sev | Issue | Fix |
|
||||||
|----|-----|-------|-----|
|
|----|-----|-------|-----|
|
||||||
| PR1-1 | P1 | Reload during onboarding lost look/name/step | Session draft via `onboarding-draft.ts` |
|
| PR1-1 | P1 | Reload during onboarding lost look/name/step | Session draft via `onboarding-draft.ts` |
|
||||||
| PR1-2 | P1 | DE UI: goal placeholder / discovery “Chief of Staff” English | Lingui for discovery; intake DE E2E |
|
| PR1-2 | P1 | DE UI: goal placeholder / discovery “Chief of Staff” English | Lingui for discovery; intake DE E2E |
|
||||||
| PR1-3 | P1 | Account delete left team sandbox + homes | `destroySpaceSandboxes` on account delete |
|
| PR1-3 | P1 | Account delete left team sandbox + homes | Scoped `destroySpaceSandboxes` on account delete |
|
||||||
| PR1-4 | P1 | Chat felt idle while queued / slow send | Composer `sending`+run chrome; queued label; 120s watchdog |
|
| PR1-4 | P1 | Chat felt idle while queued / slow send | Composer `sending`+run chrome; queued label; 120s watchdog |
|
||||||
| PR1-5 | P2 | Console errors on main journeys | `journey-console-errors` spec |
|
| PR1-5 | P2 | Console errors on main journeys | Initial `journey-console-errors` spec |
|
||||||
|
|
||||||
### Remaining — Phase 2+
|
### Fixed in Phase 2 (this PR)
|
||||||
|
|
||||||
|
| ID | Sev | Issue | Fix |
|
||||||
|
|----|-----|-------|-----|
|
||||||
|
| R2-1 | P1 | Profile focus fact values stored in English for DE users | `focusFactValue()` in `onboarding-intake.ts`; DE assertion in `onboarding-intake-de` |
|
||||||
|
| R2-2 | P1 | PG journey: account delete sandbox/home teardown | Phase 1 `account-delete-sandbox.postgres` + journey 11 |
|
||||||
|
| R2-3 | P2 | Persona-test console/network errors | Extended `journey-console-errors` + deep-link matrix; benign React Router / source-map filtering |
|
||||||
|
| R2-4 | P2 | 390px layout overflow | Heute header wrap; shell safe-area; **`mobile-layout-audit`** |
|
||||||
|
| R2-5 | P2 | Touch targets <44px | `rk-touch-target` on mobile nav/back; **`mobile-layout-audit`** |
|
||||||
|
| R2-6 | P2 | Deep link / reload matrix | **`route-deep-link-matrix`** |
|
||||||
|
| R2-7 | P2 | Empty project / SessionUnavailable / teaching composer E2E | **`empty-project-state`**, **`session-unavailable`**, **`teaching-composer`** |
|
||||||
|
|
||||||
|
### Remaining — Phase 3+
|
||||||
|
|
||||||
| ID | Sev | Issue | Notes |
|
| ID | Sev | Issue | Notes |
|
||||||
|----|-----|-------|-------|
|
|----|-----|-------|-------|
|
||||||
| R2-1 | P1 | Profile fact *values* (e.g. focus goal text) stored/displayed in English in DE UI | Server-side localization or locale-aware fact rendering |
|
| R3-1 | P2 | Offline / model-not-connected on mobile web | Dedicated E2E with RPC fixtures |
|
||||||
| R2-2 | P1 | PG journey: assert filesystem/docker cleanup after account delete | Extend journey 11 with fake sandbox + temp data dir |
|
| R3-2 | P2 | Intake card rhythm/quiet option labels server-side for non-EN history | Cards localized in web; stored English option labels for agent history |
|
||||||
| R2-3 | P2 | Five persona-test console/network errors (root causes) | Need symbolicated list from persona run logs |
|
| R3-3 | P3 | Mobile native parity for onboarding reload | Expo secure store draft |
|
||||||
| R2-4 | P2 | 390px layout overflow on long German strings | Audit Heute, settings, project timeline |
|
| R3-4 | P3 | Intake card English questions in non-EN tests | Extend locale matrix beyond DE |
|
||||||
| R2-5 | P2 | Touch targets <44px in dense sidebar rows | Measure `sidebar-unterhaltungen` rows |
|
| R3-5 | P3 | NPS/SUS follow-ups from persona round 2 | Product research, not CI |
|
||||||
| R2-6 | P2 | Deep link every route while signed out/in | Table-driven Playwright matrix |
|
| R3-6 | P3 | Tailscale / exotic auth cookie edge cases | Manual / staging |
|
||||||
| R2-7 | P2 | Offline / model-not-connected on mobile web | No E2E |
|
|
||||||
| R2-8 | P3 | Mobile native parity for onboarding reload | Expo secure store draft |
|
|
||||||
| R2-9 | P3 | Intake card English questions in non-EN tests | Extend locale matrix beyond DE |
|
|
||||||
| R2-10 | P3 | NPS/SUS follow-ups from persona round 2 | Product research, not CI |
|
|
||||||
|
|
||||||
## CI gates (Product)
|
## CI gates (Product)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user