import { chromium } from "playwright"; import { startPreview } from "./preview.mjs"; import { root } from "./build.mjs"; import { resolve } from "node:path"; import assert from "node:assert/strict"; import * as fs from "node:fs/promises"; process.chdir(root); const preview = process.env.BASE_URL ? null : await startPreview({ port: 0 }); const base = process.env.BASE_URL || preview.url; (async () => { let browser; try { browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 390, height: 1000 }, deviceScaleFactor: 1, }); const errors = []; page.on("pageerror", (e) => errors.push(e.message)); const sections = [ "hero", "workflow", "video", "selector", "agent", "prepared", "context", "progression", "stats", "modules", "closing", ]; if (process.argv.includes("--capture")) { await fs.mkdir("captures/current", { recursive: true }); for (const [label, width] of [ ["desktop", 1440], ["tablet", 768], ["mobile", 390], ]) { await page.setViewportSize({ width, height: 960 }); for (const section of sections) { await page.goto(base + "?capture=1§ion=" + section); await page.evaluate(() => document.fonts.ready); await page .locator('[data-capture-section="' + section + '"]') .screenshot({ path: "captures/current/" + label + "-" + section + ".png", }); } } for (const [section, phases] of [ ["hero", ["queued", "moving", "review"]], ["agent", ["queued", "assigned", "filling", "review"]], ]) { await page.setViewportSize({ width: 1440, height: 1300 }); for (const phase of phases) { await page.goto( base + "?capture=1§ion=" + section + "&phase=" + phase, ); await page.evaluate(() => document.fonts.ready); await page .locator('[data-capture-section="' + section + '"]') .screenshot({ path: "captures/current/" + section + "-phase-" + phase + ".png", }); } } await page.setViewportSize({ width: 320, height: 1100 }); await page.goto(base + "?capture=1§ion=hero"); await page.evaluate(() => document.fonts.ready); await page .locator("[data-capture-section=hero]") .screenshot({ path: "captures/current/small-hero.png" }); } const removed = [ ".flow-caption", ".progress-footnote", ".story-product-footer", ".prepared-entry-footer", ".context-caption", ".sel-stage-bottom", ".control-kicker", ".control-example-label", ".editorial-focus-bottom", ".agent-form-footer", ".agent-playback", ]; await page.goto(base + "?capture=1"); for (const selector of removed) assert.equal(await page.locator(selector).count(), 0, selector); assert.equal(await page.locator("#video-title").count(), 1); assert.equal( await page.locator(".agent-form-header [data-agent-replay]").count(), 1, ); // Real controls still work after surrounding narration has been removed. await page.goto(base); await page.locator("#agent").scrollIntoViewIfNeeded(); await page.waitForFunction( () => document.querySelector("[data-agent-demo]").dataset.agentPhase === "review", ); await page.locator("[data-agent-replay]").click(); await page.waitForFunction( () => document.querySelector("[data-agent-demo]").dataset.agentPhase === "queued", ); await page.waitForFunction( () => document.querySelector("[data-agent-demo]").dataset.agentPhase === "review", ); await page.locator('[data-sel-choice="connections"]').click(); assert.equal( await page.locator("#selector-stage").getAttribute("data-focus"), "connections", ); await page.locator('[data-context-choice="work"]').click(); assert.equal( await page.locator("#context-demo").getAttribute("data-context"), "work", ); await page.locator('[data-progress-choice="move"]').click(); assert.equal( await page .locator("[data-progress-stage]") .getAttribute("data-progress-stage"), "move", ); const sourceManifest = await ( await page.request.get(base + "source/manifest.json") ).json(); assert.ok(sourceManifest.files.length > 25); const fullSource = await ( await page.request.get(base + "source/bundle.json") ).json(); assert.ok(JSON.stringify(fullSource).includes("Your next move.")); assert.ok( await page.evaluate( () => document.querySelector(".splash-actions").getBoundingClientRect() .top < document.querySelector(".splash-screen").getBoundingClientRect().top, ), ); const widths = [ 320, 360, 375, 390, 430, 540, 600, 680, 681, 768, 800, 801, 1024, 1440, ]; for (const width of widths) { await page.setViewportSize({ width, height: 1000 }); await page.goto(base + "?capture=1"); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(50); const result = await page.evaluate(() => ({ width: innerWidth, scroll: document.documentElement.scrollWidth, cards: [...document.querySelectorAll("[data-splash-card]")].map((n) => n.getBoundingClientRect().toJSON(), ), slots: [...document.querySelectorAll("[data-splash-slot]")].map((n) => n.getBoundingClientRect().toJSON(), ), })); assert.ok(result.scroll <= width, "overflow " + width); assert.equal(result.cards.length, 3); for (const [i, slot] of [1, 0, 2].entries()) { assert.ok(Math.abs(result.cards[i].x - result.slots[slot].x) < 1); assert.ok(Math.abs(result.cards[i].y - result.slots[slot].y) < 1); } if (width <= 800) { for (const selector of [ ".menu-toggle", ".header-cta", ".stat-source", ".site-footer nav a", "[data-agent-replay]", ]) { const height = await page .locator(selector) .first() .evaluate((n) => n.getBoundingClientRect().height); assert.ok(height >= 44, selector + " height " + height); } } } await page.setViewportSize({ width: 390, height: 1000 }); await page.goto(base); await page.evaluate(() => document.fonts.ready); assert.equal(await page.locator("[data-splash-replay]").count(), 0); assert.equal(await page.locator("[data-splash-announcement]").count(), 0); assert.ok( !(await page.locator(".splash").textContent()).includes( "Keeping your books in sync", ), ); const poses = () => page .locator("[data-splash-card]") .evaluateAll((nodes) => nodes.map((n) => n.style.transform)); const initial = await poses(); await page.waitForFunction( () => document.querySelector("[data-splash-demo]").dataset.splashPhase === "moving", ); await page.locator("[data-splash-toggle]").click(); const frozen = await poses(); await page.waitForTimeout(450); assert.deepEqual(await poses(), frozen); assert.equal( await page.locator("[data-splash-toggle]").getAttribute("aria-label"), "Resume task animation", ); await page.locator("[data-splash-toggle]").click(); await page.waitForTimeout(1700); assert.notDeepEqual(await poses(), initial); await page.locator("#stats").scrollIntoViewIfNeeded(); await page.waitForTimeout(100); const offscreen = await poses(); await page.waitForTimeout(500); assert.deepEqual(await poses(), offscreen); await page.locator(".splash-screen").scrollIntoViewIfNeeded(); await page.emulateMedia({ reducedMotion: "reduce" }); await page.waitForTimeout(100); const reduced = await poses(); await page.waitForTimeout(300); assert.deepEqual(await poses(), reduced); assert.ok(await page.locator("[data-splash-toggle]").isDisabled()); await page.emulateMedia({ reducedMotion: "no-preference" }); await page.waitForTimeout(100); assert.ok(await page.locator("[data-splash-toggle]").isEnabled()); await page.waitForFunction( (previous) => JSON.stringify( [...document.querySelectorAll("[data-splash-card]")].map( (n) => n.style.transform, ), ) !== JSON.stringify(previous), reduced, ); assert.notDeepEqual(await poses(), reduced); assert.deepEqual(errors, []); const report = { base, widths, cards: 3, overflow: false, removedLabels: true, pauseResume: true, offscreenPause: true, reducedMotionBothDirections: true, errors, }; await fs.mkdir("reports", { recursive: true }); await fs.writeFile( "reports/" + (process.env.BASE_URL ? "live" : "local") + "-browser.json", JSON.stringify(report, null, 2), ); console.log(JSON.stringify(report)); } finally { await browser?.close(); await preview?.close(); } })().catch((e) => { console.error(e); process.exitCode = 1; });