// Captura a interface do Guará (explorer, vista de pastas e configurações) nos temas // claro/escuro e nos idiomas pt/en. Chamado por scripts/screenshots.sh: // node shots.mjs import { execFileSync, spawn } from "node:child_process"; import { homedir } from "node:os"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const [bin, image, out] = process.argv.slice(2); const core = process.env.PUPPETEER_CORE || `${homedir()}/.claude/tools/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js`; const { default: puppeteer } = await import(core); const chrome = process.env.CHROME || execFileSync("bash", ["-c", `ls -1d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome | sort -V | tail -1`], { encoding: "utf8" }).trim(); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // Dispositivo fictício: a lista real do computador não deve aparecer nas capturas. const FAKE_DEVICES = [{ path: "/dev/nvme0n1", name: "SSD interno", kind: "NVMe", size: 954 * 1024 ** 3, readable: false, mounted_at: [], removable: false }]; const LABEL = "Pendrive 64 GB"; const variants = [ { theme: "dark", lang: "pt" }, { theme: "dark", lang: "en" }, { theme: "light", lang: "pt" }, { theme: "light", lang: "en" }, ]; // Sobe `guara ui` com config isolada (sessões e ajustes próprios) e devolve a URL com o token. async function startServer(theme, lang) { const cfg = mkdtempSync(join(tmpdir(), "guara-shots-")); mkdirSync(cfg, { recursive: true }); writeFileSync(join(cfg, "settings.json"), JSON.stringify({ theme, lang, notify: true, maps: false, updates: false })); const child = spawn(bin, ["ui", "--no-open", "--port", "0"], { env: { ...process.env, GUARA_CONFIG_DIR: cfg }, stdio: ["ignore", "ignore", "pipe"] }); const url = await new Promise((resolve, reject) => { let buf = ""; child.stderr.on("data", (d) => { buf += d; const m = buf.match(/http:\/\/\S+/); if (m) resolve(m[0]); }); child.on("exit", () => reject(new Error("guara ui saiu antes de subir"))); setTimeout(() => reject(new Error("timeout subindo guara ui")), 20000); }); return { url, stop: () => { child.kill(); rmSync(cfg, { recursive: true, force: true }); } }; } async function newPage(browser, url, w = 1600, h = 940) { const page = await browser.newPage(); await page.setViewport({ width: w, height: h, deviceScaleFactor: 1 }); await page.setRequestInterception(true); page.on("request", (req) => { if (req.url().includes("/rpc/devices")) { req.respond({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_DEVICES) }); } else req.continue(); }); await page.goto(url, { waitUntil: "networkidle0" }); await sleep(1200); return page; } // Abre a imagem de teste, faz a varredura profunda e espera terminar. async function scanImage(page) { await page.click("#openImageBtn"); await sleep(400); await page.type("#dlg input[name=v]", image); await page.click("#dlg button[value=ok]"); await sleep(1200); await page.click("#goDeep"); await page.waitForFunction(() => document.querySelector(".xp-tile, .xp-lrow"), { timeout: 120000 }); await page.waitForFunction(() => !document.querySelector("#progressArea .progress-card"), { timeout: 120000 }); await sleep(2500); // miniaturas e autosave da sessão } const relabel = (page) => page.evaluate((t) => { const sub = document.querySelector("#scanSub"); if (sub) sub.innerHTML = `${t}`; }, LABEL); const browser = await puppeteer.launch({ executablePath: chrome, args: ["--no-sandbox"] }); try { for (const { theme, lang } of variants) { const srv = await startServer(theme, lang); try { // 1ª rodada: só para deixar uma sessão salva em "Recentes" (a 2ª rodada cria a outra). let page = await newPage(browser, srv.url); await scanImage(page); await page.close(); page = await newPage(browser, srv.url); await page.evaluate(() => { try { localStorage.setItem("guara-view", "grid"); } catch {} }); await page.reload({ waitUntil: "networkidle0" }); await sleep(1000); await scanImage(page); await relabel(page); // Explorador em grade, com a primeira foto marcada. const chk = await page.$(".xp-tile .xp-chk"); if (chk) await chk.click(); else await (await page.$(".xp-tile")).click(); await sleep(800); await page.screenshot({ path: `${out}/explorer-${theme}-${lang}.png` }); console.log("ok", `explorer-${theme}-${lang}`); if (theme === "light") { // Vista de pastas: pasta DCIM marcada e selecionada. await page.click('button[data-view="folders"]'); await sleep(1000); const dcim = await page.$('.xp-lrow.xp-dir[data-dir="0"]'); await (await dcim.$(".xp-chk")).click(); await sleep(400); await dcim.click({ offset: { x: 200, y: 20 } }); await sleep(800); await relabel(page); await page.screenshot({ path: `${out}/folders-${theme}-${lang}.png` }); console.log("ok", `folders-${theme}-${lang}`); await page.click('button[data-view="grid"]'); await sleep(500); } if (theme === "light") { // Diálogo de configurações, recortado com a moldura borrada ao redor. await page.click("#settingsBtn"); await sleep(900); const box = await (await page.$("#dlg")).boundingBox(); const pad = 24; await page.screenshot({ path: `${out}/settings-language-${lang}.png`, clip: { x: Math.max(0, box.x - pad), y: Math.max(0, box.y - pad), width: box.width + 2 * pad, height: box.height + 2 * pad }, }); console.log("ok", `settings-language-${lang}`); } } finally { srv.stop(); } } } finally { await browser.close(); }