Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clients/static-site/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- **Breaking: Screenshot naming format updated** - Screenshot names no longer include viewport suffixes (e.g., `@mobile`, `@desktop`). Instead, viewport information is now stored as properties for better organization and compatibility with file system restrictions.
- **Before:** `blog/post-1@mobile` (could cause validation errors with slashes)
- **After:** Name: `blog-post-1`, Properties: `{ viewport: 'mobile', viewportWidth: 375, viewportHeight: 667 }`
- **After:** Name: `blog-post-1`, Properties: `{ viewport: 'mobile', viewport_width: 375, viewport_height: 667 }`
- Path separators (`/` and `\`) are now replaced with hyphens for cleaner, more portable names
- This change improves compatibility with Vizzly's security validation and enables better grouping in the dashboard

Expand Down
21 changes: 21 additions & 0 deletions clients/static-site/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Stubborn Mule Software

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
34 changes: 27 additions & 7 deletions clients/static-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { run } from '@vizzly-testing/static-site';

await run('./dist', {
viewports: 'mobile:375x667,desktop:1920x1080',
concurrency: 3,
concurrency: 4,
}, {
output: console,
config: vizzlyConfig,
Expand All @@ -58,6 +58,10 @@ export default {
// Standard Vizzly config
server: { port: 47392 },
build: { environment: 'test' },
comparison: {
threshold: 0.1,
minClusterSize: 2,
},

// Static Site plugin config
staticSite: {
Expand All @@ -68,13 +72,16 @@ export default {
],

browser: {
type: 'chromium',
headless: true,
args: ['--no-sandbox'],
},

screenshot: {
fullPage: true,
omitBackground: false,
timeout: 45000,
requestTimeout: 60000,
},

// Concurrency auto-detected from CPU cores (min 2, max 8)
Expand All @@ -94,6 +101,10 @@ export default {
};
```

The shared top-level `comparison` section controls cloud/run thresholds. The
`staticSite` section stays focused on page discovery, browser settings,
viewports, and screenshot capture.

### Interactions File (Optional)

For page-specific interactions and overrides, create a `vizzly.static-site.js` file:
Expand All @@ -117,6 +128,7 @@ export default {
viewports: ['mobile', 'desktop'],
},
'/pricing': {
interaction: 'products/*',
screenshot: { fullPage: true },
},
},
Expand All @@ -136,15 +148,18 @@ Configuration is merged in this order (later overrides earlier):

- `--viewports <list>` - Comma-separated viewport definitions (format: `name:WxH`)
- `--concurrency <n>` - Number of parallel browser tabs (default: auto-detected based on CPU cores, min 2, max 8)
- `--browser <type>` - Browser engine to use: `chromium`, `firefox`, or `webkit`
- `--include <pattern>` - Include page pattern (glob)
- `--exclude <pattern>` - Exclude page pattern (glob)
- `--browser-args <args>` - Additional Puppeteer browser arguments
- `--browser-args <args>` - Additional Playwright browser arguments
- `--headless` - Run browser in headless mode (default: true)
- `--no-headless` - Run browser with a visible window
- `--full-page` - Capture full page screenshots (default: true)
- `--no-full-page` - Capture viewport-only screenshots
- `--timeout <ms>` - Screenshot timeout in milliseconds (default: 45000)
- `--dry-run` - Print discovered pages and task count without capturing screenshots
- `--use-sitemap` - Use sitemap.xml for page discovery (default: true)
- `--no-use-sitemap` - Disable sitemap.xml page discovery
- `--sitemap-path <path>` - Path to sitemap.xml relative to build directory

## Page Discovery
Expand Down Expand Up @@ -234,16 +249,21 @@ Patterns support glob-like syntax:

## Screenshot Naming

Screenshots are named based on the page path, with viewport information stored as properties for better grouping:
Screenshots are named based on the page path. The plugin records browser,
viewport, viewport dimensions, page URL, and capture mode metadata
automatically. You can add custom screenshot `properties` from config when you
need extra signature dimensions such as theme, locale, or auth state:

**Name format:** `path-to-page` (slashes replaced with hyphens)

**Properties:** Viewport metadata (`viewport`, `viewportWidth`, `viewportHeight`)
**Properties:** Browser, viewport, URL, capture-mode metadata, and any custom
properties (`browser`, `viewport`, `viewport_width`, `viewport_height`, `url`,
`fullPage`, plus user-defined fields)

