#910: the header's Update sheet said "Update to v0.1.1020" with v0.1.1024 out. The sheet shows the update the updater downloaded at its last look at the feed, which is every six hours by default (Settings' UpdateEvery), and releases come several a day; usemagpie.ai's feed was current (it answers v0.1.1031 now). The restart itself already asked the feed again (recheck), so only the sheet was behind. Now GET /api/update/notes first asks the feed again, as the restart does, when a version is downloaded and waiting and the feed was last asked over five minutes ago; a newer release starts downloading and the restart waits for it, as one after a recheck does. The answer carries latest, and the sheet retitles itself "Update to v<newest>" with every release's notes up to it. TestUpdateNotesAskTheFeedAgain (a download of 0.1.1020 three hours old, a feed at 0.1.1024: the answer names 0.1.1024 with both releases' notes; asked a moment ago, the feed isn't asked) and update-notes-ask.test.cjs' new case (the sheet names v0.1.403 and lists three releases when the notes name it) fail without the change. #868 (Hu9956): once Names & levels was open (to set Provider in model names) it couldn't be closed: the only way was its button under the list, out of sight below it, and naming is page state, so it stayed open in every provider editor after until magpie restarted. Now the open list has a quiet title (Names & levels) and Fold at its top, held there while its rows scroll; Fold folds it, the button under the list reads off again and takes the focus, nothing scrolls. An editor opened afresh (no draft yet) has it folded; one redrawn keeps it as it was. en/zh/ja/de ("Fold", "Fold Names & levels"). names-fold.test.cjs (WebKit, en/zh/ja/de: opens folded, the title and Fold, Fold stays at the top while the rows scroll, picking Not on names I set leaves it open and foldable, Fold folds it and turns the button off without scrolling, no left border, a fresh editor opens folded, no select) fails on the old code; name-suffix-where-named, model-price-edit, provider-levels-save, provider-same-as, provider-detect, provider-detect-models, readable-focus and click-scroll pass in WebKit; model-api-preset and provider-levels-scroll fail the same on origin/main. The editor's other fields aren't regrouped. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestUserPath and TestClaudeSignedOut timed out once under load and pass alone).
138 lines
8.9 KiB
JavaScript
138 lines
8.9 KiB
JavaScript
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||
// Hu9956, #844: a restart to update went ahead on the click, and what it
|
||
// brought was only to be found in Settings › About › What's new. Now the
|
||
// header's Update pill and Settings' Restart to update first show what
|
||
// changed since this version, every release up to the new one, newest first,
|
||
// and restart only on that sheet's Restart to update; Later leaves it be.
|
||
// The update's own notes show at once, the full list once it comes. No click
|
||
// moves the page; no coloured left border. English and Chinese; no backend.
|
||
const assert = require("node:assert/strict");
|
||
const fs = require("node:fs/promises");
|
||
const path = require("node:path");
|
||
const { test } = require("node:test");
|
||
const { chromium, webkit } = require("playwright");
|
||
|
||
const assets = path.resolve(__dirname, "../assets");
|
||
|
||
const words = {
|
||
en: { pill: "Update", restart: "Restart to update", later: "Later", head: "Update to v0.1.402", newer: "Update to v0.1.403", since: "What changed since v0.1.400", version: "Version" },
|
||
zh: { pill: "更新", restart: "重启以更新", later: "稍后", head: "更新到 v0.1.402", newer: "更新到 v0.1.403", since: "自 v0.1.400 以来的更新内容", version: "版本" },
|
||
};
|
||
|
||
function server(lang, ctl) {
|
||
return async (route) => {
|
||
const req = route.request(), url = new URL(req.url());
|
||
const json = (data) => route.fulfill({ json: data });
|
||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
|
||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||
const settings = { theme: "light", lang, version: "0.1.400", dir: "~/.config/magpie", gateway: "http://127.0.0.1:3425", fx: { rate: 7.2, at: new Date().toISOString() } };
|
||
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings });
|
||
if (url.pathname === "/api/settings") return json(settings);
|
||
if (url.pathname === "/api/update") return json({ state: "ready", current: "0.1.400", latest: "0.1.402", notes: "- Two: the pill asks first", url: "https://example.test/v0.1.402" });
|
||
if (url.pathname === "/api/update/notes") {
|
||
ctl.asked.push(url.searchParams.get("lang"));
|
||
await new Promise((r) => setTimeout(r, 200));
|
||
// #910: the feed asked again names a release out since the download
|
||
if (ctl.newer) return json({ latest: "0.1.403", releases: [{ version: "0.1.403", notes: "- Three: out since" }, { version: "0.1.402", notes: "- Two: the pill asks first" }, { version: "0.1.401", notes: "- One: a fix" }] });
|
||
return json({ latest: "0.1.402", releases: [{ version: "0.1.402", notes: "- Two: the pill asks first" }, { version: "0.1.401", notes: "- One: a fix" }] });
|
||
}
|
||
if (url.pathname === "/api/update/install") {
|
||
ctl.installs.push(req.postDataJSON() || {});
|
||
return route.fulfill({ status: 204 });
|
||
}
|
||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
|
||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||
if (url.pathname.startsWith("/api/")) return json({});
|
||
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)];
|
||
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
|
||
};
|
||
}
|
||
|
||
const scrolls = (page) => page.evaluate(() => [window.scrollY, document.scrollingElement.scrollTop, ...[...document.querySelectorAll(".view")].map((v) => v.scrollTop)].join(","));
|
||
// the sheet's releases: each version with its notes
|
||
const shown = (page) => page.$$eval("#modal .update-ask .wn-rel", (rs) => rs.map((r) => [r.querySelector(".wn-ver")?.textContent.trim(), r.querySelector(".wn-notes")?.textContent.trim()]));
|
||
|
||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||
for (const lang of ["en", "zh"]) {
|
||
test(`${engine} ${lang}: what an update brings is shown before it restarts`, async (t) => {
|
||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||
t.after(() => browser.close());
|
||
const w = words[lang], ctl = { installs: [], asked: [] }, errors = [];
|
||
const page = await (await browser.newContext({ viewport: { width: 1000, height: 640 }, reducedMotion: "reduce" })).newPage();
|
||
page.setDefaultTimeout(5000);
|
||
page.on("pageerror", (e) => errors.push(e.message));
|
||
await page.route("**/*", server(lang, ctl));
|
||
await page.goto("http://magpie.test/?view=agents");
|
||
const pill = page.locator("#update");
|
||
await pill.waitFor({ state: "visible" });
|
||
const before = await scrolls(page);
|
||
|
||
// the pill: the sheet, not a restart
|
||
await pill.click();
|
||
const ask = page.locator("#modal .update-ask");
|
||
await ask.waitFor();
|
||
assert.equal((await ask.locator(".ehead b").textContent()).trim(), w.head);
|
||
assert.equal((await ask.locator(".wn-since").textContent()).trim(), w.since);
|
||
await page.waitForFunction(() => document.querySelectorAll("#modal .update-ask .wn-rel").length === 2);
|
||
assert.deepEqual(await shown(page), [["v0.1.402", "Two: the pill asks first"], ["v0.1.401", "One: a fix"]]);
|
||
assert.deepEqual(ctl.asked, [lang], "the notes are asked for in the page's language");
|
||
assert.deepEqual(await ask.locator(".bar button").allTextContents(), [w.later, w.restart]);
|
||
for (const n of [ask, ask.locator(".wn-rel").first()]) assert.equal(await n.evaluate((e) => getComputedStyle(e).borderLeftStyle), "none");
|
||
assert.deepEqual(ctl.installs, [], "nothing restarts until it's asked to");
|
||
if (process.env.SHOT) await page.screenshot({ path: `${process.env.SHOT}/${engine}-${lang}.png` });
|
||
|
||
// Later: the sheet goes, nothing restarts
|
||
await ask.locator(".bar button", { hasText: w.later }).click();
|
||
await ask.waitFor({ state: "detached" });
|
||
await page.waitForTimeout(150);
|
||
assert.deepEqual(ctl.installs, []);
|
||
|
||
// the pill again, then the sheet's Restart to update
|
||
await pill.click();
|
||
await ask.locator(".bar button.primary").click();
|
||
for (let i = 0; i < 50 && !ctl.installs.length; i++) await page.waitForTimeout(20);
|
||
assert.equal(ctl.installs.length, 1, "the sheet's Restart to update restarts");
|
||
await ask.waitFor({ state: "detached" });
|
||
assert.equal(await scrolls(page), before, "no click moves the page");
|
||
|
||
// Settings › About's Restart to update asks the same
|
||
await page.goto("http://magpie.test/?view=settings&tab=about");
|
||
const row = page.locator("#about .row.pref", { has: page.locator(".name", { hasText: w.version }) }).first();
|
||
await row.locator("button", { hasText: w.restart }).click();
|
||
await ask.waitFor();
|
||
await page.waitForFunction(() => document.querySelectorAll("#modal .update-ask .wn-rel").length === 2);
|
||
assert.equal(ctl.installs.length, 1, "the row's click asks first");
|
||
await ask.locator(".bar button.primary").click();
|
||
for (let i = 0; i < 50 && ctl.installs.length < 2; i++) await page.waitForTimeout(20);
|
||
assert.equal(ctl.installs.length, 2);
|
||
assert.deepEqual(errors, []);
|
||
});
|
||
|
||
// Moody-Sin, #910: an update downloaded hours ago held the sheet to its
|
||
// version with newer ones out; the notes' answer asks the feed again,
|
||
// and the sheet names the newest, with every release up to it
|
||
test(`${engine} ${lang}: the sheet names a release out since the download`, async (t) => {
|
||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||
t.after(() => browser.close());
|
||
const w = words[lang], ctl = { installs: [], asked: [], newer: true }, errors = [];
|
||
const page = await (await browser.newContext({ viewport: { width: 1000, height: 640 }, reducedMotion: "reduce" })).newPage();
|
||
page.setDefaultTimeout(5000);
|
||
page.on("pageerror", (e) => errors.push(e.message));
|
||
await page.route("**/*", server(lang, ctl));
|
||
await page.goto("http://magpie.test/?view=agents");
|
||
const pill = page.locator("#update");
|
||
await pill.waitFor({ state: "visible" });
|
||
await pill.click();
|
||
const ask = page.locator("#modal .update-ask");
|
||
await ask.waitFor();
|
||
await page.waitForFunction(() => document.querySelectorAll("#modal .update-ask .wn-rel").length === 3);
|
||
assert.equal((await ask.locator(".ehead b").textContent()).trim(), w.newer);
|
||
assert.equal((await ask.locator(".wn-since").textContent()).trim(), w.since);
|
||
assert.deepEqual((await shown(page)).map((r) => r[0]), ["v0.1.403", "v0.1.402", "v0.1.401"]);
|
||
assert.deepEqual(errors, []);
|
||
});
|
||
}
|
||
}
|