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:
1 parent
5ecb1dc365
commit
76dc5aed48
19 files changed
+47
-26
No files matched your search
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"]`);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 + "$") }) });
|
||||
|
||||
@@ -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) }) });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 + "$") }) });
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user