diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index 1945bb0d04..20302b4f08 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -7,7 +7,7 @@ zh: 3fd3ff9fcd1e1e90 /apps-web-browser-e2e/completion-observations: en: ae500691f64d1e43 - zh: ef8d84b9907ab67d + zh: 2bfc53b52d210b24 /apps-web-browser-e2e/these-are-host-face-tests: en: 8c406d423c8e2137 zh: a66a144fd146f56c diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index 96b164d56b..a13aae71b9 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -18,7 +18,7 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps 依赖状态的用例使用 Workspace、接纳、附件和模型流屏障,区分可见中间状态与已完成操作。模型选择器的持久化断言等待默认设置保存完成,不以菜单关闭作为完成信号。详情关闭等待框架过渡结束;归档验证为 seed Session 设置显式标题,并跨重载跟踪该身份。[CI 测试可靠性工作流](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state)负责这些同步规则。 -[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器位置的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。 +[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器中的消息归位的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。 显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。 diff --git a/apps/web/tests/idle-submission-handoff.e2e.ts b/apps/web/tests/idle-submission-handoff.e2e.ts index 6053f4311b..25fcebadb6 100644 --- a/apps/web/tests/idle-submission-handoff.e2e.ts +++ b/apps/web/tests/idle-submission-handoff.e2e.ts @@ -17,6 +17,7 @@ import { newEnglishPage } from './support.ts' const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/live-interactions/session.v3.jsonl', import.meta.url)) const SESSION_ID = 'idle-submission-handoff' const TEXT = 'IDLE_SUBMISSION_HANDOFF Keep this message in the conversation.' +const HANDOFF_TIMEOUT_MS = 30_000 interface Delivery { readonly stream: string @@ -91,7 +92,14 @@ class HistoryDeliveryGate { /** Match the scaffold's turn-completion budget for Host frames and browser rendering. */ function poll(read: () => T) { - return expect.poll(read, { timeout: 30_000 }) + return expect.poll(read, { timeout: HANDOFF_TIMEOUT_MS }) +} + +/** The caller's await reports Turn failure; an earlier assertion may bypass that await. */ +function whenTurnSettled(scaffold: Awaited>) { + const settled = scaffold.whenTurnSettled(HANDOFF_TIMEOUT_MS) + void settled.catch(() => undefined) + return settled } async function placement(page: Page, input = TEXT) { @@ -118,10 +126,10 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first const releasePrompt = Promise.withResolvers() const promptBlocked = Promise.withResolvers() const gate = new HistoryDeliveryGate() - let browser: Awaited> | undefined try { await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SESSION_ID) - browser = await chromium.launch() + const browser = await chromium.launch() + onTestFinished(() => browser.close()) const page = await newEnglishPage(browser) const tripwire = watchConsole(page) await gate.install(page) @@ -138,7 +146,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first await route.continue() }, { times: 1 }) const trace: Array<{ phase: string } & Awaited>> = [] - const settled = scaffold.whenTurnSettled() gate.holding = true const input = page.locator('[data-composer-input]').first() await input.fill(TEXT) @@ -146,6 +153,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first await promptBlocked.promise await poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 }) trace.push({ phase: 'optimistic', ...await placement(page) }) + const settled = whenTurnSettled(scaffold) releasePrompt.resolve(undefined) if (order === 'transcript-first') { @@ -192,7 +200,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first } finally { releasePrompt.resolve(undefined) gate.releaseAll() - await browser?.close() } }, ) @@ -200,23 +207,21 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA'])( 'hands off three rapid submissions through all Turns in Host order %s', async (hostOrder) => { const root = await mkdtemp(join(tmpdir(), 'dsh-input-handoff-')) + onTestFinished(() => rm(root, { recursive: true, force: true })) const releases = new Map(['A', 'B', 'C'].map(id => [id, Promise.withResolvers()])) const blocked = new Set() const gate = new HistoryDeliveryGate() - let browser: Awaited> | undefined - let scaffold: Awaited> | undefined - onTestFinished(async () => { - try { await scaffold?.close() } finally { await rm(root, { recursive: true, force: true }) } - }) try { const fixture = await readFile(FIXTURE, 'utf8') const recorded = deriveReplayScript(parseSessionLog(fixture)) expect(recorded).toHaveLength(1) const override = join(root, 'replay.override.json') await writeFile(override, JSON.stringify([recorded[0], recorded[0], recorded[0]])) - scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: override, compareReplaySession: false }) + const scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: override, compareReplaySession: false }) + onTestFinished(() => scaffold.close()) await seedSession(scaffold, fixture, SESSION_ID) - browser = await chromium.launch() + const browser = await chromium.launch() + onTestFinished(() => browser.close()) const page = await newEnglishPage(browser) const tripwire = watchConsole(page) await gate.install(page) @@ -244,7 +249,7 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB } const entered: string[] = [] for (const id of hostOrder) { - const settled = scaffold.whenTurnSettled() + const settled = whenTurnSettled(scaffold) releases.get(id)!.resolve(undefined) await poll(() => gate.turnStartDelivery()).toBeDefined() gate.releaseThrough(gate.turnStartDelivery()!) @@ -276,7 +281,6 @@ it.skipIf(webSnapshotMode() === 'record').each(['ABC', 'ACB', 'BAC', 'BCA', 'CAB } finally { for (const release of releases.values()) release.resolve(undefined) gate.releaseAll() - await browser?.close() } }, )