gui: Routing flies a request still under way when the page's first frame came before the trace, and the Requests table's sideways scrollbar goes once the table fits; GUI tests brought up to date with the app

Two app bugs, found where GUI tests failed on main in Chromium:
- Routing: the page's first frame looked for requests to fly before the first trace had come in, found none, and nothing flew them after it came, so a request still under way when Routing opened never reached its member's row (routing-try-fixed failed every run). The first trace now flies those under way when a frame has already been drawn, as coming back into sight does (underWay, shared with resume).
- Usage › Requests: a table that had come to fit its window (led tight) kept #ledHScroll's bar under it with nothing to scroll; ledGrow now redraws the bar on every resize, not only when it leaves columns out (usage-speed "no sideways scrollbar").

Stale or racy tests, the app right:
- library-own-folder, list-sort, shared-skills and nine other Library tests counted the loading skeleton's rows (the "5 vs 4 rows" and the empty lists were the skeleton, not the repo group headers); they wait for .lib-body:not(.lib-skel). library-own-folder's 61px scroll was Playwright's own retry on a button still sliding in, not the app (no scroll setter ran): it waits for the entrance to finish.
- sessions-overview measured the picked bar while the folder's overview redrew the bars; it measures once the overview is in.
- menu-scroll: its agent needs wired and a ref to show a model picker, and the page now asks plugins/updates, usage/quotas, agents/install and whatsnew.
- routing-sessions' large-history tests failed from local midnight until the UTC offset passed: the fixture's times were UTC while the gateway writes local times with their offset, which openRoute takes the day from.
- ledger-hscroll and view-scrollbar measured scrollbars in Chromium launched with --hide-scrollbars; they launch without it, as menu-scroll does.

routing-try-fixed and usage-speed fail without the app changes and pass with them (checked by putting origin/main's routing.js and app.js back). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestHostJSCancellation timed out once under the full run's load and passes on its own, 3 runs).
This commit is contained in:
yetone committed 2026-10-06 03:20:34 +08:00
1 parent 5ecb1dc365
commit 76dc5aed48
19 files changed
+47 -26

No files matched your search

