CodeNomad/packages/ui/tests/browser/plugin-controls.test.ts
heunghingwan c977ec67cb
test(ui): shard browser CI four ways and give every fixture an owned server (#1139)
The `tests` job spent about two of its 2.2 hours in `test:browser`: more
than 140 fixture files, each starting its own Vite dev server, run one at
a time, many without the protected-teardown wiring or sharing the
checkout's default optimizer cache.

Every Vite-hosting browser test now uses `createFixtureCache` and
`createFixtureShutdown`, and a static `fixture-wiring` test fails when a
file hosts a Vite server without that wiring, launches Chromium without
the navigation budget, or imports a helper through a specifier that does
not resolve. `armFixtureNavigationBudget` lets cold page loads take up
to 120 s while action timeouts keep Playwright's defaults.

Browser rendering regressions move out of `tests` into a four-way
`tests-browser` matrix: each shard runs an interleaved slice serially
(`tests/browser/shard.mjs`), and `build` waits for every shard. Locally,
`npm run test:browser` still runs one file at a time, since the Git
fixtures rebuild one shared Monaco asset directory. The leftover
`docs/superpowers/` spec is removed.

Validation: the four shards took 26, 32, 37 and 25 minutes and `tests`
12 minutes in the last completed CI run, against about 2 h 14 for
`tests` with the browser step; the wiring, navigation-budget and
build-order tests pass, and a 20-file slice of heavy fixtures passed
391/391.
2026-10-25 20:38:46 +01:00

364 lines
21 KiB
TypeScript

import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import { mkdir } from "node:fs/promises"
import { join } from "node:path"
import { chromium, type Browser } from "playwright"
import { createFixtureCache } from "./fixture-cache"
import { createFixtureShutdown } from "./fixture-shutdown"
import { armFixtureNavigationBudget } from "./fixture-navigation-budget"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
let server: ViteDevServer, browser: Browser, url: string
before(async () => {
const cache = await createFixtureCache(), shutdown = createFixtureShutdown(cache)
server = await createServer({
configFile: false,
root: fileURLToPath(new URL("../..", import.meta.url)),
logLevel: "error", cacheDir: cache.cacheDir,
plugins: [shutdown.plugin, solid(), { name: "plugin-controls-fixture", configureServer(vite) {
vite.middlewares.use("/fixture", async (_request, response) => {
response.setHeader("Content-Type", "text/html")
response.end(await vite.transformIndexHtml("/fixture", '<html><body><div id="root"></div><script type="module" src="/tests/browser/fixtures/plugin-controls.tsx"></script></body></html>'))
})
} }],
resolve: { dedupe: ["solid-js"] },
optimizeDeps: { exclude: ["lucide-solid"] },
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null },
})
shutdown.own(server)
await server.listen()
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/fixture`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
armFixtureNavigationBudget(browser)
})
after(async () => {
await browser?.close()
await server?.close()
})
test("V2 plugin controls load on demand and expose explicit Global and Project switches", async () => {
const page = await browser.newPage({ viewport: { width: 520, height: 900 }, locale: "en-US" })
const errors: string[] = []
page.on("pageerror", (error) => errors.push(error.message))
await page.goto(url)
await page.waitForTimeout(100)
assert.equal(await page.evaluate(() => (window as any).fixture.reads()), 0, "a hidden plugin section must not load")
await page.evaluate(() => (window as any).fixture.show())
await page.getByText("acme.reviewer", { exact: true }).waitFor()
if (process.env.CODENOMAD_PLUGIN_SCREENSHOTS) {
await mkdir(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, { recursive: true })
await page.screenshot({ path: join(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, "plugins-initial.png"), fullPage: true })
}
assert.equal(await page.locator(".plugin-control-row").count(), 3)
assert.equal(await page.locator('.plugin-control-row input[type="checkbox"]').count(), 6)
assert.deepEqual(await page.locator(".plugin-control-scope-label").allTextContents(), ["Global", "Project"])
assert.equal(await page.locator(".plugin-controls-description, .plugin-controls-footer, .plugin-control-sub, .plugin-controls-notice").count(), 0)
assert.equal(await page.locator(".plugin-controls-header button").count(), 1, "only the compact refresh button appears in the header")
assert.ok(await page.locator(".plugin-control-identity button").count() > 0, "package rows expose a compact native update/check action")
const refresh = page.locator(".plugin-controls-header button")
assert.equal(await refresh.isEnabled(), true)
assert.equal(await refresh.locator(".animate-spin").count(), 0)
const readsBeforeRefresh = await page.evaluate(() => (window as any).fixture.reads())
await page.evaluate(() => (window as any).fixture.holdRead())
await refresh.click()
await page.waitForFunction((before) => (window as any).fixture.reads() > before, readsBeforeRefresh)
assert.equal(await refresh.isDisabled(), true)
assert.equal(await refresh.getAttribute("aria-busy"), "true")
assert.equal(await refresh.locator(".animate-spin").count(), 1)
await page.evaluate(() => (window as any).fixture.releaseRead())
await page.waitForFunction(() => !document.querySelector<HTMLButtonElement>(".plugin-controls-header button")!.disabled)
assert.equal(await refresh.locator(".animate-spin").count(), 0)
assert.equal(await page.locator(".plugin-control-tooltip").count(), 0, "details stay out of the resting surface")
await page.locator('[data-plugin-id="broken.plugin"] .plugin-control-name').hover()
await page.locator(".plugin-control-tooltip").waitFor()
assert.match(await page.locator(".plugin-control-tooltip").innerText(), /Failed — Setup failed safely/)
await page.mouse.move(500, 800)
await page.locator(".plugin-control-tooltip").waitFor({ state: "hidden" })
assert.equal(await page.getByText("Runtime inventory", { exact: true }).count(), 0)
assert.equal(await page.getByText("Configured entries", { exact: true }).count(), 0)
assert.equal(await page.getByText("opencode.provider.demo", { exact: true }).count(), 0)
assert.equal(await page.getByText("opencode.prompt.identity", { exact: true }).count(), 0)
assert.equal(await page.locator('.plugin-control-row input[type="checkbox"]').first().isDisabled(), false)
const inherited = page.locator('[data-plugin-id="broken.plugin"]')
const inheritedGlobal = inherited.locator('[data-scope="global"] input[type="checkbox"]')
const inheritedProject = inherited.locator('[data-scope="project"] input[type="checkbox"]')
assert.equal(await inheritedGlobal.isChecked(), true)
assert.equal(await inheritedProject.isChecked(), true)
await inheritedProject.click()
await page.waitForFunction(() => (window as any).fixture.calls.some((call: any) => (
call.type === "mutation" && call.pluginId === "broken.plugin"
&& call.scope === "project" && call.enabled === false
)))
assert.equal(await inheritedGlobal.isChecked(), true, "a Project override must not change the Global switch")
assert.equal(await inheritedProject.isChecked(), false)
const sleeping = page.locator('[data-plugin-id="sleeping.plugin"]')
const globalToggle = sleeping.locator('[data-scope="global"] input[type="checkbox"]')
const projectToggle = sleeping.locator('[data-scope="project"] input[type="checkbox"]')
assert.equal(await globalToggle.isChecked(), true)
assert.equal(await projectToggle.isChecked(), false)
await projectToggle.click()
await page.waitForFunction(() => (window as any).fixture.calls.some((call: any) => (
call.type === "mutation" && call.pluginId === "sleeping.plugin"
&& call.scope === "project" && call.enabled === true
)))
assert.equal(await projectToggle.isChecked(), true)
const acmeGlobal = page.locator('[data-plugin-id="acme.reviewer"] [data-scope="global"] input[type="checkbox"]')
await acmeGlobal.click()
await page.waitForFunction(() => (window as any).fixture.calls.some((call: any) => (
call.type === "mutation" && call.pluginId === "acme.reviewer"
&& call.scope === "global" && call.enabled === false
)))
const switchGeometry = await page.evaluate(() => {
const track = document.querySelector(".plugin-control-row .MuiSwitch-track")!
const root = document.querySelector(".plugin-control-row .MuiSwitch-root") as HTMLElement | null
const box = (root ?? track as HTMLElement).getBoundingClientRect()
return { radius: getComputedStyle(track).borderRadius, width: Math.round(box.width), height: Math.round(box.height) }
})
assert.notEqual(switchGeometry.radius, "0px")
assert.deepEqual([switchGeometry.width, switchGeometry.height], [40, 24])
const readsBeforeSessionChange = await page.evaluate(() => (window as any).fixture.reads())
await page.evaluate(() => (window as any).fixture.switchSession())
await page.waitForTimeout(100)
assert.equal(
await page.evaluate(() => (window as any).fixture.reads()),
readsBeforeSessionChange,
"another session in the same worktree must reuse the snapshot",
)
const readsBeforeActivation = await page.evaluate(() => (window as any).fixture.reads())
await page.evaluate(() => (window as any).fixture.hide())
await page.waitForFunction(() => !(window as any).fixture.isActive())
await page.evaluate(() => (window as any).fixture.activateSleepingPlugin())
await page.waitForTimeout(100)
assert.equal(
await page.evaluate(() => (window as any).fixture.reads()),
readsBeforeActivation,
"events must not refresh a hidden plugin section",
)
await page.evaluate(() => (window as any).fixture.show())
await page.waitForFunction((before) => (window as any).fixture.reads() > before, readsBeforeActivation)
const burstReads = await page.evaluate(() => (window as any).fixture.eventBurst())
assert.equal(burstReads, 2, "an in-flight event burst must coalesce into one trailing refresh")
assert.deepEqual(errors, [])
await page.close()
})
test("failed packages without IDs remain actionable and reconcile into loaded plugin rows", async () => {
const page = await browser.newPage({ viewport: { width: 380, height: 600 }, locale: "en-US" })
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
try {
await page.goto(url)
await page.evaluate(() => { (window as any).fixture.failedPackagesOnly(); (window as any).fixture.setNarrow(); (window as any).fixture.show() })
const target = "opencode-gemini-auth@latest"
const row = page.locator(`[data-package-target="${target}"]`)
await row.waitFor()
assert.equal(await page.locator(".plugin-control-row").count(), 2, "duplicate native targets share one recovery row")
assert.equal(await page.locator('.plugin-control-row input[type="checkbox"]').count(), 0, "a package target must never be used as an activation ID")
await row.locator(".plugin-control-name").focus()
await page.locator(".plugin-control-tooltip").waitFor()
assert.match(await page.locator(".plugin-control-tooltip").innerText(), /Failed — Package failed before exporting a plugin ID/)
await row.getByRole("button", { name: `Check updates for ${target}`, exact: true }).click()
await page.getByRole("menuitem", { name: `Check updates for ${target}`, exact: true }).click()
const update = row.getByRole("button", { name: `Update ${target}`, exact: true })
await update.waitFor()
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true)
if (process.env.CODENOMAD_PLUGIN_SCREENSHOTS) {
await mkdir(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, { recursive: true })
await page.screenshot({ path: join(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, "plugins-failed-packages.png"), fullPage: true })
}
await update.click()
await page.getByRole("menuitem", { name: `Update ${target}`, exact: true }).click()
await page.locator('[data-plugin-id="recovered.plugin"]').waitFor()
assert.equal(await row.count(), 0, "successful native loading replaces the package-only row")
assert.equal(await page.locator(".plugin-control-row").count(), 2)
assert.deepEqual(await page.evaluate(() => (window as any).fixture.calls.filter((call: any) => ["check", "update"].includes(call.type))), [
{ type: "check", location: { directory: "/repo" }, target },
{ type: "update", location: { directory: "/repo" }, targets: [target] },
])
assert.deepEqual(errors, [])
} finally { await page.close() }
})
test("V2 plugin controls disable Project when it resolves to the Global document", async () => {
const page = await browser.newPage({ viewport: { width: 520, height: 900 }, locale: "en-US" })
const errors: string[] = []
page.on("pageerror", (error) => errors.push(error.message))
await page.goto(url)
await page.evaluate(() => (window as any).fixture.setTargets(["global"]))
await page.evaluate(() => (window as any).fixture.show())
await page.getByText("acme.reviewer", { exact: true }).waitFor()
const row = page.locator('[data-plugin-id="acme.reviewer"]')
assert.equal(await row.locator('[data-scope="global"] input[type="checkbox"]').isDisabled(), false)
assert.equal(await row.locator('[data-scope="project"] input[type="checkbox"]').isDisabled(), true)
const projectDescription = await row.locator('[data-scope="project"] input').getAttribute("aria-describedby")
const noticeId = projectDescription?.split(" ").find((id) => id.endsWith("-notice"))
assert.ok(noticeId)
assert.equal(await page.locator(`[id="${noticeId}"]`).getAttribute("class"), "sr-only")
assert.ok(projectDescription?.split(" ").includes(noticeId))
assert.equal(await page.evaluate((ids) => ids!.split(" ").every(id => Boolean(document.getElementById(id))), projectDescription), true)
assert.ok(!(await row.locator('[data-scope="global"] input').getAttribute("aria-describedby"))?.split(" ").includes(noticeId))
const mutationsBefore = await page.evaluate(() => (window as any).fixture.calls.filter((call: any) => call.type === "mutation").length)
await row.locator('[data-scope="project"] input[type="checkbox"]').click({ force: true })
await page.waitForTimeout(200)
assert.equal(
await page.evaluate(() => (window as any).fixture.calls.filter((call: any) => call.type === "mutation").length),
mutationsBefore,
"a disabled Project switch must not mutate",
)
await page.evaluate(() => (window as any).fixture.setNarrow())
await page.waitForTimeout(100)
const narrowLane = await row.locator('[data-scope="global"]').boundingBox()
assert.ok((narrowLane?.width ?? 48) <= 40, "narrow panels compact the switch lanes")
await row.locator(".plugin-control-name").focus()
await page.locator(".plugin-control-tooltip").waitFor()
assert.match(await page.locator(".plugin-control-tooltip").innerText(), /Project controls are unavailable/)
await row.locator("input").first().focus()
await page.locator(".plugin-control-tooltip").waitFor({ state: "hidden" })
if (process.env.CODENOMAD_PLUGIN_SCREENSHOTS) {
await mkdir(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, { recursive: true })
await page.screenshot({ path: join(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, "plugins-unavailable-narrow.png"), fullPage: true })
}
assert.deepEqual(errors, [])
await page.close()
})
test("plugin rows retain focus through passive refresh and guarded pending mutations", async () => {
const page = await browser.newPage({ viewport: { width: 520, height: 900 } })
await page.goto(url)
await page.evaluate(() => (window as any).fixture.show())
const input = page.locator('[data-plugin-id="acme.reviewer"] [data-scope="global"] input')
await input.waitFor()
await input.focus()
const original = await input.elementHandle()
await page.evaluate(() => (window as any).fixture.eventBurst())
assert.equal(await original!.evaluate((node) => node.isConnected && node === document.activeElement), true)
await page.evaluate(() => (window as any).fixture.holdMutation())
await input.press("Space")
await page.waitForFunction(() => document.activeElement?.getAttribute("aria-busy") === "true")
assert.equal(await input.isChecked(), true, "pending state retains the authoritative value")
await input.press("Space")
assert.equal(await page.evaluate(() => (window as any).fixture.calls.filter((call: any) => call.type === "mutation").length), 1)
assert.equal(await input.isChecked(), true, "blocked repeat activation must not visually toggle")
await input.click({ force: true })
assert.equal(await page.evaluate(() => (window as any).fixture.calls.filter((call: any) => call.type === "mutation").length), 1)
assert.equal(await input.isChecked(), true, "blocked pointer activation must not visually toggle")
await page.evaluate(() => (window as any).fixture.releaseMutation())
await page.waitForFunction(() => document.activeElement?.getAttribute("aria-busy") === "false")
assert.equal(await original!.evaluate((node) => node.isConnected && node === document.activeElement), true)
assert.equal(await input.isChecked(), false)
await input.press("Tab")
assert.equal(await page.locator('[data-plugin-id="acme.reviewer"] [data-scope="project"] input').evaluate((node) => node === document.activeElement), true)
await input.focus()
await page.evaluate(() => { (window as any).fixture.holdMutation(); (window as any).fixture.failMutation() })
await input.press("Space")
await page.waitForFunction(() => document.activeElement?.getAttribute("aria-busy") === "true")
await page.evaluate(() => (window as any).fixture.releaseMutation())
await page.waitForFunction(() => document.activeElement?.getAttribute("aria-busy") === "false")
assert.equal(await original!.evaluate((node) => node.isConnected && node === document.activeElement), true)
assert.equal(await input.isChecked(), false, "failed mutation retains the authoritative value")
await page.close()
})
test("narrow layouts keep rows compact and reveal complete details on name focus", async () => {
const page = await browser.newPage({ viewport: { width: 320, height: 1200 }, hasTouch: true })
await page.goto(url)
const details = await page.evaluate(() => {
const fixture = (window as any).fixture
fixture.setNarrow()
const details = fixture.setLongDetails()
fixture.show()
return details
})
const row = page.locator(`[data-plugin-id="${details.id}"]`)
await row.waitFor()
for (const direction of ["ltr", "rtl"]) {
await page.evaluate((locale) => (window as any).fixture.setLocale(locale), direction === "rtl" ? "he" : "fr")
await page.locator(".plugin-control-scope-label").filter({ hasText: direction === "rtl" ? "פרויקט" : "Projet" }).waitFor()
await page.evaluate((dir) => { document.documentElement.dir = dir }, direction)
await row.locator(".plugin-control-name").focus()
await page.locator(".plugin-control-tooltip").waitFor()
const tooltip = page.locator(".plugin-control-tooltip")
assert.ok((await tooltip.innerText()).includes(details.id))
assert.ok((await tooltip.innerText()).includes(details.source))
assert.equal(await tooltip.evaluate((node) => node.scrollWidth <= node.clientWidth + 1), true)
await page.locator('[data-plugin-id="broken.plugin"] .plugin-control-name').focus()
await page.waitForFunction((error) => [...document.querySelectorAll('[role="tooltip"]')].some((node) => node.textContent?.includes(error)), details.error)
await row.locator("input").first().focus()
await page.locator(".plugin-control-tooltip").waitFor({ state: "hidden" })
assert.ok((await row.boundingBox())!.height <= 36, "long identities must not enlarge a compact row")
assert.equal(await page.locator("main").evaluate((node) => node.scrollWidth <= node.clientWidth + 1), true)
if (direction === "ltr") assert.deepEqual(await page.locator(".plugin-control-scope-label").allTextContents(), ["Global", "Projet"])
if (process.env.CODENOMAD_PLUGIN_SCREENSHOTS) {
await mkdir(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, { recursive: true })
await page.screenshot({ path: join(process.env.CODENOMAD_PLUGIN_SCREENSHOTS, `plugins-narrow-${direction}.png`), fullPage: true })
}
}
await page.close()
})
for (const fail of [false, true]) {
test(`disposed plugin surfaces suppress late mutation feedback, failure=${fail}`, async () => {
const page = await browser.newPage({ viewport: { width: 520, height: 900 } })
await page.goto(url)
await page.evaluate((fail) => {
const fixture = (window as any).fixture
fixture.show()
fixture.holdMutation()
if (fail) fixture.failMutation()
}, fail)
const input = page.locator('[data-plugin-id="acme.reviewer"] [data-scope="project"] input')
await input.click()
await page.waitForFunction(() => document.querySelector('input[aria-busy="true"]'))
await page.evaluate(() => (window as any).fixture.unmount())
assert.equal(await page.locator(".plugin-controls").count(), 0)
await page.evaluate(() => (window as any).fixture.releaseMutation())
// Flush the deferred response and its async presentation continuation.
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 0)))
assert.deepEqual(await page.evaluate(() => (window as any).fixture.toastHistory()), [])
await page.close()
})
}
for (const dispose of [false, true]) {
test(`queued plugin reads wait for visible demand after hiding, disposed=${dispose}`, async () => {
const page = await browser.newPage({ viewport: { width: 520, height: 900 } })
await page.goto(url)
await page.evaluate(() => (window as any).fixture.show())
await page.locator('[data-plugin-id="acme.reviewer"]').waitFor()
await page.evaluate(() => { (window as any).fixture.holdRead(); (window as any).fixture.invalidate() })
await page.waitForFunction(() => (window as any).fixture.reads() === 2)
await page.evaluate(() => (window as any).fixture.invalidate())
await page.evaluate((dispose) => {
const fixture = (window as any).fixture
if (dispose) fixture.unmount()
else fixture.hide()
fixture.releaseRead()
}, dispose)
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 0)))
assert.equal(await page.evaluate(() => (window as any).fixture.reads()), 2, "a queued read must not start after demand is withdrawn")
await page.evaluate((dispose) => {
const fixture = (window as any).fixture
if (dispose) fixture.remount()
else fixture.show()
}, dispose)
await page.waitForFunction(() => (window as any).fixture.reads() === 3)
await page.locator('[data-plugin-id="acme.reviewer"]').waitFor()
await page.close()
})
}