The README opens like a product page: the mark, a line on what it is, the links, the language switch and a picture of the real interface running on an invented mailbox, in dark and light and in both languages. scripts/screenshots holds that invented mailbox and the script that photographs it, so the pictures can be redone. Previews now decode any HTML entity through the browser rather than a short list, so a Portuguese or marketing preview no longer prints ç or ·. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
67 lines
3.3 KiB
JavaScript
67 lines
3.3 KiB
JavaScript
// 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();
|