+5 -2
View File
@@ -14408,14 +14408,17 @@ new ResizeObserver(() => {
ledHScroll();
}).observe($("#ledWrap"));
// a table that grows past the window when its width didn't change (a font
// arriving, a redraw while out of sight) leaves its columns out then too
// arriving, a redraw while out of sight) leaves its columns out then too;
// and its scrollbar goes once a table left without them has come to fit,
// rather than staying under a table with nothing to scroll
let ledGrowFrame = 0;
const ledGrow = new ResizeObserver(() => {
if (ledGrowFrame) return;
ledGrowFrame = requestAnimationFrame(() => {
ledGrowFrame = 0;
const wrap = $("#ledWrap"), table = wrap.querySelector("table.led");
if (table && !table.classList.contains("tight") && wrap.clientWidth && wrap.scrollWidth > wrap.clientWidth + 1) { ledFit(); ledHScroll(); }
if (table && !table.classList.contains("tight") && wrap.clientWidth && wrap.scrollWidth > wrap.clientWidth + 1) ledFit();
ledHScroll();
});
});
new ResizeObserver(() => {
+5 -2
View File
@@ -2242,14 +2242,14 @@
// stale: it ends, the stage is drawn as it is now, and only the requests
// still under way fly (#302).
const LIVE = 4;
const underWay = () => pinned || day ? [] : [...src().values()].filter((r) => !r.done && matchesPurpose(r)).sort((a, b) => a.id - b.id).slice(-LIVE);
function resume() {
stopPlays();
if (!loaded) return;
if (!pinned && !rp) cur = newest();
if (cur) sync(true);
else empty();
const live = pinned || day ? [] : [...src().values()].filter((r) => !r.done && matchesPurpose(r)).sort((a, b) => a.id - b.id).slice(-LIVE);
for (const r of live) play(r.id);
for (const r of underWay()) play(r.id);
renderAll();
}
let seen = false, lastFrame = 0, ticking = 0;
@@ -2381,6 +2381,9 @@
if (wanted) { wanted = 0; params.delete("req"); history.replaceState(null, "", params.size ? "?" + params : location.pathname); }
if (asked && asked.id !== newest().id) pinned = asked;
if (r) { cur = r; sync(true); say(affWhy(r, true) || ruleWhy(r, true) || firstWhy(r)); renderAll(); } else empty();
// the page's first frame came before the trace did, and found
// nothing to fly: the requests under way fly now, not never
if (seen && shown()) for (const u of underWay()) if (!playing.has(u.id)) play(u.id);
} else {
if (cur && routes.has(cur.id) && !rp) cur = routes.get(cur.id);
if (pinned && routes.has(pinned.id)) pinned = routes.get(pinned.id);
+1 -1
View File
@@ -102,7 +102,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
const row = (sheet, id) => sheet.locator(`.lib-byagent-row[data-agent="${id}"]`);
+1 -1
View File
@@ -79,7 +79,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
@@ -71,7 +71,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts, fail));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
+1 -1
View File
@@ -51,7 +51,7 @@ const L = {
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
test(engine + ": the Requests table's scrollbar and a row's status", async (t) => {
assert(["chromium", "webkit"].includes(engine), "BROWSER must be chromium or webkit");
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium", ignoreDefaultArgs: ["--hide-scrollbars"] }));
t.after(() => browser.close());
for (const lang of ["en", "zh"]) {
const w = L[lang];
+1 -1
View File
@@ -105,7 +105,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("http://magpie.test/**", server(lang, posts, fail, fixture));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
const row = (page, name) => page.locator("#view-library .lib-row").filter({ has: page.locator(".name", { hasText: new RegExp("^" + name + "$") }) });
+2 -2
View File
@@ -97,7 +97,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("http://magpie.test/**", server(lang, theme, posts, { flaky }));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
// every row inside the list and its own box: the label on one line, the
@@ -152,7 +152,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.locator("#view-library .lib-tabs .opt").first().click();
await card.waitFor();
await page.locator("#view-library .lib-tabs .opt").nth(2).click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
const again = card.getByRole("button", { name: words[lang].again });
const before = await page.evaluate(() => document.querySelector("#view-library").scrollTop);
@@ -105,7 +105,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("http://magpie.test/**", server(lang, checks));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
const status = (name) => page.locator(`#view-library .lib-health[data-server="${name}"]`);
// every server asked for at once, each showing it's being checked
@@ -104,7 +104,10 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("http://magpie.test/**", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
// the page's entrance played out: a button still sliding in isn't
// stable, and Playwright scrolls the view to click it
await page.waitForFunction(() => !document.querySelector("#view-library").getAnimations({ subtree: true }).some((a) => a.playState === "running" && isFinite(a.effect.getComputedTiming().endTime)));
return page;
};
const row = (page, name) => page.locator("#view-library .lib-row").filter({ has: page.locator(".name", { hasText: new RegExp("^" + name) }) });
+2 -2
View File
@@ -98,7 +98,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
const openLib = async () => {
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
};
await openLib();
assert.deepEqual(await serverNames(), ["Alpha", "beta", "gamma"]);
@@ -124,7 +124,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await openLib();
assert.deepEqual(await skillNames(), ["pear", "mango", "kiwi", "cherry", "apple"]);
await page.locator("#view-library .lib-tabs .opt").nth(1).click();
await page.locator("#view-library .lib-list .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-list .lib-row").first().waitFor();
assert.deepEqual(await serverNames(), ["gamma", "beta", "Alpha"]);
await page.locator("#view-library .lib-tabs .opt").nth(2).click();
await pick("libSkills", 0);
+5 -2
View File
@@ -17,9 +17,9 @@ const sessions = usage.map((u, i) => ({
last: date + "T08:00:00Z", start: date + "T07:00:00Z", models: [u], unpriced: 0,
}));
const state = {
agents: [{ id: "fixture", name: "Fixture", path: "/test/config.toml", fields: [{
agents: [{ id: "fixture", name: "Fixture", path: "/test/config.toml", wired: true, fields: [{
key: "model", label: "model", value: "model-01",
options: usage.map(u => ({ value: u.model, label: u.model })),
options: usage.map(u => ({ value: u.model, label: u.model, ref: u.model })),
}] }],
profiles: [], settings: { lang: "en", theme: "light" },
};
@@ -38,6 +38,9 @@ async function serve(route) {
if (url.pathname === "/api/agents/cli") return json({ agents: {}, pending: false });
if (url.pathname === "/api/groups") return json({ groups: [] });
if (url.pathname === "/api/gateway/trace") return json({ mine: false, now: date, totals: { requests: 0, rerouted: 0, errors: 0 } });
if (url.pathname === "/api/plugins/updates") return json({ waiting: [] });
if (url.pathname === "/api/usage/quotas" || url.pathname === "/api/usage/quotas/history" || url.pathname === "/api/agents/install") return json([]);
if (url.pathname === "/api/whatsnew") return json({});
assert(!url.pathname.startsWith("/api/"), "Unexpected API: " + url.pathname);
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
+1 -1
View File
@@ -98,7 +98,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
@@ -79,7 +79,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
+10 -3
View File
@@ -7,8 +7,15 @@ const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const now = new Date();
const day = [now.getFullYear(), now.getMonth() + 1, now.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
// a time as the gateway writes it: local, with its offset, so its date is
// the local day its history keeps it under (a UTC time is a day off for
// the hours between the two midnights)
const stamp = (d) => {
const o = -d.getTimezoneOffset(), p = (n) => String(Math.floor(Math.abs(n))).padStart(2, "0");
return new Date(d.getTime() + o * 60e3).toISOString().slice(0, 19) + (o < 0 ? "-" : "+") + p(o / 60) + ":" + p(o % 60);
};
function req(id, agent, session, cost, priced = true) {
const time = new Date(now.getTime() - (10 - id) * 60e3).toISOString();
const time = stamp(new Date(now.getTime() - (10 - id) * 60e3));
const seat = { id: "relay", provider: "relay", name: "Relay", who: "Test account", kind: "account", model: id === 1 ? "model-a" : "model-b" };
return { id, seq: id, time, agent, session, model: `relay/${seat.model}`, provider: "relay", cost, priced,
order: [seat], tries: [{ id: seat.id, model: seat.model, start: time, done: true, status: 200, ms: 4500, effort: "medium" }],
@@ -424,8 +431,8 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
const page = await browser.newPage({ viewport: { width: 1100, height: 800 }, reducedMotion: "reduce" });
page.setDefaultTimeout(10000);
const errors = [], titleRequests = [];
const fixture = Array.from({ length: 2000 }, (_, i) => ({ ...req(i + 1, mixed && i < 800 ? "claude" : "codex", `chat-${i + 1}`, 0.01), time: now.toISOString() }));
const extra = { ...req(2001, "codex", "opened-chat", 0.01), time: now.toISOString() };
const fixture = Array.from({ length: 2000 }, (_, i) => ({ ...req(i + 1, mixed && i < 800 ? "claude" : "codex", `chat-${i + 1}`, 0.01), time: stamp(now) }));
const extra = { ...req(2001, "codex", "opened-chat", 0.01), time: stamp(now) };
const feed = { titleRequests, route: extra, names: ({ ids }) => ({ names: Object.fromEntries(ids.map((id) => [id, `Applied ${id}`])) }) };
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, feed, fixture));
@@ -250,9 +250,11 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
const y = (await bar.boundingBox()).y;
await bar.click();
await page.waitForFunction(() => document.querySelector("#sessProjects .sess-bar.on"));
// the bars are drawn again once the folder's overview is in: the bar
// is measured where that left it, not as it is taken off for it
await page.waitForFunction(() => document.querySelector("#sessStats .kpi b")?.textContent === "7");
assert.equal((await page.locator("#sessProjects .sess-bar.on").boundingBox()).y, y);
assert.match(await page.locator("#sessFolder").innerText(), /beta/);
await page.waitForFunction(() => document.querySelector("#sessStats .kpi b")?.textContent === "7");
assert(seen.some((s) => s.includes("cwd=%2Fwork%2Fbeta")), seen.join("\n"));
assert.equal(await page.locator("#sessProjects .sess-bar").count(), 2);
await page.locator("#sessProjects .sess-bar.on").click();
+1 -1
View File
@@ -65,7 +65,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
const row = (page, name) => page.locator("#view-library .lib-row").filter({ has: page.locator(".name", { hasText: new RegExp("^" + name + "$") }) });
+1 -1
View File
@@ -108,7 +108,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.route("**/*", server(lang, posts));
await page.goto("http://magpie.test/");
await page.locator('button[data-view="library"]').click();
await page.locator("#view-library .lib-row").first().waitFor();
await page.locator("#view-library .lib-body:not(.lib-skel) .lib-row").first().waitFor();
return page;
};
+1 -1
View File
@@ -39,7 +39,7 @@ function serve() {
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
test(`${engine}: a long page shows a scrollbar, on the Mac too`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium", ignoreDefaultArgs: ["--hide-scrollbars"] }));
const page = await (await browser.newContext({ viewport: { width: 900, height: 500 }, reducedMotion: "reduce" })).newPage();
t.after(() => browser.close());
page.setDefaultTimeout(5000);