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.
This commit is contained in:
Yichen Jiang
2026-08-20 18:50:59 +08:00
parent e920185700
commit 4f808771ce
12 changed files with 28 additions and 29 deletions
@@ -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)
+1 -1
View File
@@ -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 => (
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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')
@@ -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]')
+2 -2
View File
@@ -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) => {
+3
View File
@@ -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)
@@ -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
@@ -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"
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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]')