From 4f808771ceccc00124e585b3b4bc540e023f2ada Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 20 Aug 2026 18:50:59 +0800 Subject: [PATCH] test(web): finish the e2e migration to the composer surface textContent/data-placeholder probes replace inputValue/placeholder reads, evaluate-string selectors move to the composer anchor, queued fills wait out the submit round-trip's read-only span, and the refreshed aria goldens drop the hover tooltip the old interaction order happened to capture. --- apps/web/tests/chat-continuous-conversation.e2e.ts | 6 +++--- apps/web/tests/chat-long-interactions.e2e.ts | 2 +- apps/web/tests/complex-history.perf.ts | 8 ++++---- apps/web/tests/default-model.e2e.ts | 2 +- apps/web/tests/goal-bar.e2e.ts | 2 +- apps/web/tests/goal-command-presentation.e2e.ts | 2 +- apps/web/tests/lifecycle-chrome.e2e.ts | 4 ++-- apps/web/tests/queue-actions.e2e.ts | 3 +++ .../composer-draft-scroll/geometry.expected.md | 11 +++-------- .../snapshots/lifecycle-chrome/hero.expected.md | 3 ++- apps/web/tests/startup-auto-selection.e2e.ts | 12 ++++++------ apps/web/tests/steering.e2e.ts | 2 +- 12 files changed, 28 insertions(+), 29 deletions(-) diff --git a/apps/web/tests/chat-continuous-conversation.e2e.ts b/apps/web/tests/chat-continuous-conversation.e2e.ts index f6e291df8b..8f99b5a8a1 100644 --- a/apps/web/tests/chat-continuous-conversation.e2e.ts +++ b/apps/web/tests/chat-continuous-conversation.e2e.ts @@ -222,10 +222,10 @@ describe('web e2e: continuous conversation grown through the composer', () => { for (const spec of specs) { const eventStart = sessionEvents.length - expect(await composer.inputValue()).toBe('') + expect(await composer.textContent()).toBe('') expect(await composer.isEnabled()).toBe(true) await composer.fill(spec.prompt) - expect(await composer.inputValue()).toBe(spec.prompt) + expect(await composer.textContent()).toBe(spec.prompt) const settled = scaffold.whenTurnSettled(60_000) await page.getByRole('button', { name: 'Send message', exact: true }).click() @@ -257,7 +257,7 @@ describe('web e2e: continuous conversation grown through the composer', () => { await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 15_000 }).toBe(0) await page.getByText(spec.doneMarker, { exact: false }).last().waitFor({ timeout: 15_000 }) - await expect.poll(() => composer.inputValue(), { timeout: 10_000 }).toBe('') + await expect.poll(() => composer.textContent(), { timeout: 10_000 }).toBe('') await expect.poll(() => composer.isEnabled(), { timeout: 10_000 }).toBe(true) const turnEvents = sessionEvents.slice(eventStart) diff --git a/apps/web/tests/chat-long-interactions.e2e.ts b/apps/web/tests/chat-long-interactions.e2e.ts index 5542dda8a6..b3835c7551 100644 --- a/apps/web/tests/chat-long-interactions.e2e.ts +++ b/apps/web/tests/chat-long-interactions.e2e.ts @@ -279,7 +279,7 @@ describe('web e2e: long Chat interaction contract', () => { expect(await settled).toBe(child.session.id) await page.getByText(CONTINUE_DONE, { exact: false }).last().waitFor({ timeout: 15_000 }) await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 15_000 }).toBe(0) - expect(await composer.inputValue()).toBe('') + expect(await composer.textContent()).toBe('') expect(await composer.isEnabled()).toBe(true) expect(source.session.events.some(event => carries(event, CONTINUE_PROMPT))).toBe(false) expect(child.session.events.filter(event => ( diff --git a/apps/web/tests/complex-history.perf.ts b/apps/web/tests/complex-history.perf.ts index c9399f0ab1..b0b4bcd7ab 100644 --- a/apps/web/tests/complex-history.perf.ts +++ b/apps/web/tests/complex-history.perf.ts @@ -947,8 +947,8 @@ async function continueConversation( const spec = options.turnSpec(index) const composerFill = await measure(cdp, async () => { await composer.fill(spec.prompt) - await expect.poll(() => composer.inputValue()).toBe(spec.prompt) - return (await composer.inputValue()).length + await expect.poll(() => composer.textContent()).toBe(spec.prompt) + return (await composer.textContent()).length }) expect(composerFill.value).toBe(spec.prompt.length) @@ -1067,8 +1067,8 @@ async function measurePostSoakUserRender( const composer = world.page.locator('[data-composer-input][contenteditable="true"]').last() const composerFill = await measure(cdp, async () => { await composer.fill(spec.prompt) - await expect.poll(() => composer.inputValue()).toBe(spec.prompt) - return (await composer.inputValue()).length + await expect.poll(() => composer.textContent()).toBe(spec.prompt) + return (await composer.textContent()).length }) expect(composerFill.value).toBe(spec.prompt.length) diff --git a/apps/web/tests/default-model.e2e.ts b/apps/web/tests/default-model.e2e.ts index 3433d4e48c..4bd5dd2ef4 100644 --- a/apps/web/tests/default-model.e2e.ts +++ b/apps/web/tests/default-model.e2e.ts @@ -140,7 +140,7 @@ describe('web e2e: the composer model switch is the default for later sessions', await scaffold.ctx.settings.replace(settingsNamespace('llm-pi-ai'), { providers: {} }) await expect.poll(async () => box.isEnabled(), { timeout: 15_000 }).toBe(false) - expect(await box.getAttribute('placeholder')).toBe('当前模型不可用,请先选择模型') + expect(await box.getAttribute('data-placeholder')).toBe('当前模型不可用,请先选择模型') // The block is an affordance; the refusal is the Host's. A client that // never disabled anything still cannot start a turn on a dead route. diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index 00260a79ee..75313eeb32 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -43,7 +43,7 @@ describe('web e2e: goal bar clear convergence', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear')) // Startup reuses the fixture workspace's blank session, keeping this // command independent of alpha's running replay and pending question. - const input = page.getByPlaceholder('Describe what you want to build') + const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build"]') await input.waitFor({ timeout: 10_000 }) await input.fill('/goal guard rapid clear clicks') await input.press('Enter') diff --git a/apps/web/tests/goal-command-presentation.e2e.ts b/apps/web/tests/goal-command-presentation.e2e.ts index b09df1c242..037ba0ffb4 100644 --- a/apps/web/tests/goal-command-presentation.e2e.ts +++ b/apps/web/tests/goal-command-presentation.e2e.ts @@ -51,7 +51,7 @@ describe('web e2e: /goal human transcript presentation', () => { const input = page.locator('[data-composer-input]').first() await input.fill('/goal') await input.press('Enter') - await expect.poll(() => input.inputValue()).toBe('/goal ') + await expect.poll(() => input.textContent()).toBe('/goal ') await input.press('Enter') const commandInput = page.locator('[data-command-input]') diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 8abc101be2..b249aa073f 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -108,14 +108,14 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () const menu = activePage.getByRole('listbox', { name: 'Trigger suggestions' }) await menu.waitFor({ timeout: 10_000 }) await menu.getByRole('option', { name: 'plan Enter or leave plan mode' }).click() - await expect.poll(() => input.inputValue()).toBe('/plan ') + await expect.poll(() => input.textContent()).toBe('/plan ') await input.press('Enter') const planButton = activePage.getByRole('button', { name: 'Plan mode on, press to turn off' }) await planButton.waitFor({ timeout: 10_000 }) // The golden encodes an empty composer, and the button arriving does not // mean the submitted text is gone yet: under load the capture can catch // a textbox still holding `/plan`. - await expect.poll(() => input.inputValue(), { timeout: 10_000 }).toBe('') + await expect.poll(() => input.textContent(), { timeout: 10_000 }).toBe('') const planSnapshot = await captureStableAria(activePage, '[class*="frame"]', activeScaffold.workspaceCwd) await compareOrRefreshGolden(PLAN_ACTIVE_EXPECTED, planSnapshot, MODE) const planStyle = await planButton.evaluate((element) => { diff --git a/apps/web/tests/queue-actions.e2e.ts b/apps/web/tests/queue-actions.e2e.ts index eff5c784f4..0f577566fe 100644 --- a/apps/web/tests/queue-actions.e2e.ts +++ b/apps/web/tests/queue-actions.e2e.ts @@ -93,6 +93,8 @@ describe('web e2e: queue row actions', () => { await expect.poll(() => existsSync(readyFile), { timeout: 15_000 }).toBe(true) for (const text of [REMOVE, EDIT]) { + // A just-submitted composer is read-only for the prompt round-trip. + await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor({ timeout: 10_000 }) await input.fill(text) await input.press('Enter') } @@ -198,6 +200,7 @@ describe('web e2e: queue row actions', () => { const input = page.locator('[data-composer-input]').first() const settled = scaffold.whenTurnSettled() + await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor({ timeout: 10_000 }) await input.fill('/goal Keep the composer context panels aligned') await input.press('Enter') await expect.poll(() => existsSync(readyFile), { timeout: 15_000 }).toBe(true) diff --git a/apps/web/tests/snapshots/composer-draft-scroll/geometry.expected.md b/apps/web/tests/snapshots/composer-draft-scroll/geometry.expected.md index ef08cdfd94..58644e22a8 100644 --- a/apps/web/tests/snapshots/composer-draft-scroll/geometry.expected.md +++ b/apps/web/tests/snapshots/composer-draft-scroll/geometry.expected.md @@ -1,31 +1,26 @@ -# Composer draft scrolling (14-line cap, two text layers, one scrollport) +# Composer draft scrolling (14-line cap, one editable surface, one scrollport) ## At the start of the draft - draft overflows the capped box: true - visible lines: 14 -- the textarea holds no scroll offset of its own: true -- all three layers wrap at one width: true +- the surface holds no scroll offset of its own: true - scroll offset: 0px -- caret and glyphs stay level when the offset changes: true - first draft line is on screen: true - last draft line is on screen: false ## Scrolled to the end of the draft - offset moved: true -- caret sits on its own glyphs: true -- caret and glyphs stay level when the offset changes: true +- the surface holds no scroll offset of its own: true - first draft line has scrolled out above: true - last draft line is on screen: true ## Draft ending in a newline, scrolled to the end -- caret sits on its own glyphs: true - the draft's own last line is on screen: true ## Right after pasting a long block at the end - the composer scrolled to the caret it left: true -- caret and glyphs stay level when the offset changes: true - the pasted block's last line is on screen: true diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 64ff6ae8f0..149eb4734d 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -29,7 +29,8 @@ - img - text: Standard mode - img -- textbox "Describe what you want to build" +- textbox "Describe what you want to build": + - paragraph - button "Commands": - img - tooltip "Commands" diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index a4922c11aa..77b8de8c41 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -81,7 +81,7 @@ describe('web e2e: startup auto-selection', () => { workspaceChip: document.querySelector('[aria-label="Choose workspace"]'), scrollBody: document.querySelector('[data-conversation-scroll]'), composerSeat: document.querySelector('[data-composer-seat]'), - textarea: document.querySelector('textarea'), + composer: document.querySelector('[data-composer-input]'), } if (Object.values(refs).some(node => node === null)) throw new Error('incomplete initial Hero tree') ;(window as unknown as { __heroTree: typeof refs }).__heroTree = refs @@ -99,8 +99,8 @@ describe('web e2e: startup auto-selection', () => { workspaceChip: document.querySelector('[aria-label="Choose workspace"]') === before.workspaceChip, scrollBody: document.querySelector('[data-conversation-scroll]') === before.scrollBody, composerSeat: document.querySelector('[data-composer-seat]') === before.composerSeat, - textarea: document.querySelector('textarea') === before.textarea, - textareaEnabled: !(document.querySelector('textarea') as HTMLTextAreaElement).disabled, + composer: document.querySelector('[data-composer-input]') === before.composer, + composerEnabled: document.querySelector('[data-composer-input]')?.getAttribute('aria-disabled') !== 'true', } })).toEqual({ phase: 'hero', @@ -108,8 +108,8 @@ describe('web e2e: startup auto-selection', () => { workspaceChip: true, scrollBody: true, composerSeat: true, - textarea: true, - textareaEnabled: true, + composer: true, + composerEnabled: true, }) expect(tripwire.pageErrors).toEqual([]) }, 120_000) @@ -156,7 +156,7 @@ describe('web e2e: startup auto-selection', () => { expect(await page.locator('[data-composer-input]').first().isVisible()).toBe(true) releaseHistory() - await page.locator('textarea:enabled[placeholder="Describe what you want to build"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build"]') .waitFor({ timeout: 15_000 }) acknowledgeReloadConnectionLoss(tripwire, warningsBefore) diff --git a/apps/web/tests/steering.e2e.ts b/apps/web/tests/steering.e2e.ts index 0219370f02..c6f1df177e 100644 --- a/apps/web/tests/steering.e2e.ts +++ b/apps/web/tests/steering.e2e.ts @@ -209,7 +209,7 @@ describe('web e2e: composer shortcut steers directly', () => { await input.fill(STEER) await input.press('Meta+Enter') - await expect.poll(() => input.inputValue(), { timeout: 5_000 }).toBe('') + await expect.poll(() => input.textContent(), { timeout: 5_000 }).toBe('') expect(await page.locator('[data-queue-dock]').count()).toBe(0) const composer = page.locator('[data-question-key]')