Examples:
- Name: `index`, Properties: `{ viewport: 'mobile', viewportWidth: 375, viewportHeight: 667 }`
- Name: `blog-post-1`, Properties: `{ viewport: 'desktop', viewportWidth: 1920, viewportHeight: 1080 }`
- Name: `docs-getting-started`, Properties: `{ viewport: 'tablet', viewportWidth: 768, viewportHeight: 1024 }`
- Name: `index`, Properties: `{ browser: 'chromium', viewport: 'mobile', viewport_width: 375, viewport_height: 667, url: 'http://localhost:3000/' }`
- Name: `blog-post-1`, Properties: `{ browser: 'chromium', viewport: 'desktop', viewport_width: 1920, viewport_height: 1080, url: 'http://localhost:3000/blog/post-1' }`
- Name: `docs-getting-started`, Properties: `{ browser: 'webkit', viewport: 'tablet', viewport_width: 768, viewport_height: 1024, url: 'http://localhost:3000/docs/getting-started' }`

This approach allows Vizzly to group screenshots by viewport while keeping names clean and compatible with file system restrictions.

Expand Down
19 changes: 4 additions & 15 deletions clients/static-site/examples/sample-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample Page - Vizzly Static Site Example</title>

<!-- Vizzly meta tags for per-page configuration -->
<!-- Only capture mobile and desktop viewports for this page -->
<meta name="vizzly:viewports" content="mobile,desktop">

<!-- Reference named interaction from config -->
<meta name="vizzly:interaction" content="scroll-to-footer">

<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
Expand Down Expand Up @@ -49,26 +42,22 @@
<body>
<header>
<h1>Sample Static Site Page</h1>
<p>Demonstrating Vizzly screenshot capture with meta tag configuration</p>
<p>Demonstrating Vizzly screenshot capture with the companion config files</p>
</header>

<main class="content">
<section>
<h2>About This Page</h2>
<p>This page demonstrates how to configure Vizzly screenshot capture using HTML meta tags.</p>
<p>This page is discovered like any other HTML file. Viewports and interactions are configured in the companion <code>vizzly.config.js</code> and <code>vizzly.static-site.js</code> files.</p>

<div class="card">
<h3>Viewport Configuration</h3>
<p>This page uses custom viewport configuration:</p>
<code>&lt;meta name="vizzly:viewports" content="mobile,desktop"&gt;</code>
<p>Only mobile and desktop viewports will be captured.</p>
<p>The example config captures only mobile and desktop viewports for this page.</p>
</div>

<div class="card">
<h3>Interaction Hooks</h3>
<p>This page references a named interaction hook:</p>
<code>&lt;meta name="vizzly:interaction" content="scroll-to-footer"&gt;</code>
<p>The page will scroll to the footer before the screenshot is taken.</p>
<p>The example interactions file references a named hook that scrolls to the footer before the screenshot is taken.</p>
</div>
</section>

