#!/usr/bin/env bun // Screenshots of every desktop screen in light and dark, from the Vite dev server in fixture mode (no Tauri, no store // accounts), plus a few interaction checks (command palette, toast progress, keyboard focus). Dev tool only. // cd apps/desktop && VITE_FIXTURES=1 bunx vite --port 1431 & // bun scripts/screens.ts --base http://127.0.0.1:1431 --out [--only portfolio,app:health] [--themes light,dark] // Uses playwright (root devDependency) with the installed Google Chrome (channel "chrome"). import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { chromium, type Page } from 'playwright'; const arg = (k: string, d: string) => { const i = process.argv.indexOf(`--${k}`); return i >= 0 ? process.argv[i + 1] ?? d : d; }; const base = arg('base', 'http://127.0.0.1:1431'); const out = arg('out', join(import.meta.dir, '..', '.screens')); const only = arg('only', '').split(',').filter(Boolean); const themes = arg('themes', 'light,dark').split(','); const lang = arg('lang', 'en'); const suffix = arg('suffix', ''); const prefix = arg('prefix', 'd3'); export const SCREENS: [string, string][] = [ ['portfolio', 'portfolio'], ['releases', 'app:releases'], ['releases-2', 'app:releases:2'], ['catalog', 'app:catalog'], ['listings', 'app:listings'], ['health', 'app:health'], ['reviews', 'app:reviews'], ['testflight', 'app:testflight'], ['checks', 'app:checks'], ['apps', 'apps'], ['accounts', 'accounts'], ['dependencies', 'dependencies'], ['settings', 'settings'], ['ignored', 'ignored'], ['store-app', 'app:releases:3'], ['store-app-reviews', 'app:reviews:3'], ['store-app-single', 'app:releases:7'], ['settings-pairing', 'settings:pairing'], ['settings-refresh', 'settings:refresh'], ['settings-privacy', 'settings:privacy'], ['palette', 'palette'], ['welcome-1', 'welcome:1'], ['welcome-2', 'welcome:2'], ['welcome-3', 'welcome:3'], ['welcome-4', 'welcome:4'], ['welcome-5', 'welcome:5'], ['welcome-6', 'welcome:6'], ['prices', 'app:prices'], ['art', 'app:art'], ['growth', 'app:growth'], ['growth-2', 'app:growth:2'], ['appinfo', 'app:appinfo'], ['appinfo-2', 'app:appinfo:2'], ['store-app-growth', 'app:growth:3'], ]; async function settle(page: Page) { await page.waitForLoadState('networkidle'); await page.waitForTimeout(900); } mkdirSync(out, { recursive: true }); const browser = await chromium.launch({ channel: 'chrome', headless: true }); const files: string[] = []; for (const theme of themes) { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, colorScheme: theme === 'dark' ? 'dark' : 'light', reducedMotion: 'reduce' }); const page = await ctx.newPage(); const errors: string[] = []; page.on('pageerror', e => errors.push(String(e))); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); for (const [name, start] of SCREENS) { if (only.length && !only.includes(name)) continue; await page.goto(`${base}/?start=${encodeURIComponent(start)}&theme=${theme}&lang=${lang}`); await settle(page); const f = join(out, `${prefix}-${name}${suffix}-${theme}${lang === 'en' ? '' : `-${lang}`}.png`); await page.screenshot({ path: f }); files.push(f); } if (!only.length || only.includes('toast')) { // a background job: Read the stores, caught mid-way with its progress toast await page.goto(`${base}/?start=portfolio&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /Read the stores|Mağazaları oku/ }).first().click(); await page.waitForTimeout(900); const f = join(out, `${prefix}-toast${suffix}-${theme}${lang === 'en' ? '' : `-${lang}`}.png`); await page.screenshot({ path: f }); files.push(f); } const shot = async (name: string) => { const f = join(out, `${prefix}-${name}${suffix}-${theme}${lang === 'en' ? '' : `-${lang}`}.png`); await page.screenshot({ path: f }); files.push(f); }; if (!only.length || only.includes('accounts-tested')) { // every account checked: one Play service account was never invited (permission missing, with the fix) await page.goto(`${base}/?start=accounts&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /Test all accounts|Tüm hesapları sına/ }).click(); await page.waitForTimeout(900); await page.getByText(/Sample Labs · Play/).first().scrollIntoViewIfNeeded(); await page.evaluate(() => document.getElementById('content')?.scrollBy({ top: 260 })); await page.waitForTimeout(200); await shot('accounts-tested'); } if (!only.length || only.includes('portfolio-filter')) { // one studio, sorted by name, no grouping, with a search await page.goto(`${base}/?start=portfolio&theme=${theme}&lang=${lang}`); await settle(page); await page.locator('.fchip', { hasText: 'Example Games' }).click(); await page.getByPlaceholder(/Search apps|Uygulama, bundle/).fill('o'); await page.waitForTimeout(300); await shot('portfolio-filter'); await page.locator('.fchip').first().click(); await page.getByPlaceholder(/Search apps|Uygulama, bundle/).fill(''); } if (!only.length || only.includes('tf-edit')) { // TestFlight: What to Test editor of the newest build await page.goto(`${base}/?start=app:testflight&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /What to Test|Neyi test etmeli/ }).first().click(); await page.getByRole('textbox').first().fill('New combo meter and the calmer night palette. Please try level 31 at a 20x combo.'); await page.waitForTimeout(200); await page.evaluate(() => document.getElementById('content')?.scrollBy({ top: 330 })); await page.waitForTimeout(200); await shot('tf-edit'); } if (!only.length || only.includes('reply-edit')) { // Reviews: the reply editor of the approved draft, edited (not saved yet) await page.goto(`${base}/?start=app:reviews&theme=${theme}&lang=${lang}`); await settle(page); await page.locator('textarea.reply-edit').fill('Sorry about the lost streak! The fix for the crash at high combos is in 2.4.0, in review now. Thank you for the level number.'); await page.waitForTimeout(200); await shot('reply-edit'); } if (!only.length || only.includes('write-toast')) { // a write running: halt the Play rollout (owner action), caught with its progress toast await page.goto(`${base}/?start=app:releases&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /^Halt$|^Durdur$/ }).first().click(); await page.waitForTimeout(450); await shot('write-toast'); } if (!only.length || only.includes('import')) { // Import apps running, with its progress toast await page.goto(`${base}/?start=accounts&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /^Import apps$|^Uygulamaları içe aktar$/ }).first().click(); await page.waitForTimeout(700); await shot('import'); } if (!only.length || only.includes('prices-diff')) { // a price sheet compared with the stores, with the owner's Apply await page.goto(`${base}/?start=app:prices&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /Compare store\/prices.csv|store\/prices.csv karşılaştır/ }).click(); await page.waitForTimeout(1500); await shot('prices-diff'); } if (!only.length || only.includes('export-menu')) { // the top-right Export menu of a screen await page.goto(`${base}/?start=app:reviews&theme=${theme}&lang=${lang}`); await settle(page); await page.getByRole('button', { name: /^Export$|^Dışa aktar$/ }).click(); await page.waitForTimeout(300); await shot('export-menu'); } if (!only.length || only.includes('art-lightbox')) { // one render opened large await page.goto(`${base}/?start=app:art&theme=${theme}&lang=${lang}`); await settle(page); await page.locator('.art-pic').first().click(); await page.waitForTimeout(500); await shot('art-lightbox'); } if (errors.length) console.error(`${theme}: page errors:\n ${[...new Set(errors)].join('\n ')}`); await ctx.close(); } await browser.close(); console.log(files.join('\n'));