// Photographs the webmail running against the invented mailbox of // stub-server.mjs, for the README. Needs puppeteer-core and a Chrome: // // CHROME_PATH=/usr/bin/google-chrome node scripts/screenshots/capture.mjs // // BASE is where the webmail is listening (default http://localhost:3100). import puppeteer from "puppeteer-core"; import path from "node:path"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const out = path.join(here, "..", "..", "docs", "img"); const base = process.env.BASE ?? "http://localhost:3100"; const stub = process.env.STUB ?? "http://127.0.0.1:4001"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // The webmail only checks that a session cookie looks like one; the stub // accepts anything, so a token with the right claims is enough. const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url"); const session = `${b64({ alg: "HS256" })}.${b64({ aud: "lambdamail:user", purpose: "session", surface: "user", exp: Math.floor(Date.now() / 1000) + 3600 })}.stub`; const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH, headless: true, args: ["--no-sandbox"] }); async function shoot({ name, theme, locale, width = 1440, height = 900, steps }) { // Every picture starts from the mailbox as it began: opening a message marks it read. await fetch(`${stub}/__reset`); const page = await browser.newPage(); await page.setViewport({ width, height, deviceScaleFactor: 1 }); const host = new URL(base).hostname; await page.setCookie( { name: "lm_user_session", value: session, domain: host, path: "/" }, { name: "lm_locale", value: locale, domain: host, path: "/" }, ); await page.evaluateOnNewDocument((t) => localStorage.setItem("lm_theme", t), theme); await page.goto(`${base}/user/mail/inbox`, { waitUntil: "domcontentloaded", timeout: 120000 }); await page.waitForSelector("button.lm-row", { timeout: 120000 }); // Next's development badge is not part of the product. await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); await sleep(1500); const open = async (re) => { for (const row of await page.$$("button.lm-row")) { if (re.test(await row.evaluate((e) => e.textContent))) { await row.click(); return; } } throw new Error(`no row matches ${re}`); }; await steps(page, open); await page.screenshot({ path: path.join(out, `${name}.png`) }); await page.close(); console.log("wrote", name); } const launch = async (page, open) => { await open(/Launch checklist/); await sleep(3500); }; const digest = async (page, open) => { await open(/weekly digest/); await sleep(3500); }; const menu = async (page, open) => { await open(/Launch checklist/); await sleep(3000); await page.keyboard.down("Control"); await page.keyboard.press("k"); await page.keyboard.up("Control"); await sleep(700); }; for (const [locale, tag] of [["en", "en"], ["pt-BR", "pt"]]) { await shoot({ name: `webmail-dark-${tag}`, theme: "dark", locale, steps: launch }); await shoot({ name: `webmail-light-${tag}`, theme: "light", locale, steps: launch }); await shoot({ name: `menu-dark-${tag}`, theme: "dark", locale, steps: menu }); await shoot({ name: `designed-light-${tag}`, theme: "light", locale, steps: digest }); } await browser.close();