Skip to content

Commit 76808f0

Browse files
committed
✨ Add --dry-run option to preview page discovery
- Add --dry-run flag to print discovered pages without capturing screenshots - Shows pages grouped by source (sitemap vs HTML scan) - Displays task count, viewports, and concurrency settings - Useful for debugging page discovery and include/exclude patterns Also includes tests for: - Sitemap XML parsing (standard and index formats) - Config schema validation and smart concurrency defaults - Dry-run mode behavior with various options
1 parent 5a8988d commit 76808f0

File tree

11 files changed

+628
-9
lines changed

11 files changed

+628
-9
lines changed

clients/static-site/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export default {
7777
omitBackground: false,
7878
},
7979

80-
concurrency: 3,
80+
// Concurrency auto-detected from CPU cores (min 2, max 8)
81+
// concurrency: 4,
8182

8283
// Page filtering
8384
include: 'blog/**',
@@ -134,12 +135,13 @@ Configuration is merged in this order (later overrides earlier):
134135
## CLI Options
135136

136137
- `--viewports <list>` - Comma-separated viewport definitions (format: `name:WxH`)
137-
- `--concurrency <n>` - Number of parallel pages to process (default: 3)
138+
- `--concurrency <n>` - Number of parallel browser tabs (default: auto-detected based on CPU cores, min 2, max 8)
138139
- `--include <pattern>` - Include page pattern (glob)
139140
- `--exclude <pattern>` - Exclude page pattern (glob)
140141
- `--browser-args <args>` - Additional Puppeteer browser arguments
141142
- `--headless` - Run browser in headless mode (default: true)
142143
- `--full-page` - Capture full page screenshots (default: false)
144+
- `--dry-run` - Print discovered pages and task count without capturing screenshots
143145
- `--use-sitemap` - Use sitemap.xml for page discovery (default: true)
144146
- `--sitemap-path <path>` - Path to sitemap.xml relative to build directory
145147

