167 lines
9.3 KiB
JavaScript
167 lines
9.3 KiB
JavaScript
// Run with Node's test runner and Playwright on the module path; see README.md.
|
|
// Settings → Automatic updates turns magpie's own update checks (and the
|
|
// downloads they start) off and on, and Check every picks how often they
|
|
// run: 30 min, 1 h, 6 h (the default, nothing set) or 24 h (#472). Off, the
|
|
// interval stays, dimmed, kept for when it is on again, and the version
|
|
// row, not asked yet, says so and offers Check, which asks the backend.
|
|
// The rows are in Settings' About tab. No click moves the page. English
|
|
// and Chinese; no backend, the API is faked.
|
|
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: { auto: "Automatic updates", every: "Check every", off: "Off", on: "On", m30: "30 min", h6: "6 h", h24: "24 h",
|
|
version: "Version", check: "Check", isOff: "Automatic updates are off", latest: "Up to date", whileOn: "While automatic updates are on", often: "How often magpie looks for a newer version" },
|
|
zh: { auto: "自动更新", every: "检查间隔", off: "关闭", on: "开启", m30: "30 分钟", h6: "6 小时", h24: "24 小时",
|
|
version: "版本", check: "检查", isOff: "自动更新已关闭", latest: "已是最新", whileOn: "自动更新开启时生效", often: "magpie 多久检查一次新版本" },
|
|
};
|
|
|
|
function settingsPayload(lang, ctl) {
|
|
const s = {
|
|
theme: "light", lang, tray: "panel", quotaLeft: false, currency: "usd",
|
|
dock: false, dockWindow: false, proxy: "", redact: false, redactPersonal: false, redactWords: [],
|
|
codexWarmup: "", claudeWarmup: "", codexWarmAt: "", claudeWarmAt: "", workbuddyCheckin: false, noStats: false,
|
|
trayUsage: "", trayUsageEvery: 3, vision: "", imageGen: "",
|
|
version: "0.1.400", dir: "~/.config/magpie", gateway: "http://127.0.0.1:3425",
|
|
proxyNow: "none", proxySource: "none", login: false,
|
|
visionModels: [], imageGenModels: [], workbuddyCheckins: [], lanURLs: [],
|
|
fx: { rate: 7.2, at: new Date().toISOString(), stale: false },
|
|
};
|
|
// as the backend sends them: left out while they are their defaults
|
|
if (ctl.off) s.noAutoUpdate = true;
|
|
if (ctl.every) s.updateEvery = ctl.every;
|
|
return s;
|
|
}
|
|
|
|
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 = {};" });
|
|
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: settingsPayload(lang, ctl), fx: settingsPayload(lang, ctl).fx });
|
|
if (url.pathname === "/api/settings") {
|
|
if (req.method() === "POST") {
|
|
const body = req.postDataJSON();
|
|
ctl.off = !!body.noAutoUpdate;
|
|
ctl.every = body.updateEvery === 360 ? 0 : body.updateEvery;
|
|
ctl.saves.push(body);
|
|
}
|
|
return json(settingsPayload(lang, ctl));
|
|
}
|
|
if (url.pathname === "/api/update/check") {
|
|
ctl.checks++;
|
|
ctl.state = "latest";
|
|
return json({ state: "latest", current: "0.1.400", latest: "0.1.400" });
|
|
}
|
|
if (url.pathname === "/api/update") return json({ state: ctl.state, current: "0.1.400" });
|
|
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
|
if (url.pathname === "/api/usage/quotas") return json([]);
|
|
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
|
|
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)];
|
|
await route.fulfill({ body: await fs.readFile(file), contentType });
|
|
};
|
|
}
|
|
|
|
// wheel a row into sight, as a user would; a click must then not move the page
|
|
async function showRow(page, row) {
|
|
await page.locator("#view-settings").hover();
|
|
for (let i = 0; i < 40; i++) {
|
|
const box = await row.boundingBox();
|
|
const view = await page.locator("#view-settings").boundingBox();
|
|
if (box && view && box.y >= view.y + 4 && box.y + box.height <= view.y + view.height - 4) break;
|
|
await page.mouse.wheel(0, 180);
|
|
await page.waitForTimeout(16);
|
|
}
|
|
await page.waitForTimeout(250);
|
|
}
|
|
const scrolls = (page) => page.evaluate(() => [window.scrollY, document.scrollingElement.scrollTop, ...[...document.querySelectorAll(".view")].map((v) => v.scrollTop)].join(","));
|
|
|
|
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
|
test(engine + ": automatic updates and how often", async (t) => {
|
|
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
|
t.after(() => browser.close());
|
|
for (const lang of ["en", "zh"]) {
|
|
const w = words[lang];
|
|
await t.test(lang + ": on every 6 h by default; the interval and the switch are saved", async () => {
|
|
const ctl = { off: false, every: 0, state: "", checks: 0, saves: [] };
|
|
const errors = [];
|
|
const page = await (await browser.newContext({ viewport: { width: 900, height: 560 }, 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=settings");
|
|
// the rows are in Settings' About part
|
|
const aboutTab = async () => {
|
|
await page.locator("#setTab-about").click();
|
|
await page.locator("#setPage-about").waitFor({ state: "visible" });
|
|
};
|
|
await aboutTab();
|
|
const rowOf = (name) => page.locator("#about .row.pref", { has: page.locator(".name", { hasText: new RegExp("^" + name + "$") }) });
|
|
const auto = rowOf(w.auto), every = rowOf(w.every), version = rowOf(w.version);
|
|
await auto.waitFor();
|
|
assert.equal((await auto.locator(".opt.on").innerText()).trim(), w.on);
|
|
assert.equal((await every.locator(".opt.on").innerText()).trim(), w.h6);
|
|
assert.deepEqual((await every.locator(".opt").allInnerTexts()).map((s) => s.trim()), [w.m30, lang === "zh" ? "1 小时" : "1 h", w.h6, w.h24]);
|
|
|
|
// every 30 minutes
|
|
await showRow(page, every);
|
|
let before = await scrolls(page);
|
|
await every.locator(".opt", { hasText: w.m30 }).click();
|
|
await page.waitForFunction(() => document.querySelector(".update-every-row .opt.on")?.textContent.startsWith("30"));
|
|
assert.equal(ctl.saves.at(-1).updateEvery, 30);
|
|
assert.equal(ctl.saves.at(-1).noAutoUpdate, false);
|
|
assert.equal(await scrolls(page), before, "the click must not scroll the page");
|
|
|
|
// off: the interval stays, dimmed, as it was
|
|
await showRow(page, auto);
|
|
before = await scrolls(page);
|
|
await auto.locator(".opt", { hasText: w.off }).click();
|
|
await every.locator(".sub", { hasText: w.whileOn }).waitFor();
|
|
assert.equal(await every.evaluate((r) => r.classList.contains("off")), true);
|
|
assert.equal(ctl.saves.at(-1).noAutoUpdate, true);
|
|
assert.equal(ctl.saves.at(-1).updateEvery, 30);
|
|
assert.equal(await scrolls(page), before, "the click must not scroll the page");
|
|
|
|
// after a reload: still off; the version row says so and Check asks
|
|
await page.reload();
|
|
await aboutTab();
|
|
await auto.waitFor();
|
|
assert.equal((await auto.locator(".opt.on").innerText()).trim(), w.off);
|
|
assert.equal((await every.locator(".opt.on").innerText()).trim(), w.m30);
|
|
await version.locator(".sub", { hasText: w.isOff }).waitFor();
|
|
await showRow(page, version);
|
|
before = await scrolls(page);
|
|
await version.locator("button", { hasText: w.check }).click();
|
|
await version.locator(".sub", { hasText: w.latest }).waitFor();
|
|
assert.equal(ctl.checks, 1);
|
|
assert.equal(await scrolls(page), before, "the click must not scroll the page");
|
|
|
|
// on again, every 24 hours
|
|
await showRow(page, auto);
|
|
before = await scrolls(page);
|
|
await auto.locator(".opt", { hasText: w.on }).click();
|
|
await every.locator(".sub", { hasText: w.often }).waitFor();
|
|
assert.equal(ctl.saves.at(-1).noAutoUpdate, false);
|
|
assert.equal(await every.evaluate((r) => r.classList.contains("off")), false);
|
|
assert.equal((await every.locator(".opt.on").innerText()).trim(), w.m30);
|
|
assert.equal(await scrolls(page), before, "the click must not scroll the page");
|
|
await showRow(page, every);
|
|
before = await scrolls(page);
|
|
await every.locator(".opt", { hasText: w.h24 }).click();
|
|
await page.waitForFunction(() => document.querySelector(".update-every-row .opt.on")?.textContent.startsWith("24"));
|
|
assert.equal(ctl.saves.at(-1).updateEvery, 1440);
|
|
assert.equal(await scrolls(page), before, "the click must not scroll the page");
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
}
|
|
});
|
|
}
|