Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup. Co-authored-by: Cursor <cursoragent@cursor.com>
273 lines
8.3 KiB
JavaScript
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,
|
|
});
|
|
});
|
|
});
|