Files
OneOS-V2/scripts/capture-theme-homepage.test.mjs
2026-07-29 16:04:39 +08:00

273 lines
8.3 KiB
JavaScript

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,
});
});
});