import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { findWorkspaceRoot, normalizeViewport, parseCaptureArgs, resolveChromiumLaunchOptions, resolveCaptureJobs, resolveScreenshotBox, resolveScreenshotPlan, resolveWidthNormalization, } from './capture-theme-homepage.mjs'; function makeFixture() { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'theme-capture-')); const appRoot = path.join(workspaceRoot, 'apps', 'make-project'); const themeRoot = path.join(appRoot, 'src', 'themes', 'linear'); fs.mkdirSync(themeRoot, { recursive: true }); fs.writeFileSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'), 'packages: []\n'); fs.writeFileSync( path.join(themeRoot, 'theme.json'), JSON.stringify({ source: { websiteUrl: 'https://linear.app/', originalDetailUrl: 'https://getdesign.md/linear.app/design-md', }, }, null, 2), ); return { workspaceRoot, appRoot }; } describe('capture-theme-homepage CLI planning', () => { it('parses screenshot options needed for stable long homepage captures', () => { const args = parseCaptureArgs([ 'node', 'capture-theme-homepage.mjs', '--', '--theme', 'linear', '--url', 'https://linear.app', '--viewport', '1440x1200', '--wait-for-selector', 'main', '--dismiss-selector', 'button:has-text("Accept")', '--remove-selector', '.newsletter-modal', '--header', 'Authorization=Bearer token', '--connect-cdp', 'http://localhost:9222', '--storage-state', '.local/auth/linear.json', '--hide-scrollbar', '--scroll-warmup', '--quality', '92', ]); expect(args.theme).toBe('linear'); expect(args.url).toBe('https://linear.app'); expect(args.viewport).toEqual({ width: 1440, height: 1200 }); expect(args.waitForSelectors).toEqual(['main']); expect(args.dismissSelectors).toEqual(['button:has-text("Accept")']); expect(args.removeSelectors).toEqual(['.newsletter-modal']); expect(args.headers).toEqual({ Authorization: 'Bearer token' }); expect(args.connectCdp).toBe('http://localhost:9222'); expect(args.storageState).toBe('.local/auth/linear.json'); expect(args.hideScrollbar).toBe(true); expect(args.scrollWarmup).toBe(true); expect(args.quality).toBe(92); }); it('resolves a single theme from theme.json metadata into the official homepage asset path', () => { const { workspaceRoot, appRoot } = makeFixture(); const args = parseCaptureArgs(['node', 'capture-theme-homepage.mjs', '--theme', 'linear']); const jobs = resolveCaptureJobs(args, { appRoot, workspaceRoot }); expect(jobs).toHaveLength(1); expect(jobs[0].theme).toBe('linear'); expect(jobs[0].url).toBe('https://linear.app/'); expect(jobs[0].outputPath).toBe(path.join(appRoot, 'src/themes/linear/assets/official-homepage.webp')); expect(jobs[0].reportPath).toBe(path.join(workspaceRoot, '.local/theme-captures/linear/meta.json')); expect(jobs[0].viewport).toEqual({ width: 1440, height: 1200 }); expect(jobs[0].waitUntil).toBe('load'); expect(jobs[0].hideScrollbar).toBe(true); expect(jobs[0].scrollWarmup).toBe(true); expect(jobs[0]).not.toHaveProperty('dryRun'); expect(jobs[0]).not.toHaveProperty('help'); }); it('merges config defaults, per-theme settings, and CLI overrides for batch capture', () => { const { workspaceRoot, appRoot } = makeFixture(); const configPath = path.join(workspaceRoot, 'theme-capture.json'); fs.writeFileSync( configPath, JSON.stringify({ defaults: { viewport: '1920x1080', waitForSelectors: ['main'], removeSelectors: ['.chat-widget'], headers: { 'X-Capture': 'theme' }, }, themes: { linear: { url: 'https://linear.app/custom', dismissSelectors: ['button:has-text("Accept all")'], waitAfterLoadMs: 2500, }, stripe: 'https://stripe.com', }, }, null, 2), ); const args = parseCaptureArgs([ 'node', 'capture-theme-homepage.mjs', '--config', configPath, '--theme', 'linear', '--viewport', '1366x900', '--header', 'Authorization=Bearer local', ]); const jobs = resolveCaptureJobs(args, { appRoot, workspaceRoot }); expect(jobs).toHaveLength(1); expect(jobs[0]).toMatchObject({ theme: 'linear', url: 'https://linear.app/custom', viewport: { width: 1366, height: 900 }, waitForSelectors: ['main'], removeSelectors: ['.chat-widget'], dismissSelectors: ['button:has-text("Accept all")'], waitAfterLoadMs: 2500, headers: { 'X-Capture': 'theme', Authorization: 'Bearer local', }, }); }); it('expands all config themes when --all is requested', () => { const { workspaceRoot, appRoot } = makeFixture(); const configPath = path.join(workspaceRoot, 'theme-capture.json'); fs.writeFileSync( configPath, JSON.stringify({ themes: [ { theme: 'linear', url: 'https://linear.app' }, { theme: 'stripe', url: 'https://stripe.com' }, ], }), ); const args = parseCaptureArgs(['node', 'capture-theme-homepage.mjs', '--config', configPath, '--all']); expect(resolveCaptureJobs(args, { appRoot, workspaceRoot }).map((job) => job.theme)).toEqual([ 'linear', 'stripe', ]); }); it('finds the workspace root and rejects invalid viewport strings', () => { const { workspaceRoot, appRoot } = makeFixture(); expect(findWorkspaceRoot(appRoot)).toBe(workspaceRoot); expect(() => normalizeViewport('wide')).toThrow('Invalid viewport'); }); it('builds launch options with an explicit or discovered Chrome executable fallback', () => { expect(resolveChromiumLaunchOptions({ headless: true, browserExecutable: '/tmp/Test Chrome', }).executablePath).toBe('/tmp/Test Chrome'); const discovered = resolveChromiumLaunchOptions( { headless: false }, { candidatePaths: ['/missing/chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'], exists: (candidate) => candidate.includes('Google Chrome'), }, ); expect(discovered.headless).toBe(false); expect(discovered.executablePath).toBe('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'); expect(discovered.args).toContain('--disable-blink-features=AutomationControlled'); }); it('captures webp outputs through a temporary png screenshot plan', () => { const plan = resolveScreenshotPlan({ theme: 'linear', format: 'webp', outputPath: '/tmp/linear/official-homepage.webp', reportDir: '/tmp/.local/theme-captures/linear', }); expect(plan).toEqual({ playwrightType: 'png', screenshotPath: '/tmp/.local/theme-captures/linear/official-homepage-source.png', outputPath: '/tmp/linear/official-homepage.webp', needsConversion: true, }); }); it('clips very tall webp screenshots to the encoder-safe height', () => { expect(resolveScreenshotBox({ format: 'webp', viewport: { width: 1440, height: 1200 }, maxScreenshotHeight: 16383, }, { scrollHeight: 24000, })).toEqual({ fullPage: false, viewport: { width: 1440, height: 16383 }, clipped: true, sourceHeight: 24000, outputHeight: 16383, }); expect(resolveScreenshotBox({ format: 'webp', viewport: { width: 1440, height: 1200 }, maxScreenshotHeight: 16383, }, { scrollHeight: 8000, })).toEqual({ fullPage: true, clipped: false, sourceHeight: 8000, outputHeight: 8000, }); }); it('normalizes horizontally overflowing screenshots back to the viewport width', () => { expect(resolveWidthNormalization({ viewport: { width: 1440, height: 1200 }, }, { width: 3870, height: 6517, })).toEqual({ needed: true, width: 1440, height: 6517, x: 0, y: 0, }); expect(resolveWidthNormalization({ viewport: { width: 1440, height: 1200 }, }, { width: 1440, height: 6517, })).toEqual({ needed: false, width: 1440, height: 6517, x: 0, y: 0, }); }); });