From 08ff96eec8876cebfa9b6d317cd6aef8a3e3c9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 12:23:17 -0400 Subject: [PATCH] S3.3: pixel components: name-plate dialog titles, sunken fields, raised tabs, notched badges, XP bars, inventory slots, guide hint bubbles, game-over errors with Continue Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/app/StartupScreen.tsx | 14 ++--- src/renderer/src/i18n/zh.ts | 12 +++-- src/renderer/src/overlays.css | 3 +- src/renderer/src/pages/HomePage.tsx | 9 ++-- src/renderer/src/pages/LibraryPage.tsx | 3 +- src/renderer/src/pages/TeamPage.tsx | 11 ++-- src/renderer/src/pages/WorkPage.tsx | 3 +- src/renderer/src/pages/files/FilesGrid.tsx | 16 ++---- .../src/pages/settings/BillingPanel.tsx | 5 +- src/renderer/src/shared/ErrorBoundary.tsx | 20 +++---- src/renderer/src/shared/GameOver.tsx | 16 ++++++ src/renderer/src/shared/HintBubble.tsx | 12 +++++ src/renderer/src/shared/XpBar.tsx | 9 ++++ src/renderer/src/styles.css | 2 + src/renderer/src/styles/components.css | 53 +++++++++---------- src/renderer/src/styles/pixel.css | 26 +++++++++ tests/e2e/pixel-style.spec.ts | 14 +++++ tests/e2e/render-recovery.spec.ts | 4 ++ tests/e2e/startup-failure.spec.ts | 3 +- 19 files changed, 149 insertions(+), 86 deletions(-) create mode 100644 src/renderer/src/shared/GameOver.tsx create mode 100644 src/renderer/src/shared/HintBubble.tsx create mode 100644 src/renderer/src/shared/XpBar.tsx diff --git a/src/renderer/src/app/StartupScreen.tsx b/src/renderer/src/app/StartupScreen.tsx index b6104620..9516c18d 100644 --- a/src/renderer/src/app/StartupScreen.tsx +++ b/src/renderer/src/app/StartupScreen.tsx @@ -1,4 +1,4 @@ -import { Button } from "../shared/ui"; +import { GameOver } from "../shared/GameOver"; import { t } from "../i18n/i18n"; import type { SyncFailure } from "./useSnapshot"; @@ -7,12 +7,12 @@ export function StartupScreen({ failure, onRetry }: { failure: SyncFailure | nul return (
{failure ? ( -
- {t("AgentCompany could not load its data.")} -
{failure.message}
-
{t("Retrying every few seconds (attempt {count}).", { count: failure.attempts })}
-
-
+ ) : (
{t("Starting AgentCompany…")}
)} diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index d70069bb..9dc04b83 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -601,6 +601,13 @@ export const zh: Record = { "Version (semver)": "版本(semver)", "Visibility": "可见性", "Needs you": "需要你处理", + "Goal progress": "目标进度", + "Budget allocated": "已分配预算", + "Budget compared with the largest agent budget": "与最高智能体预算相比", + "GAME OVER": "游戏结束", + "The rest of the app still works; Continue tries this page again.": "应用其他部分仍可使用;点“继续”会重新打开此页。", + "Continue reloads the window; your data is kept.": "点“继续”会重新加载窗口;数据会保留。", + "Retrying every few seconds (attempt {count}); Continue tries now.": "每隔几秒自动重试(第 {count} 次);点“继续”立即重试。", "Follow the clock": "跟随时间(夜间变暗)", "Next in line": "下一项工作", "Stopped: {reason}": "已停下:{reason}", @@ -742,12 +749,7 @@ export const zh: Record = { "Stay": "留下", "This page could not be shown.": "此页面无法显示。", "AgentCompany hit an error it could not recover from here.": "AgentCompany 遇到了无法在此恢复的错误。", - "The rest of the app still works; go to another page or try this one again.": "应用的其他部分仍可使用;可以前往其他页面,或重试此页面。", - "Reloading the window starts it again; your data is kept.": "重新加载窗口即可重新开始;你的数据会保留。", - "Try again": "重试", - "Reload": "重新加载", "AgentCompany could not load its data.": "AgentCompany 无法加载数据。", - "Retrying every few seconds (attempt {count}).": "每隔几秒重试一次(第 {count} 次)。", "Retry now": "立即重试", "Starting AgentCompany…": "正在启动 AgentCompany…", "Direct: only its members take part, and every message wakes the other members. As the board, you can read it even when you are not a member.": "私聊:只有成员参与,每条消息都会唤醒其他成员。作为董事会,即使你不是成员也能阅读。", diff --git a/src/renderer/src/overlays.css b/src/renderer/src/overlays.css index 699a3246..b8ed57d5 100644 --- a/src/renderer/src/overlays.css +++ b/src/renderer/src/overlays.css @@ -23,7 +23,8 @@ from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: none; } } -.modal-title { margin: 0 0 var(--sp-4); font-size: var(--fs-title); font-weight: 600; letter-spacing: 0; } +/* Dialog name plate, as a speaker's name above an RPG text box. */ +.modal-title { display: inline-block; margin: 0 0 var(--sp-4); padding: 2px 10px; font-size: var(--fs-title); font-weight: 400; background: var(--btn-primary-bg); color: var(--btn-primary-fg); box-shadow: 2px 2px 0 0 var(--border-strong); } .modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-5); } .onboarding-window { diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx index 23891e66..88814dca 100644 --- a/src/renderer/src/pages/HomePage.tsx +++ b/src/renderer/src/pages/HomePage.tsx @@ -1,3 +1,4 @@ +import { XpBar } from "../shared/XpBar"; import { runTriggerLabel } from "../i18n/labels"; import { goalProgress, type GoalPace } from "./home/goal-progress"; import { nextUp } from "./home/next-up"; @@ -122,9 +123,7 @@ function GoalCard({ {progress?.pace ? statusLabel(progress.pace) : t("no target date")} {goal && }
-
-
-
+
{goal?.status === "active" && goal.stalledReason && (
{goal.stalledReason} @@ -156,9 +155,7 @@ function SpendCard({ {money(totalBudgetCents)} {capCents > 0 ? `/ ${money(capCents)}` : t("allocated")}
-
-
-
+
{connected.slice(0, 3).map((provider) => { const allocatedCents = agents.filter((agent) => agent.modelProviderId === provider.id).reduce((sum, agent) => sum + agent.budgetCents, 0); diff --git a/src/renderer/src/pages/LibraryPage.tsx b/src/renderer/src/pages/LibraryPage.tsx index 089c3c8a..8ac953dc 100644 --- a/src/renderer/src/pages/LibraryPage.tsx +++ b/src/renderer/src/pages/LibraryPage.tsx @@ -1,3 +1,4 @@ +import { HintBubble } from "../shared/HintBubble"; import { oneOf, usePersistentState } from "../shared/view-state"; import { useMemo } from "react"; import { DocumentsPanel } from "./library/DocumentsPanel"; @@ -49,7 +50,7 @@ export function LibraryPage({ snapshot, actions, focus }: PageProps) {

{tab === "documents" ? t("Documents") : t("Knowledge")}

-

{t("Agents read and write these while working on tasks. Nothing here starts work; a task does.")}

+ {t("Agents read and write these while working on tasks. Nothing here starts work; a task does.")} {tab === "documents" ? : } diff --git a/src/renderer/src/pages/TeamPage.tsx b/src/renderer/src/pages/TeamPage.tsx index 6a0feea2..250fe94b 100644 --- a/src/renderer/src/pages/TeamPage.tsx +++ b/src/renderer/src/pages/TeamPage.tsx @@ -1,3 +1,4 @@ +import { XpBar } from "../shared/XpBar"; import { useRevealObject } from "../shared/reveal"; import { oneOf, usePersistentState } from "../shared/view-state"; import { agentModel } from "../../../shared/agent-model"; @@ -322,14 +323,8 @@ function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onE — ) : (
-
-
0 ? Math.min(100, Math.round((row.budgetCents / row.budgetCapCents) * 100)) : 0}%`, - height: "100%", - background: "var(--text-2)", - }} - /> +
+ 0 ? (row.budgetCents / row.budgetCapCents) * 100 : 0} label={t("Budget compared with the largest agent budget")} />
{money(row.budgetCents)}
diff --git a/src/renderer/src/pages/WorkPage.tsx b/src/renderer/src/pages/WorkPage.tsx index 2692b2ae..080d52ce 100644 --- a/src/renderer/src/pages/WorkPage.tsx +++ b/src/renderer/src/pages/WorkPage.tsx @@ -1,3 +1,4 @@ +import { HintBubble } from "../shared/HintBubble"; import { oneOf, usePersistentState } from "../shared/view-state"; import { useMemo, useState } from "react"; import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui"; @@ -113,7 +114,7 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {

{t("Work")}

-

{t("Only a task runs: an agent picks it up and does the work. Goals and projects group tasks; documents and knowledge are what agents read and write along the way.")}

+ {t("Only a task runs: an agent picks it up and does the work. Goals and projects group tasks; documents and knowledge are what agents read and write along the way.")} {scope && (
diff --git a/src/renderer/src/pages/files/FilesGrid.tsx b/src/renderer/src/pages/files/FilesGrid.tsx index 62eec51f..1ef2f85b 100644 --- a/src/renderer/src/pages/files/FilesGrid.tsx +++ b/src/renderer/src/pages/files/FilesGrid.tsx @@ -26,7 +26,7 @@ export function FilesGrid({ onTogglePin: (file: FileObjectRecord) => void; }) { return ( -
+
{files.map((file) => ( { if (event.key === "Enter") onOpen(); } : undefined} - style={{ - border: "1px solid var(--border)", - borderRadius: 10, - background: "var(--panel)", - padding: 14, - display: "flex", - flexDirection: "column", - gap: 10, - minWidth: 0, - cursor: canOpen ? "pointer" : "default", - }} + style={{ cursor: canOpen ? "pointer" : "default" }} >
diff --git a/src/renderer/src/pages/settings/BillingPanel.tsx b/src/renderer/src/pages/settings/BillingPanel.tsx index 64b72849..4be8fc07 100644 --- a/src/renderer/src/pages/settings/BillingPanel.tsx +++ b/src/renderer/src/pages/settings/BillingPanel.tsx @@ -1,3 +1,4 @@ +import { XpBar } from "../../shared/XpBar"; import { Avatar, Pill } from "../../shared/ui"; import type { AgentRecord, HiringPolicyRecord, ProviderRecord } from "../../../../shared/domain"; import { money } from "../types"; @@ -52,9 +53,7 @@ export function BillingPanel({
{t("Spent this month")}
-
-
0 ? Math.min(100, Math.round((spentCents / totalBudget) * 100)) : 0}%`, height: "100%", background: "var(--accent)" }} /> -
+
0 ? (spentCents / totalBudget) * 100 : 0} label={t("Spent this month")} />
{t("${spent} of ${allocated} allocated to agents", { spent: (spentCents / 100).toFixed(2), allocated: (totalBudget / 100).toFixed(2) })} {unpricedCalls > 0 && <> · {t("{count} calls not priced", { count: unpricedCalls })}} diff --git a/src/renderer/src/shared/ErrorBoundary.tsx b/src/renderer/src/shared/ErrorBoundary.tsx index 2964b69e..7aaa7bf9 100644 --- a/src/renderer/src/shared/ErrorBoundary.tsx +++ b/src/renderer/src/shared/ErrorBoundary.tsx @@ -1,5 +1,5 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; -import { Button } from "./ui"; +import { GameOver } from "./GameOver"; import { t } from "../i18n/i18n"; /** Contains a render error to the part of the app that threw, says what failed, and offers a way back; the error is logged with its component stack. */ @@ -19,17 +19,13 @@ export class ErrorBoundary extends Component<{ scope: "page" | "app"; children: if (!error) return this.props.children; const page = this.props.scope === "page"; return ( -
- {page ? t("This page could not be shown.") : t("AgentCompany hit an error it could not recover from here.")} -
{error.message}
-
- {page ? t("The rest of the app still works; go to another page or try this one again.") : t("Reloading the window starts it again; your data is kept.")} -
-
- {page - ? - : } -
+
+ this.setState({ error: null }) : () => window.location.reload()} + />
); } diff --git a/src/renderer/src/shared/GameOver.tsx b/src/renderer/src/shared/GameOver.tsx new file mode 100644 index 00000000..a955e89c --- /dev/null +++ b/src/renderer/src/shared/GameOver.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from "react"; +import { Button } from "./ui"; +import { t } from "../i18n/i18n"; + +/** A failure as a game-over screen: what failed, the error itself, what happens next, and Continue to try again. */ +export function GameOver({ title, error, note, onContinue }: { title: string; error: string; note: ReactNode; onContinue: () => void }) { + return ( +
+ + {title} +
{error}
+
{note}
+
+
+ ); +} diff --git a/src/renderer/src/shared/HintBubble.tsx b/src/renderer/src/shared/HintBubble.tsx new file mode 100644 index 00000000..474b5fdc --- /dev/null +++ b/src/renderer/src/shared/HintBubble.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from "react"; +import { Icon } from "./Icon"; + +/** A hint spoken by the office guide: a pixel portrait and a speech bubble, so help reads like a character talking. */ +export function HintBubble({ children }: { children: ReactNode }) { + return ( +
+ +

{children}

+
+ ); +} diff --git a/src/renderer/src/shared/XpBar.tsx b/src/renderer/src/shared/XpBar.tsx new file mode 100644 index 00000000..5637542c --- /dev/null +++ b/src/renderer/src/shared/XpBar.tsx @@ -0,0 +1,9 @@ +/** An XP-style bar: a framed track filled in pixel blocks, announced as a progress value. */ +export function XpBar({ percent, label }: { percent: number; label: string }) { + const filled = Math.max(0, Math.min(100, Math.round(percent))); + return ( +
+
+
+ ); +} diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index e2418206..7b96d6f3 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -21,6 +21,7 @@ --amber: #a85a1d; --amber-soft: #fbe6c4; --red: #b13e53; + --red-soft: #f6d5da; --r-sm: 0; --r-md: 0; @@ -88,6 +89,7 @@ --amber: #ffcd75; --amber-soft: #3d3220; --red: #ef7d57; + --red-soft: #3d1f26; --shadow-sm: 2px 2px 0 0 #10111c; --shadow-md: 4px 4px 0 0 rgba(0, 0, 0, 0.5); diff --git a/src/renderer/src/styles/components.css b/src/renderer/src/styles/components.css index 72df7bc4..314ab5ba 100644 --- a/src/renderer/src/styles/components.css +++ b/src/renderer/src/styles/components.css @@ -65,41 +65,37 @@ .icon-button:disabled { opacity: 0.4; cursor: default; } .icon-button:disabled:hover { background: transparent; color: var(--text-2); } +/* Tabs: a strip of pixel tabs on a 2px rule; the chosen tab is raised and joins the panel below. */ .segmented { display: inline-flex; - align-items: center; - gap: var(--sp-1); - padding: var(--sp-1); - border: 1px solid var(--border); - border-radius: var(--r-sm); - background: var(--panel); + align-items: flex-end; + gap: 2px; + border-bottom: 2px solid var(--border-strong); } .segmented .segment { - height: 24px; - padding: 0 var(--sp-2); - border-radius: var(--r-sm); - border: 0; - background: transparent; + padding: 2px var(--sp-2); + margin-bottom: -2px; + border: 2px solid var(--border-strong); + border-radius: 0; + background: var(--panel-3); color: var(--text-2); font-size: var(--fs-caption); - font-weight: 500; - transition: background var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out); } -.segmented .segment:hover:not(.active) { color: var(--text-1); } -.segmented .segment.active { background: var(--panel-3); color: var(--text-1); font-weight: 540; } +.segmented .segment:hover:not(.active) { color: var(--text-1); background: var(--hover); } +.segmented .segment.active { background: var(--panel); color: var(--text-1); border-bottom-color: var(--panel); padding-bottom: 6px; } .pill { display: inline-flex; align-items: center; gap: var(--sp-1); - height: 20px; - padding: 0 var(--sp-2); - border-radius: var(--r-full); - background: var(--panel-2); + padding: 2px 8px; + border-radius: 0; + background: var(--panel-3); color: var(--text-2); - border: 1px solid var(--border); + border: 0; + /* Pixel badge: a filled block with its corner pixels notched out. */ + clip-path: polygon(2px 0, calc(100% - 2px) 0, 100% 2px, 100% calc(100% - 2px), calc(100% - 2px) 100%, 2px 100%, 0 calc(100% - 2px), 0 2px); font-size: var(--fs-caption); - font-weight: 500; white-space: nowrap; max-width: 100%; min-width: 0; @@ -107,13 +103,13 @@ } .pill .pill-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pill .dot { width: 6px; height: 6px; border-radius: 0; background: var(--text-3); flex-shrink: 0; transition: background var(--motion-base) var(--ease-out); } -.pill.amber { background: var(--amber-soft); color: var(--amber); border-color: transparent; } +.pill.amber { background: var(--amber-soft); color: var(--amber); } .pill.amber .dot { background: var(--amber); } -.pill.green { background: var(--green-soft); color: var(--green); border-color: transparent; } +.pill.green { background: var(--green-soft); color: var(--green); } .pill.green .dot { background: var(--green); } -.pill.violet { background: var(--accent-soft); color: var(--accent); border-color: transparent; } +.pill.violet { background: var(--accent-soft); color: var(--accent); } .pill.violet .dot { background: var(--accent); } -.pill.red { background: rgba(210, 59, 59, 0.1); color: var(--red); border-color: transparent; } +.pill.red { background: var(--red-soft); color: var(--red); } .pill.red .dot { background: var(--red); } .avatar { @@ -141,8 +137,9 @@ .input { width: 100%; - border: 1px solid var(--border-strong); - border-radius: var(--r-sm); + border: 2px solid var(--border-strong); + border-radius: 0; + box-shadow: inset 2px 2px 0 0 var(--border-soft); background: var(--panel); color: var(--text-1); padding: var(--sp-2) var(--sp-3); @@ -150,7 +147,7 @@ font-size: var(--fs-body); transition: border-color var(--motion-fast) var(--ease-out), box-shadow var(--motion-fast) var(--ease-out); } -.input:focus { border-color: var(--text-1); box-shadow: 0 0 0 3px rgba(24, 24, 24, 0.06); } +.input:focus { border-color: var(--accent); outline: 2px dashed var(--accent); outline-offset: 2px; } input[type="checkbox"] { accent-color: var(--text-1); } textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; } .field { diff --git a/src/renderer/src/styles/pixel.css b/src/renderer/src/styles/pixel.css index e7066841..7c48ef06 100644 --- a/src/renderer/src/styles/pixel.css +++ b/src/renderer/src/styles/pixel.css @@ -59,3 +59,29 @@ /* Icons share the font's grid: 12 icon pixels at the same whole-screen-pixel scale. */ .pixel-icon { width: calc(12px * var(--px)); height: calc(12px * var(--px)); flex-shrink: 0; } + +/* XP bar: a framed track, filled in 6px blocks with a 2px gap, a lighter top row for shine. */ +.xp-bar { height: calc(10px * var(--px)); padding: 1px; border: 2px solid var(--border-strong); background: var(--panel-3); } +.xp-fill { + height: 100%; + background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 8px); + box-shadow: inset 0 2px 0 0 rgba(255, 255, 255, 0.35); + transition: width var(--motion-slow) var(--ease-out); +} + +/* Hint bubble: the guide's portrait and a speech box with a stepped tail. */ +.hint-bubble { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 var(--sp-4); } +.hint-speaker { display: grid; place-items: center; width: calc(24px * var(--px)); height: calc(24px * var(--px)); background: var(--accent); color: var(--btn-primary-fg); box-shadow: 2px 2px 0 0 var(--border-strong); flex-shrink: 0; } +.hint-text { position: relative; margin: 0; padding: 6px 10px; background: var(--panel); border: 2px solid var(--border-strong); color: var(--text-2); } +.hint-text::before { content: ""; position: absolute; left: -8px; top: 6px; width: 6px; height: 8px; background: var(--border-strong); clip-path: polygon(100% 0, 100% 100%, 67% 100%, 67% 75%, 34% 75%, 34% 50%, 0 50%, 0 25%, 34% 25%, 34% 0); } + +/* Game over: a framed panel with a blinking banner and Continue. */ +.game-over { display: grid; gap: 12px; max-width: 520px; text-align: center; justify-items: center; } +.game-over-banner { font-size: var(--fs-display); color: var(--red); letter-spacing: 0.1em; animation: px-blink 1.2s steps(1, end) infinite; } +.game-over-error { font-size: var(--fs-caption); color: var(--text-2); word-break: break-word; } +.game-over-note { color: var(--text-3); } + +/* Inventory: files sit in square-cornered bevelled slots. */ +.inventory { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; padding: 8px; background: var(--panel-3); box-shadow: inset 2px 2px 0 0 var(--border-soft); } +.inventory-slot { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 12px; background: var(--panel); border: 2px solid var(--border-strong); box-shadow: inset -2px -2px 0 0 var(--border-soft), inset 2px 2px 0 0 #ffffff40; } +.inventory-slot:hover { background: var(--hover); } diff --git a/tests/e2e/pixel-style.spec.ts b/tests/e2e/pixel-style.spec.ts index 10acec8c..97fde6b0 100644 --- a/tests/e2e/pixel-style.spec.ts +++ b/tests/e2e/pixel-style.spec.ts @@ -60,7 +60,21 @@ test("text uses the bundled pixel font, and the palette follows the day part, se await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); + // Progress is an XP bar announced as a value; hints are spoken by the guide in a bubble. + await page.getByRole("button", { name: "Home" }).click(); + await expect(page.getByRole("progressbar", { name: "Goal progress" })).toHaveAttribute("aria-valuenow", "0"); + // On the roster the only agent holds the largest budget, and the bar says it compares budgets, not spend. + await page.getByRole("button", { name: "Team", exact: true }).click(); + await expect(page.getByRole("progressbar", { name: "Budget compared with the largest agent budget" })).toHaveAttribute("aria-valuenow", "100"); + await page.getByRole("button", { name: "Work", exact: true }).click(); + const hint = page.getByRole("note").filter({ hasText: "Only a task runs" }); + await expect(hint.locator(".hint-speaker svg.pixel-icon")).toHaveCount(1); + // Tabs are raised pixel tabs; fields are sunken pixel boxes. + const views = page.getByRole("radiogroup", { name: "View" }); + expect(await views.locator("[aria-checked=true]").evaluate((tab) => getComputedStyle(tab).borderBottomColor === getComputedStyle(tab).backgroundColor)).toBe(true); + await page.getByRole("button", { name: "Settings" }).click(); + expect(await page.getByRole("combobox", { name: "Theme" }).evaluate((field) => [getComputedStyle(field).borderTopWidth, getComputedStyle(field).borderRadius])).toEqual(["2px", "0px"]); await page.getByRole("combobox", { name: "Theme" }).selectOption("dark"); await expect(root).toHaveAttribute("data-theme", "dark"); await page.getByRole("combobox", { name: "Theme" }).selectOption({ label: "Follow the clock" }); diff --git a/tests/e2e/render-recovery.spec.ts b/tests/e2e/render-recovery.spec.ts index 6822fe77..7d13904f 100644 --- a/tests/e2e/render-recovery.spec.ts +++ b/tests/e2e/render-recovery.spec.ts @@ -33,6 +33,10 @@ test("a record that breaks one page leaves the rest of the app working", async ( await page.getByRole("button", { name: "Approvals" }).click(); const failure = page.getByRole("alert").filter({ hasText: "This page could not be shown." }); await expect(failure).toContainText("Approval risk tone is missing."); + // A failure reads as a game over, with Continue to try again; the same fault fails the same way. + await expect(failure).toContainText("GAME OVER"); + await failure.getByRole("button", { name: "Continue" }).click(); + await expect(failure).toContainText("Approval risk tone is missing."); await page.getByRole("button", { name: "Home" }).click(); await expect(failure).toHaveCount(0); await expect(page.getByRole("button", { name: "Approvals" })).toBeVisible(); diff --git a/tests/e2e/startup-failure.spec.ts b/tests/e2e/startup-failure.spec.ts index 370b96bd..98309b93 100644 --- a/tests/e2e/startup-failure.spec.ts +++ b/tests/e2e/startup-failure.spec.ts @@ -35,7 +35,8 @@ test("a first load that fails says why, keeps retrying, and recovers", async () await expect(failure).toContainText(/attempt [2-9]/); setDraft(null); - await page.getByRole("button", { name: "Retry now" }).click(); + await expect(failure).toContainText("GAME OVER"); + await page.getByRole("button", { name: "Continue" }).click(); await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible(); await app.close(); });