|
|
@@ -52,6 +52,22 @@ function expectInputOverlap(value: boolean): void {
|
|
|
expect(value).toBe(true)
|
|
|
}
|
|
|
|
|
|
+async function waitForReplyMarker(page: Page, marker: string, timeout = 30000) {
|
|
|
+ return page.waitForFunction(({ marker, first, done }) => {
|
|
|
+ const reply = Array.from(document.querySelectorAll('[data-chat-flow-kind="assistant-step"]')).at(-1)
|
|
|
+ if (!reply) return false
|
|
|
+ const text = document.createTreeWalker(reply, NodeFilter.SHOW_TEXT)
|
|
|
+ let node: Node | null
|
|
|
+ while ((node = text.nextNode())) {
|
|
|
+ if (!node.textContent?.includes(marker) || !node.parentElement?.checkVisibility({ checkVisibilityCSS: true })) continue
|
|
|
+ const composer = Array.from(document.querySelectorAll('[data-composer-input][contenteditable="true"]')).at(-1)
|
|
|
+ const transcript = reply.textContent ?? ''
|
|
|
+ return { atMs: window.performance.now(), focused: document.activeElement === composer, first: transcript.includes(first), done: transcript.includes(done) }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }, { marker, first: FIRST, done: DONE }, { polling: 'raf', timeout })
|
|
|
+}
|
|
|
+
|
|
|
async function watchInputOverlap(composer: Locator): Promise<void> {
|
|
|
await composer.evaluate((element, markers) => {
|
|
|
element.removeAttribute('data-benchmark-input-witness')
|
|
|
@@ -95,6 +111,22 @@ it('accepts recorded hosted paging and Trajectory medians and rejects slower end
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+it('waits for visible marker text in the latest Assistant step', async () => {
|
|
|
+ const browser = await chromium.launch({ headless: true })
|
|
|
+ try {
|
|
|
+ const page = await browser.newPage()
|
|
|
+ await page.setContent(`<div data-chat-flow-kind="assistant-step">${FIRST}</div><div data-chat-flow-kind="assistant-step"><span style="visibility:hidden">${FIRST}</span></div>`)
|
|
|
+ await expect(waitForReplyMarker(page, FIRST, 100)).rejects.toThrow('Timeout')
|
|
|
+ await page.locator('span').evaluate(element => { element.style.visibility = 'visible' })
|
|
|
+ const observation = await waitForReplyMarker(page, FIRST)
|
|
|
+ expect(await observation.jsonValue()).toMatchObject({ first: true, done: false })
|
|
|
+ await observation.dispose()
|
|
|
+ await expect(waitForReplyMarker(page, DONE, 100)).rejects.toThrow('Timeout')
|
|
|
+ } finally {
|
|
|
+ await browser.close()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
it('opens, pages, navigates and streams into a 240-turn browser history', async () => {
|
|
|
if (webSnapshotMode() !== 'replay') throw new Error('browser benchmarks require keyless replay mode')
|
|
|
const samples: { open: number; page: number; trajectory: number; first: number; streamTask: number; streamWall: number; input: number; inputOverlapped: boolean; heapMb: number; nodes: number }[] = []
|
|
|
@@ -157,22 +189,19 @@ it('opens, pages, navigates and streams into a 240-turn browser history', async
|
|
|
await watchInputOverlap(composer)
|
|
|
const started = performance.now()
|
|
|
await page.keyboard.press('Enter')
|
|
|
- const reply = page.locator('[data-chat-flow-kind="assistant-step"]').last()
|
|
|
- await reply.getByText(FIRST, { exact: false }).last().waitFor()
|
|
|
+ const firstMarker = await waitForReplyMarker(page, FIRST)
|
|
|
const first = performance.now() - started
|
|
|
- const firstObservation = await composer.evaluate((element, markers) => {
|
|
|
- const transcript = Array.from(document.querySelectorAll('[data-chat-flow-kind="assistant-step"]')).at(-1)?.textContent ?? ''
|
|
|
- return { atMs: window.performance.now(), focused: document.activeElement === element, first: transcript.includes(markers.first), done: transcript.includes(markers.done) }
|
|
|
- }, { first: FIRST, done: DONE })
|
|
|
// Keep focus across submission; mouse actionability must not delay the input probe.
|
|
|
const input = await measure(page, async () => {
|
|
|
await page.keyboard.type('next synthetic question')
|
|
|
await expect.poll(() => composer.textContent()).toBe('next synthetic question')
|
|
|
})
|
|
|
const inputOverlapped = await composer.getAttribute('data-benchmark-input-overlap') === 'true'
|
|
|
+ const firstObservation = await firstMarker.jsonValue()
|
|
|
+ await firstMarker.dispose()
|
|
|
console.log(JSON.stringify({ benchmark: 'long-session-browser/input', sample, first, input, firstObservation, witness: await composer.getAttribute('data-benchmark-input-witness'), inputTiming: await composer.getAttribute('data-benchmark-input-timing') }))
|
|
|
expectInputOverlap(inputOverlapped)
|
|
|
- await reply.getByText(DONE, { exact: false }).last().waitFor()
|
|
|
+ await (await waitForReplyMarker(page, DONE)).dispose()
|
|
|
const settlement = await settled
|
|
|
if (!settlement.ok) throw settlement.error
|
|
|
await page.waitForFunction(({ selector, expected }) => document.querySelectorAll(selector).length === expected, { selector: TAIL, expected: HISTORY_TURNS + 1 })
|