mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-01 03:20:37 +00:00
Browser tests that don't need their own server now borrow their fixture page from one Vite server per run instead of each starting one. run.mjs (local) and shard.mjs (CI) spawn the fixture daemon as a child. It announces its endpoint and secret as one stdout line; the runner hands them to the test processes through the environment and stops the daemon with an authenticated request when its slice ends, failing the run when that teardown fails or has to be forced. The daemon watches its owner's pid and cleans up after an owner that ended without stopping it; on Windows it is spawned detached so the owner's exit doesn't end it first. A test file run on its own gets a daemon child of its own. fixture-borrow.ts registers namespaced routes, fixture-exemptions.ts lists the files that keep their own servers (stateful SSE middleware, custom transforms, Electron hosts), and fixture-wiring.test.ts enforces borrow-or-exempt. AGENTS.md sanctions the daemon's single-writer optimizer cache, and dev-docs/BROWSER_TESTING.md documents the commands, the daemon model, logs and limits. Twenty owned fixtures also dispose their cache when createServer() rejects. Measured locally, 20 representative files run one at a time took 797 s against 1,091 s on owned servers (about 27 % less); the CI shards' test streams show no consistent change. Validation: the daemon, run, borrow and wiring tests pass (11/11), including split and noisy handshakes, a start that never announces, pipelined shutdown requests, a forced stop seen through by a minimal runner, and protected cleanup after a killed owner; the full suite ran through npm run test:browser on Windows, and the four CI shards passed.
81 lines
4.2 KiB
TypeScript
81 lines
4.2 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { after, before, test } from "node:test"
|
|
import { fileURLToPath } from "node:url"
|
|
import { borrowFixture } from "./fixture-borrow"
|
|
import { chromium, webkit, type Browser } from "playwright"
|
|
|
|
let browser: Browser, url: string
|
|
before(async () => {
|
|
const fixture = await borrowFixture({
|
|
name: "session-delete",
|
|
routes: [
|
|
{ path: "/fixture", html: "<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"></head><body><div id=\"root\"></div><script type=\"module\" src=\"/tests/browser/fixtures/header-windows.tsx\"></script></body></html>" },
|
|
],
|
|
})
|
|
try {
|
|
url = fixture.url
|
|
browser = await (process.env.CODENOMAD_TEST_BROWSER === "webkit" ? webkit : chromium)
|
|
.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
|
|
} catch (error) { throw error }
|
|
})
|
|
after(async () => { try { await browser?.close() } finally { } })
|
|
|
|
for (const { touch, width, name } of [
|
|
{ touch: true, width: 390, name: "phone tap" },
|
|
{ touch: false, width: 390, name: "narrow window mouse" },
|
|
{ touch: false, width: 1400, name: "desktop mouse" },
|
|
]) {
|
|
test(`session overflow Delete requires confirmation then removes the session (${name})`, async () => {
|
|
const context = await browser.newContext({ viewport: { width, height: 850 },
|
|
hasTouch: touch, isMobile: touch, locale: "en-US" })
|
|
const page = await context.newPage()
|
|
const errors: string[] = []
|
|
page.on("pageerror", error => errors.push(error.message))
|
|
// The fixture owns its session client; no real event stream may reconcile it.
|
|
await page.addInitScript(() => { window.EventSource = class extends EventTarget { close() {} } as any })
|
|
await page.route("**/api/**", route => route.fulfill({ json: {} }))
|
|
try {
|
|
await page.goto(url)
|
|
await page.waitForFunction(() => Boolean((window as any).fixture))
|
|
const sessionId = "a-long-session-title-that-needs-the-action-overflow-menu"
|
|
await page.evaluate(id => {
|
|
;(window as any).fixture.addSession(id)
|
|
;(window as any).fixture.selectSession(id)
|
|
}, sessionId)
|
|
const activate = async (locator: ReturnType<typeof page.locator>) => touch ? locator.tap() : locator.click()
|
|
const leftToggle = page.locator(".shell-panel-toggle--left button")
|
|
if (await leftToggle.getAttribute("aria-expanded") === "false") await activate(leftToggle)
|
|
const row = page.locator(`.session-item-base[data-session-id="${sessionId}"]`)
|
|
await row.waitFor()
|
|
const openDelete = async () => {
|
|
if (!touch) await row.hover()
|
|
await activate(row.locator(".action-overflow-trigger"))
|
|
await activate(page.getByRole("menuitem", { name: "Delete session", exact: true }))
|
|
await page.getByRole("dialog", { name: "Delete session", exact: true }).waitFor({ timeout: 5000 })
|
|
assert.deepEqual(await page.evaluate(() => (window as any).fixture.deletedSessions), [], "opening the dialog is not consent")
|
|
}
|
|
|
|
await openDelete()
|
|
await activate(page.getByRole("dialog").getByRole("button", { name: "Cancel", exact: true }))
|
|
await page.getByRole("dialog").waitFor({ state: "detached" })
|
|
assert.deepEqual(await page.evaluate(() => (window as any).fixture.deletedSessions), [])
|
|
assert.equal(await row.count(), 1)
|
|
|
|
if (width === 390) {
|
|
// Ordinary outside gestures still close the floating panel.
|
|
const outside = (await page.locator("#outside").boundingBox())!
|
|
if (touch) await page.touchscreen.tap(outside.x + 5, outside.y + 5)
|
|
else await page.mouse.click(outside.x + 5, outside.y + 5)
|
|
await page.waitForFunction(() => document.querySelector('.shell-panel-toggle--left button')?.getAttribute("aria-expanded") === "false")
|
|
await activate(leftToggle)
|
|
}
|
|
|
|
await openDelete()
|
|
await activate(page.getByRole("dialog").getByRole("button", { name: "Delete", exact: true }))
|
|
await page.waitForFunction(() => (window as any).fixture.deletedSessions.length === 1)
|
|
assert.deepEqual(await page.evaluate(() => (window as any).fixture.deletedSessions), [sessionId])
|
|
await row.waitFor({ state: "detached" })
|
|
assert.deepEqual(errors, [])
|
|
} finally { await context.close() }
|
|
})
|
|
}
|