93 lines
4.2 KiB
JavaScript
93 lines
4.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import { after, before, test } from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { chromium } from 'playwright'
|
|
import { createServer } from 'vite'
|
|
|
|
const app = fileURLToPath(new URL('../', import.meta.url))
|
|
const scratch = await mkdtemp(path.join(tmpdir(), 'hermes-task-scroll-'))
|
|
const output = process.env.TASK_SCROLL_OUTPUT ?? scratch
|
|
let server
|
|
let browser
|
|
let url
|
|
|
|
before(async () => {
|
|
await mkdir(output, { recursive: true })
|
|
console.log(`Task-scroll evidence: ${output}`)
|
|
// The runner loader avoids writing config bundles to shared node_modules.
|
|
globalThis.__dirname = app
|
|
server = await createServer({
|
|
root: app,
|
|
configFile: path.join(app, 'vite.config.ts'),
|
|
configLoader: 'runner',
|
|
cacheDir: path.join(scratch, 'node_modules/.vite'),
|
|
server: { host: '127.0.0.1', port: 18120, strictPort: true },
|
|
optimizeDeps: { entries: ['scripts/fixtures/tasks-scroll.html'] }
|
|
})
|
|
await server.listen()
|
|
url = 'http://127.0.0.1:18120/scripts/fixtures/tasks-scroll.html'
|
|
browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] })
|
|
})
|
|
after(async () => {
|
|
await browser?.close()
|
|
await server?.close()
|
|
if (output !== scratch) await rm(scratch, { recursive: true, force: true })
|
|
})
|
|
|
|
for (const count of [14, 20]) {
|
|
test(`${count} tasks: wheel reveals the final row above the composer`, async () => {
|
|
const page = await browser.newPage({ viewport: { width: 1200, height: 800 }, reducedMotion: 'reduce' })
|
|
const errors = []
|
|
page.on('pageerror', error => errors.push(error.message))
|
|
try {
|
|
await page.goto(`${url}?tasks=${count}`, { timeout: 120_000 })
|
|
const stack = page.locator('[data-slot="composer-status-stack"]')
|
|
const last = page.getByText(`Phase ${count}:`, { exact: false })
|
|
await last.waitFor()
|
|
await page.evaluate(() => document.fonts.ready)
|
|
const measure = () => stack.evaluate(el => {
|
|
const rect = el.getBoundingClientRect()
|
|
return { height: el.clientHeight, scrollHeight: el.scrollHeight, top: el.scrollTop,
|
|
bounds: { top: rect.top, bottom: rect.bottom }, cardHeight: el.firstElementChild.getBoundingClientRect().height }
|
|
})
|
|
const before = await measure()
|
|
await stack.hover()
|
|
await page.mouse.wheel(0, 10000)
|
|
// Wheel is asynchronous; wait for animation frames to deliver it, without
|
|
// scrollIntoView/locator.click silently repairing the broken layout.
|
|
await page.waitForTimeout(400)
|
|
const after = await measure()
|
|
const tail = await last.evaluate(el => {
|
|
const r = el.getBoundingClientRect()
|
|
return { top: r.top, bottom: r.bottom,
|
|
hit: el.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)) }
|
|
})
|
|
const composer = await page.locator('[data-slot="fixture-composer"]').boundingBox()
|
|
const receipt = { count, before, after, tail, composer, errors }
|
|
console.log(JSON.stringify(receipt))
|
|
await writeFile(path.join(output, `tasks-${count}.json`), JSON.stringify(receipt, null, 2))
|
|
await page.screenshot({ path: path.join(output, `tasks-${count}.png`) })
|
|
assert.deepEqual(errors, [])
|
|
assert.ok(after.scrollHeight > after.height && after.top > before.top, 'Long task list must scroll')
|
|
assert.ok(tail.top >= after.bounds.top && tail.bottom <= after.bounds.bottom, 'Final row must be fully inside viewport')
|
|
assert.ok(tail.bottom <= composer.y && tail.hit, 'Final row must not be occluded by composer or card')
|
|
// Collapse/expand must recover the same bounded, scrollable list.
|
|
await page.mouse.wheel(0, -10000)
|
|
await page.waitForTimeout(400)
|
|
const header = stack.getByRole('button').first()
|
|
await header.click()
|
|
assert.ok((await measure()).height < before.height, 'Collapsed list must release its height')
|
|
await header.click()
|
|
await stack.hover()
|
|
await page.mouse.wheel(0, 10000)
|
|
await page.waitForTimeout(400)
|
|
assert.ok((await measure()).top > 0, 'Re-expanded list must remain scrollable')
|
|
} finally {
|
|
await page.close()
|
|
}
|
|
})
|
|
}
|