Explorar el Código

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.
Yichen Jiang hace 3 semanas
padre
commit
4f808771ce

+ 3 - 3
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)

+ 1 - 1
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 => (

+ 4 - 4
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)
 

+ 1 - 1
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.

+ 1 - 1
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')

+ 1 - 1
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]')

+ 2 - 2
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) => {

+ 3 - 0
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)

+ 3 - 8
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

+ 2 - 1
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"

+ 6 - 6
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)
 

+ 1 - 1
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]')