Merge remote-tracking branch 'origin/master' into worktree/command-row-copy

This commit is contained in:
creatixchu
2026-07-30 17:34:38 +08:00
17 changed files with 135 additions and 57 deletions

View File

@@ -118,14 +118,14 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1)
}, 60_000)
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel open', async () => {
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel closed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-code-mode-details'))
const nest = page.locator('[data-subcalls]').first()
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
await nest.locator('[data-sample="bash-global"]').first().click()
// Tool rows do not drive layout geometry; the Session's default panel stays open.
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
// Tool rows do not drive layout geometry; the Session's default panel stays closed.
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
})
it.skipIf(MODE === 'record')('matches the conversation aria golden with stable anchors', async () => {

View File

@@ -1,12 +1,14 @@
// Keyless browser regression for the details column's Session ownership.
// The shipped composition retains geometry through unselected states and closes it only when a different Session takes ownership.
// Keyless browser regression for the details column's default visibility and Session ownership.
// The shipped composition starts closed after selection and reload, retains an explicitly opened width through
// unselected states, and closes it only when a different Session takes ownership.
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
acknowledgeReloadConnectionLoss, fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole,
webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -52,7 +54,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
await scaffold?.close()
})
it('retains geometry through hero and closes it for a different Session', async () => {
it('starts and reloads closed, then stays closed across Session ownership changes', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle'))
const settled = scaffold.whenTurnSettled()
const input = page.locator('textarea').first()
@@ -61,10 +63,18 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
await settled
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
await page.getByRole('button', { name: 'New session', exact: true }).last().click()
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await appFrame(page).waitFor({ timeout: 30_000 })
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click()
await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
@@ -72,8 +82,8 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first()
await original.click()
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
const ungrouped = page.getByText('Ungrouped', { exact: true })
const ungroupedRow = ungrouped.locator('..').locator('..')

View File

@@ -173,24 +173,24 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await expect.poll(() => page.locator('tr[data-timeline-focus]').count(), { timeout: 10_000 }).toBe(0)
}, 60_000)
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column open', async () => {
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
await page.getByRole('tab', { name: 'Chat' }).click()
const bashRow = page.locator('[data-sample="bash-global"]').first()
await bashRow.waitFor({ timeout: 15_000 })
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
await bashRow.click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
// The card's own controls are outside the summary row and must not open
// details either — the terminal card is read in place.
await page.locator('[data-sample="bash-global"] ~ [data-terminal] [class*="_copyButton_"]').first().click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
// Read summaries are host-open file links; they also must not open details.
const fileLink = page.locator('[data-variant="read"] button').first()
await fileLink.waitFor({ timeout: 10_000 })
await fileLink.click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
}, 60_000)
it.skipIf(MODE === 'record')('renders the bash row as a terminal card in the real browser', async () => {

View File

@@ -145,9 +145,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
const fileLink = page.locator('[data-variant="read"] button').first()
await fileLink.waitFor({ timeout: 10_000 })
const frame = page.locator('[style*="grid-template-columns"]').first()
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
await fileLink.click()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
// Path label survives from the recorded args (a.txt).
await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0)
})

View File

@@ -454,7 +454,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
await screen(page, '07-back-to-chat')
})
it('5 bash differential rendering: tool row click leaves the default details column open', async () => {
it('5 bash differential rendering: tool row click leaves the default details column closed', async () => {
onTestFailed(() => saveFailureShot(page, 'w5-tool-details'))
const input = page.locator('textarea').first()
await input.fill('请用 bash 工具运行命令 echo w5marker 然后告诉我结果')
@@ -466,11 +466,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
const toolRow = page.locator('[data-sample="bash-global"]')
await toolRow.waitFor({ timeout: 120_000 })
await screen(page, '08-bash-round')
expect(await detailsTrack(page)).toBe(360)
expect(await detailsTrack(page)).toBe(0)
await toolRow.click()
// Tool rows no longer drive layout.openDetails; the default column stays open.
expect(await detailsTrack(page)).toBe(360)
await screen(page, '09-details-open')
// Tool rows no longer drive layout.openDetails; the default column stays closed.
expect(await detailsTrack(page)).toBe(0)
await screen(page, '09-details-closed')
}, 150_000)
it('6 sidebar drag widens the column and resets across reload', async () => {