test(web): pin the source list's scroll geometry in the assembled browser
jsdom resolves no CSS Modules layout and reports scrollHeight === clientHeight for every element, so the unit test cannot witness the scroll the .sources max-height produces. Drive it in the real browser instead. The web-search-round search double now returns 12 provider results with a title, snippet, and date each. That pins the seam's cap end to end for the first time in a real composition — searchMaxResults keeps 8, the model-visible text carries the kept titles and none of the dropped URLs, meta.truncated is true — and 8 such rows overflow the 320px container. A case after the aria golden expands the row and asserts 8 <li>, no button, the truncation indicator, and computed max-height/overflow-y with scrollHeight > clientHeight. The recorded stream and the aria golden are unchanged: replay is a positional cursor over the fixture's chunks and the search double is a separate local endpoint, while the card is collapsed at capture time.
This commit is contained in:
@@ -26,7 +26,37 @@ const QUERY = 'DeepSeek Harness snapshot search'
|
||||
const PROMPT = `Use web_search to search exactly "${QUERY}". Then reply exactly SEARCH_DONE and stop.`
|
||||
const SEARCH_CREDENTIAL_REF = credentialRef('DSH_WEB_SEARCH_E2E_KEY')
|
||||
const SEARCH_CREDENTIAL = 'snapshot-search-key'
|
||||
const RESULT_URL = 'https://docs.example.test/search'
|
||||
|
||||
/**
|
||||
* Provider results the double returns, exceeding the shipped `searchMaxResults`
|
||||
* so the seam's cap and the card's scroll container are both exercised. Each row
|
||||
* carries a title, a snippet, and a date, so 8 kept rows exceed the `.sources`
|
||||
* 320px max-height.
|
||||
*/
|
||||
const PROVIDER_RESULT_COUNT = 12
|
||||
|
||||
/** One provider result's URL, by 1-based provider order. */
|
||||
function resultUrl(ordinal: number): string {
|
||||
return `https://docs.example.test/search/${ordinal}`
|
||||
}
|
||||
|
||||
/** One provider result's title, by 1-based provider order. */
|
||||
function resultTitle(ordinal: number): string {
|
||||
return `Snapshot Search Result ${ordinal}`
|
||||
}
|
||||
|
||||
/** One provider result's citation excerpt, by 1-based provider order. */
|
||||
function resultSnippet(ordinal: number): string {
|
||||
return `Snapshot search excerpt ${ordinal}: the harness replays this source list from a local endpoint.`
|
||||
}
|
||||
|
||||
/** One provider result's `page_age`, by 1-based provider order (July 2026 days 01..12). */
|
||||
function resultPageAge(ordinal: number): string {
|
||||
return `2026-07-${String(ordinal).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** The 1-based provider ordinals, in provider order. */
|
||||
const RESULT_ORDINALS = Array.from({ length: PROVIDER_RESULT_COUNT }, (_value, index) => index + 1)
|
||||
|
||||
interface CapturedSearchRequest {
|
||||
path: string
|
||||
@@ -51,21 +81,21 @@ async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ s
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Found one source.',
|
||||
citations: [{
|
||||
text: `Found ${PROVIDER_RESULT_COUNT} sources.`,
|
||||
citations: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result_location',
|
||||
url: RESULT_URL,
|
||||
cited_text: 'Snapshot search excerpt.',
|
||||
}],
|
||||
url: resultUrl(ordinal),
|
||||
cited_text: resultSnippet(ordinal),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'web_search_tool_result',
|
||||
content: [{
|
||||
content: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result',
|
||||
url: RESULT_URL,
|
||||
title: 'Snapshot Search Result',
|
||||
page_age: '2026-07-31',
|
||||
}],
|
||||
url: resultUrl(ordinal),
|
||||
title: resultTitle(ordinal),
|
||||
page_age: resultPageAge(ordinal),
|
||||
})),
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -142,7 +172,7 @@ describe('web e2e: shipped default web search', () => {
|
||||
if (MODE === 'record') await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('uses the real provider and persists the structured result', () => {
|
||||
it.skipIf(MODE === 'record')('uses the real provider and persists the capped structured result', () => {
|
||||
expect(searchRequests).toHaveLength(1)
|
||||
expect(searchRequests[0]).toMatchObject({
|
||||
path: '/messages',
|
||||
@@ -178,16 +208,27 @@ describe('web e2e: shipped default web search', () => {
|
||||
if (searchResult === undefined) throw new Error('web_search produced no durable result')
|
||||
const content = searchResult.data.message.content[0]
|
||||
expect(content.isError).toBe(false)
|
||||
expect(content.content.filter(block => block.type === 'text').map(block => block.text).join(''))
|
||||
.toContain(`[Snapshot Search Result](${RESULT_URL})`)
|
||||
const rendered = content.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||
// The seam caps the provider's list at the shipped searchMaxResults before
|
||||
// the tool renders it, so the kept prefix is model-visible and the dropped
|
||||
// suffix is not.
|
||||
for (const ordinal of RESULT_ORDINALS.slice(0, WEB_SEARCH_MAX_RESULTS)) {
|
||||
expect(rendered).toContain(`[${resultTitle(ordinal)}](${resultUrl(ordinal)})`)
|
||||
}
|
||||
for (const ordinal of RESULT_ORDINALS.slice(WEB_SEARCH_MAX_RESULTS)) {
|
||||
expect(rendered).not.toContain(resultUrl(ordinal))
|
||||
}
|
||||
expect(rendered).toContain(
|
||||
`(Showing the first ${WEB_SEARCH_MAX_RESULTS} sources. Refine the query for more.)`,
|
||||
)
|
||||
expect(searchResult.data.meta).toMatchObject({
|
||||
sources: [{
|
||||
url: RESULT_URL,
|
||||
title: 'Snapshot Search Result',
|
||||
snippet: 'Snapshot search excerpt.',
|
||||
publishedAt: '2026-07-31',
|
||||
}],
|
||||
truncated: false,
|
||||
sources: RESULT_ORDINALS.slice(0, WEB_SEARCH_MAX_RESULTS).map(ordinal => ({
|
||||
url: resultUrl(ordinal),
|
||||
title: resultTitle(ordinal),
|
||||
snippet: resultSnippet(ordinal),
|
||||
publishedAt: resultPageAge(ordinal),
|
||||
})),
|
||||
truncated: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -200,6 +241,36 @@ describe('web e2e: shipped default web search', () => {
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('scrolls the capped source list inside the fixed-height container', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-sources-scroll'))
|
||||
const row = page.locator('[data-tool="web_search"] [data-expandable]').first()
|
||||
await row.click()
|
||||
await expect.poll(() => row.getAttribute('aria-expanded'), { timeout: 5_000 }).toBe('true')
|
||||
|
||||
const card = page.locator('[data-web="search"]')
|
||||
const sources = card.locator('ol')
|
||||
await sources.waitFor({ timeout: 10_000 })
|
||||
// The card draws exactly the sources the model saw: the seam's cap, not the
|
||||
// provider's list length.
|
||||
expect(await sources.locator('li').count()).toBe(WEB_SEARCH_MAX_RESULTS)
|
||||
// The list is complete in the DOM, so the card carries no expand control.
|
||||
expect(await card.locator('button').count()).toBe(0)
|
||||
expect(await card.getByText('来源列表已截断').isVisible()).toBe(true)
|
||||
|
||||
const geometry = await sources.evaluate((element) => {
|
||||
const computed = getComputedStyle(element)
|
||||
return {
|
||||
maxHeight: computed.maxHeight,
|
||||
overflowY: computed.overflowY,
|
||||
scrollHeight: element.scrollHeight,
|
||||
clientHeight: element.clientHeight,
|
||||
}
|
||||
})
|
||||
expect(geometry.maxHeight).toBe('320px')
|
||||
expect(geometry.overflowY).toBe('auto')
|
||||
expect(geometry.scrollHeight).toBeGreaterThan(geometry.clientHeight)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('stayed clean and kept the exact fixture inventory', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user