Files
Sybil-2/web/tests/ui-mode.browser.mjs

101 lines
5.5 KiB
JavaScript

import assert from "node:assert/strict";
// Run against a production preview; requests to the backend are mocked.
// WEB_TEST_URL=http://127.0.0.1:5197 PLAYWRIGHT_MODULE=/path/to/playwright-core/index.mjs
// CHROME_EXECUTABLE=/path/to/chrome node tests/ui-mode.browser.mjs
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || "playwright-core");
const target = new URL(process.env.WEB_TEST_URL || "http://127.0.0.1:5197/");
assert.ok(target.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(target.hostname));
const browser = await chromium.launch({
executablePath: process.env.CHROME_EXECUTABLE || undefined,
headless: true,
...(process.env.BROWSER_NO_SANDBOX === "1" ? { args: ["--no-sandbox"] } : {}),
});
const kindle = "Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 Chrome/87.0 Safari/537.36 Kindle/3.0";
async function fixture(options = {}) {
const context = await browser.newContext({ viewport: { width: 600, height: 800 }, serviceWorkers: "block", ...options });
const requests = [], errors = [];
await context.route("**/*", async route => {
const url = new URL(route.request().url());
requests.push(url);
if (url.origin !== target.origin) return route.abort();
if (url.pathname.endsWith("/v1/auth/session")) {
return route.fulfill({ status: 401, json: { message: "missing bearer token" } });
}
if (url.pathname.includes("/api/")) return route.fulfill({ json: {} });
return route.continue();
});
const page = await context.newPage();
page.on("pageerror", error => errors.push(error.message));
const visit = async path => {
requests.length = 0;
await Promise.all([
page.waitForResponse(response => response.url().endsWith("/v1/auth/session")),
page.goto(new URL(path, target).href, { waitUntil: "networkidle" }),
]);
await page.locator('input[type="password"]').waitFor();
};
return { context, page, requests, errors, visit };
}
try {
const reader = await fixture({ userAgent: kindle });
await reader.visit("/?chat=keep-this#reply");
assert.equal(new URL(reader.page.url()).pathname, "/lite");
assert.equal(new URL(reader.page.url()).search, "?chat=keep-this");
assert.equal(new URL(reader.page.url()).hash, "#reply");
const styles = await reader.page.evaluate(() => Array.from(document.styleSheets).map(sheet => sheet.href));
assert.ok(styles.some(href => href && /lite.*\.css/.test(href)), "Lite has its own stylesheet");
assert.ok(reader.requests.every(url => !/character-.*\.(gif|png)|\/App-[^/]+\.js|agent-conversation/.test(url.href)), "Lite does not download full presentation assets");
assert.equal(await reader.page.locator(".lite-auth").evaluate(element => getComputedStyle(element).backgroundImage), "none");
const liteTypography = await reader.page.evaluate(() => ({
body: getComputedStyle(document.body).fontFamily,
wordmark: getComputedStyle(document.querySelector(".sybil-wordmark")).fontFamily,
}));
assert.ok(reader.requests.some(url => url.hostname === "fonts.googleapis.com"), "both presentations request the existing font stylesheet");
for (const text of ["A quieter workspace", "Simple pages.", "Clear text.", "No animations."]) {
assert.equal(await reader.page.getByText(text, { exact: false }).count(), 0, `no added sign-in flavor text: ${text}`);
}
assert.equal(await reader.page.locator('meta[name="viewport"]').getAttribute("content"), "width=device-width, initial-scale=1, viewport-fit=cover");
await reader.visit("/?ui=full&chat=keep-this");
assert.equal(new URL(reader.page.url()).pathname, "/");
assert.deepEqual(await reader.page.evaluate(() => ({
body: getComputedStyle(document.body).fontFamily,
wordmark: getComputedStyle(document.querySelector(".sybil-wordmark")).fontFamily,
})), liteTypography, "Lite preserves the original body and wordmark typefaces");
assert.equal(await reader.page.evaluate(() => localStorage.getItem("sybil:ui-mode")), "full");
await reader.visit("/");
assert.equal(new URL(reader.page.url()).pathname, "/", "stored full choice wins over Kindle hint");
await reader.visit("/lite?ui=lite");
await reader.visit("/");
assert.equal(new URL(reader.page.url()).pathname, "/lite", "explicit Lite choice persists");
assert.deepEqual(reader.errors, []);
await reader.context.close();
const dataSaver = await fixture();
await dataSaver.context.addInitScript(() => Object.defineProperty(navigator, "connection", { value: { saveData: true } }));
await dataSaver.visit("/");
assert.equal(new URL(dataSaver.page.url()).pathname, "/lite");
assert.deepEqual(dataSaver.errors, []);
await dataSaver.context.close();
const blocked = await fixture();
await blocked.context.addInitScript(() => Object.defineProperty(window, "localStorage", { get() { throw new Error("Storage denied"); } }));
await blocked.visit("/?ui=lite");
assert.equal(new URL(blocked.page.url()).pathname, "/lite");
assert.deepEqual(blocked.errors, [], "denied storage does not prevent sign in");
await blocked.context.close();
const failed = await fixture();
await failed.context.route("**/assets/lite-route-page-*.js", route => route.abort());
await failed.page.goto(new URL("/lite", target).href);
await failed.page.getByRole("heading", { name: "Sybil could not load" }).waitFor();
assert.equal(await failed.page.getByRole("button", { name: "Reload" }).count(), 1);
await failed.context.close();
console.log("UI routing, preference persistence, denied storage, isolated Lite assets and route failure recovery passed.");
} finally {
await browser.close();
}