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(); }