@@ -382,6 +384,13 @@ jobs:
382384
383385
### Pages not found
384386
387+
Use `--dry-run` to see which pages are discovered without capturing screenshots:
388+
```bash
389+
vizzly static-site ./dist --dry-run
390+
```
391+
392+
This shows pages grouped by source (sitemap vs HTML scan), the total screenshot count, and your current configuration.
393+
385394
Ensure your build has completed and check for sitemap.xml or HTML files:
386395
```bash
387396
ls dist/sitemap.xml

clients/static-site/src/index.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,51 @@ export async function run(buildPath, options = {}, context = {}) {
9393
// Load and merge configuration
9494
let config = await loadConfig(buildPath, options, vizzlyConfig);
9595

96+
// Handle dry-run mode early - just discover and print pages
97+
if (options.dryRun) {
98+
let pages = await discoverPages(config.buildPath, config);
99+
logger.info(
100+
`🔍 Dry run: Found ${pages.length} pages in ${config.buildPath}\n`
101+
);
102+
103+
if (pages.length === 0) {
104+
logger.warn(' No pages found matching your configuration.');
105+
return;
106+
}
107+
108+
// Group by source for clarity
109+
let sitemapPages = pages.filter(p => p.source === 'sitemap');
110+
let htmlPages = pages.filter(p => p.source === 'html');
111+
112+
if (sitemapPages.length > 0) {
113+
logger.info(` From sitemap (${sitemapPages.length}):`);
114+
for (let page of sitemapPages) {
115+
logger.info(` ${page.path}`);
116+
}
117+
}
118+
119+
if (htmlPages.length > 0) {
120+
logger.info(` From HTML scan (${htmlPages.length}):`);
121+
for (let page of htmlPages) {
122+
logger.info(` ${page.path}`);
123+
}
124+
}
125+
126+
// Show task count that would be generated
127+
let taskCount = pages.length * config.viewports.length;
128+
logger.info('');
129+
logger.info(`📸 Would capture ${taskCount} screenshots:`);
130+
logger.info(
131+
` ${pages.length} pages × ${config.viewports.length} viewports`
132+
);
133+
logger.info(
134+
` Viewports: ${config.viewports.map(v => `${v.name} (${v.width}×${v.height})`).join(', ')}`
135+
);
136+
logger.info(` Concurrency: ${config.concurrency} tabs`);
137+
138+
return;
139+
}
140+
96141
// Determine mode: TDD or Run
97142
let debug = logger.debug?.bind(logger) || (() => {});
98143
let isTdd = await isTddModeAvailable(debug);
@@ -205,7 +250,9 @@ export async function run(buildPath, options = {}, context = {}) {
205250
logger.info(' npx vizzly static-site ./dist');
206251
logger.info('');
207252
logger.info(' 2. Or set VIZZLY_TOKEN for cloud uploads:');
208-
logger.info(' VIZZLY_TOKEN=your-token npx vizzly static-site ./dist');
253+
logger.info(
254+
' VIZZLY_TOKEN=your-token npx vizzly static-site ./dist'
255+
);
209256
logger.info('');
210257
return;
211258
}

clients/static-site/src/plugin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export default {
6666
.option('--browser-args <args>', 'Additional Puppeteer browser arguments')
6767
.option('--headless', 'Run browser in headless mode')
6868
.option('--full-page', 'Capture full page screenshots')
69+
.option(
70+
'--dry-run',
71+
'Print discovered pages without capturing screenshots'
72+
)
6973
.option('--use-sitemap', 'Use sitemap.xml for page discovery')
7074
.option(
7175
'--sitemap-path <path>',

clients/static-site/src/tasks.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ export async function processAllTasks(tasks, pool, config, logger, deps = {}) {
242242

243243
// Log total time
244244
let totalTime = Date.now() - startTime;
245-
logger.info(` ✅ Completed ${total} screenshots in ${formatDuration(totalTime)}`);
245+
logger.info(
246+
` ✅ Completed ${total} screenshots in ${formatDuration(totalTime)}`
247+
);
246248

247249
if (errors.length > 0) {
248250
logger.warn(` ⚠️ ${errors.length} failed`);
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* Tests for configuration schema validation
3+
*/
4+
5+
import assert from 'node:assert';
6+
import { cpus } from 'node:os';
7+
import { describe, it } from 'node:test';
8+
import {
9+
getDefaultConcurrency,
10+
validateStaticSiteConfig,
11+
validateStaticSiteConfigWithDefaults,
12+
} from '../src/config-schema.js';
13+
14+
describe('config-schema', () => {
15+
describe('getDefaultConcurrency', () => {
16+
it('returns a positive integer', () => {
17+
let concurrency = getDefaultConcurrency();
18+
19+
assert.ok(Number.isInteger(concurrency));
20+
assert.ok(concurrency > 0);
21+
});
22+
23+
it('returns at least 2', () => {
24+
let concurrency = getDefaultConcurrency();
25+
26+
assert.ok(concurrency >= 2);
27+
});
28+
29+
it('returns at most 8', () => {
30+
let concurrency = getDefaultConcurrency();
31+
32+
assert.ok(concurrency <= 8);
33+
});
34+
35+
it('calculates based on CPU cores', () => {
36+
let cores = cpus().length;
37+
let expected = Math.max(2, Math.min(8, Math.floor(cores / 2)));
38+
let concurrency = getDefaultConcurrency();
39+
40+
assert.strictEqual(concurrency, expected);
41+
});
42+
});
43+
44+
describe('validateStaticSiteConfig', () => {
45+
it('validates minimal config', () => {
46+
let config = {};
47+
48+
let validated = validateStaticSiteConfig(config);
49+
50+
assert.ok(validated.viewports);
51+
assert.ok(validated.browser);
52+
assert.ok(validated.screenshot);
53+
assert.ok(validated.pageDiscovery);
54+
});
55+
56+
it('applies default concurrency from CPU cores', () => {
57+
let config = {};
58+
59+
let validated = validateStaticSiteConfig(config);
60+
let expected = getDefaultConcurrency();
61+
62+
assert.strictEqual(validated.concurrency, expected);
63+
});
64+
65+
it('allows overriding concurrency', () => {
66+
let config = { concurrency: 10 };
67+
68+
let validated = validateStaticSiteConfig(config);
69+
70+
assert.strictEqual(validated.concurrency, 10);
71+
});
72+
73+
it('validates viewports', () => {
74+
let config = {
75+
viewports: [
76+
{ name: 'mobile', width: 375, height: 667 },
77+
{ name: 'desktop', width: 1920, height: 1080 },
78+
],
79+
};
80+
81+
let validated = validateStaticSiteConfig(config);
82+
83+
assert.strictEqual(validated.viewports.length, 2);
84+
assert.strictEqual(validated.viewports[0].name, 'mobile');
85+
});
86+
87+
it('rejects invalid viewport', () => {
88+
let config = {
89+
viewports: [{ name: 'invalid', width: -100, height: 667 }],
90+
};
91+
92+
assert.throws(() => validateStaticSiteConfig(config));
93+
});
94+
95+
it('validates browser config', () => {
96+
let config = {
97+
browser: {
98+
headless: false,
99+
args: ['--no-sandbox'],
100+
},
101+
};
102+
103+
let validated = validateStaticSiteConfig(config);
104+
105+
assert.strictEqual(validated.browser.headless, false);
106+
assert.deepStrictEqual(validated.browser.args, ['--no-sandbox']);
107+
});
108+
109+
it('validates screenshot config', () => {
110+
let config = {
111+
screenshot: {
112+
fullPage: true,
113+
omitBackground: true,
114+
},
115+
};
116+
117+
let validated = validateStaticSiteConfig(config);
118+
119+
assert.strictEqual(validated.screenshot.fullPage, true);
120+
assert.strictEqual(validated.screenshot.omitBackground, true);
121+
});
122+
123+
it('validates page discovery config', () => {
124+
let config = {
125+
pageDiscovery: {
126+
useSitemap: false,
127+
sitemapPath: 'custom-sitemap.xml',
128+
scanHtml: true,
129+
},
130+
};
131+
132+
let validated = validateStaticSiteConfig(config);
133+
134+
assert.strictEqual(validated.pageDiscovery.useSitemap, false);
135+
assert.strictEqual(
136+
validated.pageDiscovery.sitemapPath,
137+
'custom-sitemap.xml'
138+
);
139+
});
140+
141+
it('validates include/exclude patterns', () => {
142+
let config = {
143+
include: '/blog/*',
144+
exclude: '/drafts/*',
145+
};
146+
147+
let validated = validateStaticSiteConfig(config);
148+
149+
assert.strictEqual(validated.include, '/blog/*');
150+
assert.strictEqual(validated.exclude, '/drafts/*');
151+
});
152+
153+
it('allows null include/exclude', () => {
154+
let config = {
155+
include: null,
156+
exclude: null,
157+
};
158+
159+
let validated = validateStaticSiteConfig(config);
160+
161+
assert.strictEqual(validated.include, null);
162+
assert.strictEqual(validated.exclude, null);
163+
});
164+
});
165+
166+
describe('validateStaticSiteConfigWithDefaults', () => {
167+
it('returns defaults when config is undefined', () => {
168+
let validated = validateStaticSiteConfigWithDefaults(undefined);
169+
170+
assert.ok(validated.viewports);
171+
assert.ok(validated.browser);
172+
assert.ok(validated.concurrency > 0);
173+
});
174+
175+
it('returns defaults when config is null', () => {
176+
let validated = validateStaticSiteConfigWithDefaults(null);
177+
178+
assert.ok(validated.viewports);
179+
assert.ok(validated.concurrency > 0);
180+
});
181+
182+
it('validates provided config', () => {
183+
let config = { concurrency: 5 };
184+
185+
let validated = validateStaticSiteConfigWithDefaults(config);
186+
187+
assert.strictEqual(validated.concurrency, 5);
188+
});
189+
});
190+
});

clients/static-site/tests/config.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ describe('config', () => {
6666
let config = parseCliOptions(options);
6767

6868
assert.strictEqual(config.pageDiscovery.useSitemap, false);
69-
assert.strictEqual(config.pageDiscovery.sitemapPath, 'custom-sitemap.xml');
69+
assert.strictEqual(
70+
config.pageDiscovery.sitemapPath,
71+
'custom-sitemap.xml'
72+
);
7073
});
7174
});
7275

clients/static-site/tests/crawler.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ describe('crawler', () => {
3131
});
3232

3333
it('normalizes separators', () => {
34-
assert.strictEqual(filePathToUrlPath('blog\\post-1.html'), '/blog/post-1');
34+
assert.strictEqual(
35+
filePathToUrlPath('blog\\post-1.html'),
36+
'/blog/post-1'
37+
);
3538
});
3639

3740
it('ensures leading slash', () => {

0 commit comments

Comments
 (0)