Skip to content

Commit 5ff3da4

Browse files
author
root
committed
Harden script process spawning and localize e2e assertions
1 parent 8d27e65 commit 5ff3da4

30 files changed

Lines changed: 548 additions & 165 deletions

e2e/fixtures/i18n.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from "vitest";
2+
import { translateForE2E } from "./i18n.js";
3+
4+
describe("translateForE2E", () => {
5+
it("defaults to the app default locale for welcome copy", () => {
6+
expect(translateForE2E("welcome.kicker")).toBe("开始使用");
7+
expect(translateForE2E("action.open_workspace")).toBe("打开工作区");
8+
});
9+
10+
it("can resolve English strings when requested", () => {
11+
expect(translateForE2E("welcome.kicker", "en")).toBe("GET STARTED");
12+
expect(translateForE2E("workspace.launch.title", "en")).toBe("Open Workspace");
13+
});
14+
15+
it("interpolates variables", () => {
16+
expect(translateForE2E("workspace.launch.items_count", "zh", { count: 3 })).toBe("3 项");
17+
});
18+
});

e2e/fixtures/i18n.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { readFileSync } from "node:fs";
2+
3+
function readLocale(path: string) {
4+
return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")) as Record<
5+
string,
6+
unknown
7+
>;
8+
}
9+
10+
export const E2E_LOCALES = {
11+
en: readLocale("../../packages/web/src/locales/en.json"),
12+
zh: readLocale("../../packages/web/src/locales/zh.json"),
13+
} as const;
14+
15+
export type E2ELocaleCode = keyof typeof E2E_LOCALES;
16+
17+
type NestedKeyOf<T> = T extends object
18+
? {
19+
[K in keyof T]: K extends string
20+
? T[K] extends object
21+
? `${K}.${NestedKeyOf<T[K]>}`
22+
: K
23+
: never;
24+
}[keyof T]
25+
: never;
26+
27+
export type E2ETranslationKey = NestedKeyOf<(typeof E2E_LOCALES)["zh"]>;
28+
29+
function getNestedValue(obj: unknown, path: string): string | undefined {
30+
const parts = path.split(".");
31+
let current: unknown = obj;
32+
33+
for (const part of parts) {
34+
if (current === null || current === undefined) return undefined;
35+
if (typeof current !== "object") return undefined;
36+
current = (current as Record<string, unknown>)[part];
37+
}
38+
39+
return typeof current === "string" ? current : undefined;
40+
}
41+
42+
export function translateForE2E(
43+
key: E2ETranslationKey,
44+
locale: E2ELocaleCode = "zh",
45+
params?: Record<string, string | number>
46+
): string {
47+
let text = getNestedValue(E2E_LOCALES[locale], key);
48+
49+
if (text === undefined) {
50+
throw new Error(`Missing translation for key: ${key}`);
51+
}
52+
53+
if (params) {
54+
for (const [name, value] of Object.entries(params)) {
55+
text = text.replace(new RegExp(`\\{${name}\\}`, "g"), String(value));
56+
}
57+
}
58+
59+
return text;
60+
}

e2e/fixtures/phase1-i18n.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect, type Locator, type Page } from "@playwright/test";
2+
import { translateForE2E } from "./i18n.js";
3+
4+
export async function expectWelcomeCopy(page: Page): Promise<void> {
5+
await expect(page.locator(".welcome-kicker")).toHaveText(translateForE2E("welcome.kicker"));
6+
await expect(page.locator(".welcome-title")).toHaveText(translateForE2E("welcome.title"));
7+
await expect(page.locator(".welcome-body")).toContainText(translateForE2E("welcome.description"));
8+
}
9+
10+
export async function expectOpenWorkspaceButton(locator: Locator): Promise<void> {
11+
await expect(locator).toBeVisible();
12+
await expect(locator.locator("span")).toContainText(translateForE2E("action.open_workspace"));
13+
}
14+
15+
export async function expectSettingsButton(locator: Locator): Promise<void> {
16+
await expect(locator).toBeVisible();
17+
await expect(locator.locator("span")).toContainText(translateForE2E("action.settings"));
18+
}

