storebound/apps/desktop/scripts/screens.ts

153 lines
8.2 KiB
TypeScript

#!/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 <dir> [--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'));