Expand Down
7 changes: 4 additions & 3 deletions clients/static-site/examples/vizzly.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {

comparison: {
threshold: 0.1,
minClusterSize: 2,
},

// Static Site plugin configuration
Expand Down Expand Up @@ -48,8 +49,8 @@ export default {
omitBackground: false, // Include page background
},

// Parallel processing
concurrency: 3, // Number of pages to process simultaneously
// Parallel processing defaults to CPU-aware concurrency (min 2, max 8)
// concurrency: 4,

// Page filtering
include: null, // Optional: Only process pages matching this pattern
Expand All @@ -70,6 +71,6 @@ export default {
// Storybook plugin configuration (if also using Storybook)
storybook: {
viewports: [{ name: 'default', width: 1920, height: 1080 }],
concurrency: 3,
// concurrency: 4,
},
};
3 changes: 1 addition & 2 deletions clients/static-site/examples/vizzly.static-site.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ export default {
let loadMoreBtn = await page.$('.load-more');
if (loadMoreBtn) {
await loadMoreBtn.click();
// Wait for content to load
await page.waitForTimeout(1000);
await page.waitForSelector('.portfolio-item');
}
},

Expand Down
10 changes: 6 additions & 4 deletions clients/static-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"import": "./dist/index.js"
},
"./plugin": {
"import": "./dist/plugin.js"
}
},
"main": "./dist/index.js",
"vizzlyPlugin": "./dist/plugin.js",
"files": [
"dist",
"README.md",
"LICENSE",
"CHANGELOG.md"
"CHANGELOG.md",
"LICENSE"
],
"scripts": {
"build": "pnpm run clean && pnpm run compile",
Expand Down
2 changes: 0 additions & 2 deletions clients/static-site/src/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,10 @@ export async function launchBrowser(options = {}, dependencies = {}) {
// Playwright throws plain Error objects without error codes, so we must match
// on message patterns. These patterns cover known Playwright error messages:
// - "Executable doesn't exist at <path>" (missing browser binary)
// - "browserType.launch: ..." (launch failure context)
// - "playwright install" (Playwright's own suggestion in the error)
// - "download new browsers" (alternative phrasing in some versions)
let isBrowserMissing =
error.message.includes("Executable doesn't exist") ||
error.message.includes('browserType.launch') ||
error.message.includes('playwright install') ||
error.message.includes('download new browsers');

Expand Down
36 changes: 32 additions & 4 deletions clients/static-site/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* Reads from config.staticSite section of main vizzly.config.js
*/

import { validateStaticSiteConfigWithDefaults } from './config-schema.js';
import {
getDefaultConcurrency,
validateStaticSiteConfigWithDefaults,
} from './config-schema.js';
import { loadInteractions } from './utils/interactions-loader.js';
import { parseViewport } from './utils/viewport.js';

Expand All @@ -23,7 +26,7 @@ export let defaultConfig = {
fullPage: true,
omitBackground: false,
},
concurrency: 3,
concurrency: getDefaultConcurrency(),
include: null,
exclude: null,
pageDiscovery: {
Expand All @@ -50,6 +53,9 @@ export function parseCliOptions(options) {
}

if (options.concurrency !== undefined) {
if (!Number.isInteger(options.concurrency) || options.concurrency < 1) {
throw new Error('concurrency must be a positive integer');
}
config.concurrency = options.concurrency;
}

Expand Down Expand Up @@ -82,6 +88,13 @@ export function parseCliOptions(options) {
config.screenshot = { ...config.screenshot, timeout: options.timeout };
}

if (options.requestTimeout !== undefined) {
config.screenshot = {
...config.screenshot,
requestTimeout: options.requestTimeout,
};
}

if (options.useSitemap !== undefined) {
config.pageDiscovery = {
...config.pageDiscovery,
Expand Down Expand Up @@ -130,6 +143,10 @@ export function mergeConfigs(...configs) {
...merged.interactions,
...config.interactions,
},
pages: {
...merged.pages,
...config.pages,
},
viewports: config.viewports || merged.viewports,
};
}, {});
Expand All @@ -151,8 +168,7 @@ export function getPageConfig(globalConfig, page) {
// Find matching page config by pattern
let pageOverrides = null;
for (let [pattern, config] of Object.entries(globalConfig.pages)) {
// Simple pattern matching - exact match or wildcard
if (pattern === page.path || matchPattern(pattern, page.path)) {
if (matchesPagePattern(pattern, page.path)) {
pageOverrides = config;
break;
}
Expand Down Expand Up @@ -202,6 +218,18 @@ function matchPattern(pattern, path) {
return regex.test(path);
}

function matchesPagePattern(pattern, path) {
if (pattern === path) return true;
if (!path) return false;

let value = String(path);
let withSlash = value.startsWith('/') ? value : `/${value}`;
let withoutSlash = withSlash === '/' ? '/' : withSlash.slice(1);
let candidates = [...new Set([value, withSlash, withoutSlash])];

return candidates.some(candidate => matchPattern(pattern, candidate));
}

/**
* Load and merge all configuration sources
* Priority: CLI options > vizzly.static-site.js > vizzlyConfig.staticSite > defaults
Expand Down
4 changes: 4 additions & 0 deletions clients/static-site/src/crawler.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ export function generatePageUrl(baseUrl, page) {
path = path.slice(0, -1);
}

if (path.endsWith('.html')) {
return `${baseUrl}${path}`;
}

// Try /path.html first (most common convention)
return `${baseUrl}${path}.html`;
}
Expand Down
Loading