e2e/fixtures/phase2-i18n.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { type Page } from "@playwright/test";
2+
import { type E2ELocaleCode, translateForE2E } from "./i18n.js";
3+
4+
type SettingsSection = "general" | "appearance" | "providers" | "shortcuts";
5+
type ProviderSettingLabel =
6+
| "base"
7+
| "config_file"
8+
| "open_config_file_editor"
9+
| "back_to_base"
10+
| "startup_args";
11+
type SettingsGroupLabel = "notifications" | "theme" | "language";
12+
type ConfigFileLabel = "claude" | "codex";
13+
14+
const SETTINGS_SECTION_KEYS: Record<SettingsSection, Parameters<typeof translateForE2E>[0]> = {
15+
general: "settings.general",
16+
appearance: "settings.appearance",
17+
providers: "settings.providers",
18+
shortcuts: "settings.shortcuts.title",
19+
};
20+
21+
const PROVIDER_SETTING_KEYS: Record<ProviderSettingLabel, Parameters<typeof translateForE2E>[0]> = {
22+
base: "settings.provider.base",
23+
config_file: "settings.provider.config_file",
24+
open_config_file_editor: "settings.provider.open_config_file_editor",
25+
back_to_base: "settings.provider.back_to_base",
26+
startup_args: "settings.provider.startup_args",
27+
};
28+
29+
const SETTINGS_GROUP_KEYS: Record<SettingsGroupLabel, Parameters<typeof translateForE2E>[0]> = {
30+
notifications: "settings.notifications",
31+
theme: "settings.theme.title",
32+
language: "settings.language.title",
33+
};
34+
35+
const CONFIG_FILE_KEYS: Record<ConfigFileLabel, Parameters<typeof translateForE2E>[0]> = {
36+
claude: "settings.config_files.claude_config",
37+
codex: "settings.config_files.codex_config",
38+
};
39+
40+
export const AUTH_PREVIEW_URL = new URL("../../packages/web/auth-preview.html", import.meta.url)
41+
.href;
42+
43+
function escapeRegExp(value: string): string {
44+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45+
}
46+
47+
function localizedPattern(
48+
key: Parameters<typeof translateForE2E>[0],
49+
params?: Record<string, string | number>
50+
): RegExp {
51+
const en = translateForE2E(key, "en", params);
52+
const zh = translateForE2E(key, "zh", params);
53+
const values = [...new Set([en, zh])].map(escapeRegExp);
54+
return new RegExp(`^(?:${values.join("|")})$`);
55+
}
56+
57+
export function settingsSectionLabel(
58+
section: SettingsSection,
59+
locale: E2ELocaleCode = "zh"
60+
): string {
61+
return translateForE2E(SETTINGS_SECTION_KEYS[section], locale);
62+
}
63+
64+
export function settingsSectionPattern(section: SettingsSection): RegExp {
65+
return localizedPattern(SETTINGS_SECTION_KEYS[section]);
66+
}
67+
68+
export async function openSettingsSection(
69+
page: Page,
70+
section: SettingsSection,
71+
locale?: E2ELocaleCode
72+
): Promise<void> {
73+
await page
74+
.getByRole("button", {
75+
name: locale ? settingsSectionLabel(section, locale) : settingsSectionPattern(section),
76+
})
77+
.click();
78+
}
79+
80+
export function providerSettingLabel(
81+
label: ProviderSettingLabel,
82+
locale: E2ELocaleCode = "zh"
83+
): string {
84+
return translateForE2E(PROVIDER_SETTING_KEYS[label], locale);
85+
}
86+
87+
export function providerSettingPattern(label: ProviderSettingLabel): RegExp {
88+
return localizedPattern(PROVIDER_SETTING_KEYS[label]);
89+
}
90+
91+
export function settingsGroupLabel(
92+
label: SettingsGroupLabel,
93+
locale: E2ELocaleCode = "zh"
94+
): string {
95+
return translateForE2E(SETTINGS_GROUP_KEYS[label], locale);
96+
}
97+
98+
export function settingsGroupPattern(label: SettingsGroupLabel): RegExp {
99+
return localizedPattern(SETTINGS_GROUP_KEYS[label]);
100+
}
101+
102+
export function configFileLabel(label: ConfigFileLabel, locale: E2ELocaleCode = "zh"): string {
103+
return translateForE2E(CONFIG_FILE_KEYS[label], locale);
104+
}
105+
106+
export function configFilePattern(label: ConfigFileLabel): RegExp {
107+
return localizedPattern(CONFIG_FILE_KEYS[label]);
108+
}

e2e/specs/phase1/agent-session.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectOpenWorkspaceButton, expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
test.describe("@phase1 agent session acceptance", () => {
45
test("F1-06 start session", async ({ page }) => {
56
await page.goto("/");
67
// Welcome page should render correctly
78
await expect(page.locator(".welcome-container")).toBeVisible();
8-
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
9-
await expect(page.locator(".welcome-title")).toBeVisible();
9+
await expectWelcomeCopy(page);
1010
});
1111

1212
test("F1-07 send prompt", async ({ page }) => {
1313
await page.goto("/");
1414
// Check welcome page elements
1515
const openBtn = page.locator(".welcome-btn");
16-
await expect(openBtn).toBeVisible();
17-
await expect(openBtn.locator("span")).toContainText("Open Workspace");
16+
await expectOpenWorkspaceButton(openBtn);
1817
});
1918

2019
test("F1-08 receive response", async ({ page }) => {

e2e/specs/phase1/data-integrity.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
test.describe("@phase1 data integrity acceptance", () => {
45
test("F1-37 file persistence", async ({ page }) => {
@@ -9,8 +10,8 @@ test.describe("@phase1 data integrity acceptance", () => {
910

1011
test("F1-38 session persistence", async ({ page }) => {
1112
await page.goto("/");
12-
// Check kicker
13-
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
13+
// Check translated welcome copy
14+
await expectWelcomeCopy(page);
1415
});
1516

1617
test("F1-39 terminal replay", async ({ page }) => {

e2e/specs/phase1/editor.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
test.describe("@phase1 editor acceptance", () => {
45
test("F1-11 open file", async ({ page }) => {
@@ -9,9 +10,8 @@ test.describe("@phase1 editor acceptance", () => {
910

1011
test("F1-12 edit content", async ({ page }) => {
1112
await page.goto("/");
12-
// Check welcome body text
13-
const body = page.locator(".welcome-body");
14-
await expect(body).toContainText("A local-first AI coding workbench.");
13+
// Check translated welcome copy
14+
await expectWelcomeCopy(page);
1515
});
1616

1717
test("F1-13 save file", async ({ page }) => {

e2e/specs/phase1/focus-mode.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
test.describe("@phase1 focus mode acceptance", () => {
45
test("F1-27 enter focus", async ({ page }) => {
@@ -9,7 +10,7 @@ test.describe("@phase1 focus mode acceptance", () => {
910

1011
test("F1-28 exit focus", async ({ page }) => {
1112
await page.goto("/");
12-
// Check kicker text
13-
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
13+
// Check translated welcome copy
14+
await expectWelcomeCopy(page);
1415
});
1516
});

e2e/specs/phase1/git.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
test.describe("@phase1 git acceptance", () => {
45
test("F1-16 view status", async ({ page }) => {
@@ -9,8 +10,8 @@ test.describe("@phase1 git acceptance", () => {
910

1011
test("F1-17 view diff", async ({ page }) => {
1112
await page.goto("/");
12-
// Check welcome elements
13-
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
13+
// Check translated welcome copy
14+
await expectWelcomeCopy(page);
1415
});
1516

1617
test("F1-18 commit", async ({ page }) => {

e2e/specs/phase1/visual-components.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";
23

34
/**
45
* Phase 1 Visual Acceptance Tests: Core Components
@@ -16,8 +17,8 @@ test.describe("@phase1 visual acceptance", () => {
1617

1718
test("V1-05 workspace panel baseline", async ({ page }) => {
1819
await page.goto("/");
19-
// Welcome kicker should be present
20-
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
20+
// Welcome copy should be present in the active locale
21+
await expectWelcomeCopy(page);
2122
});
2223

2324
test("V1-06 agent pane baseline", async ({ page }) => {

0 commit comments

Comments
 (0)