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,783 changes: 263 additions & 2,520 deletions clients/storybook/package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions clients/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
"scripts": {
"build": "npm run clean && npm run compile",
"clean": "rimraf dist",
"compile": "babel src --out-dir dist --ignore '**/*.spec.js'",
"prepublishOnly": "npm test && npm run build",
"test": "vitest run",
"test:watch": "vitest",
"compile": "babel src --out-dir dist --ignore '**/*.test.js'",
"prepublishOnly": "npm run lint && npm run build",
"test": "node --test --test-reporter=spec 'tests/**/*.test.js'",
"test:watch": "node --test --test-reporter=spec --watch 'tests/**/*.test.js'",
"lint": "biome lint src tests",
"lint:fix": "biome lint --write src tests",
"format": "biome format --write src tests",
Expand All @@ -69,7 +69,6 @@
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@biomejs/biome": "^2.3.8",
"rimraf": "^6.0.1",
"vitest": "^3.2.4"
"rimraf": "^6.0.1"
}
}
94 changes: 88 additions & 6 deletions clients/storybook/src/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,61 @@
import puppeteer from 'puppeteer';
import { setViewport } from './utils/viewport.js';

/**
* Check if running in a CI environment
* @returns {boolean}
*/
function isCI() {
return !!(
process.env.CI ||
process.env.GITHUB_ACTIONS ||
process.env.JENKINS_HOME ||
process.env.CIRCLECI ||
process.env.GITLAB_CI ||
process.env.BUILDKITE
);
}

/**
* Base browser args required for headless operation
*/
let BASE_ARGS = ['--no-sandbox', '--disable-setuid-sandbox'];

/**
* Additional browser args optimized for CI environments
* These reduce memory usage and improve stability in resource-constrained environments
*/
let CI_OPTIMIZED_ARGS = [
// Reduce memory usage
'--disable-dev-shm-usage', // Use /tmp instead of /dev/shm (often too small in Docker)
'--disable-gpu', // No GPU in CI
'--disable-software-rasterizer',

// Disable unnecessary features
'--disable-extensions',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad', // Crash reporting
'--disable-component-update',
'--disable-default-apps',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--disable-translate',

// Reduce resource usage
'--metrics-recording-only',
'--no-first-run',
'--safebrowsing-disable-auto-update',

// Memory optimizations
'--js-flags=--max-old-space-size=1024', // Limit V8 heap (1GB for larger Storybooks)
];

/**
* Launch a Puppeteer browser instance
* @param {Object} options - Browser launch options
Expand All @@ -16,9 +71,15 @@ import { setViewport } from './utils/viewport.js';
export async function launchBrowser(options = {}) {
let { headless = true, args = [] } = options;

let browserArgs = isCI()
? [...BASE_ARGS, ...CI_OPTIMIZED_ARGS, ...args]
: [...BASE_ARGS, ...args];

let browser = await puppeteer.launch({
headless,
args: ['--no-sandbox', '--disable-setuid-sandbox', ...args],
args: browserArgs,
// Reduce protocol timeout for faster failure detection
protocolTimeout: 60_000, // 60s instead of default 180s
});

return browser;
Expand Down Expand Up @@ -52,11 +113,32 @@ export async function createPage(browser) {
* @returns {Promise<void>}
*/
export async function navigateToUrl(page, url, options = {}) {
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000, // 30 second timeout
...options,
});
try {
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000,
...options,
});
} catch (error) {
// Fallback to domcontentloaded if networkidle2 times out
let isTimeout =
error.name === 'TimeoutError' ||
error.message.includes('timeout') ||
error.message.includes('Navigation timeout');

if (isTimeout) {
console.warn(
`Navigation timeout for ${url}, falling back to domcontentloaded`
);
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000,
...options,
});
} else {
throw error;
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Tests for concurrency control
*/

import { describe, expect, it } from 'vitest';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

// Simple concurrency control - process items with limited parallelism
async function mapWithConcurrency(items, fn, concurrency) {
Expand Down Expand Up @@ -39,8 +40,8 @@ describe('mapWithConcurrency', () => {
2
);

expect(processed).toHaveLength(5);
expect(processed.sort()).toEqual([1, 2, 3, 4, 5]);
assert.equal(processed.length, 5);
assert.deepEqual(processed.sort(), [1, 2, 3, 4, 5]);
});

it('should respect concurrency limit', async () => {
Expand All @@ -59,7 +60,7 @@ describe('mapWithConcurrency', () => {
2
);

expect(maxConcurrent).toBeLessThanOrEqual(2);
assert.ok(maxConcurrent <= 2, `Expected max concurrency <= 2, got ${maxConcurrent}`);
});

it('should handle async function results', async () => {
Expand All @@ -68,21 +69,22 @@ describe('mapWithConcurrency', () => {
await mapWithConcurrency(items, async item => item * 2, 2);

// Should complete without error
expect(true).toBe(true);
assert.ok(true);
});

it('should handle errors in processing', async () => {
let items = [1, 2, 3];

await expect(
await assert.rejects(
mapWithConcurrency(
items,
async item => {
if (item === 2) throw new Error('Test error');
return item;
},
2
)
).rejects.toThrow('Test error');
),
{ message: 'Test error' }
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Tests for configuration functions
*/

import { describe, expect, it } from 'vitest';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
defaultConfig,
loadConfig,
Expand All @@ -16,8 +17,8 @@ describe('parseCliOptions', () => {
let options = { viewports: 'mobile:375x667,desktop:1920x1080' };
let config = parseCliOptions(options);

expect(config.viewports).toHaveLength(2);
expect(config.viewports[0]).toEqual({
assert.equal(config.viewports.length, 2);
assert.deepEqual(config.viewports[0], {
name: 'mobile',
width: 375,
height: 667,
Expand All @@ -28,22 +29,22 @@ describe('parseCliOptions', () => {
let options = { viewports: 'mobile:375x667,invalid,desktop:1920x1080' };
let config = parseCliOptions(options);

expect(config.viewports).toHaveLength(2);
assert.equal(config.viewports.length, 2);
});

it('should parse concurrency', () => {
let options = { concurrency: 5 };
let config = parseCliOptions(options);

expect(config.concurrency).toBe(5);
assert.equal(config.concurrency, 5);
});

it('should parse include and exclude', () => {
let options = { include: 'button*', exclude: '*test*' };
let config = parseCliOptions(options);

expect(config.include).toBe('button*');
expect(config.exclude).toBe('*test*');
assert.equal(config.include, 'button*');
assert.equal(config.exclude, '*test*');
});

it('should parse browser options', () => {
Expand All @@ -53,15 +54,15 @@ describe('parseCliOptions', () => {
};
let config = parseCliOptions(options);

expect(config.browser.headless).toBe(false);
expect(config.browser.args).toEqual(['--arg1', '--arg2']);
assert.equal(config.browser.headless, false);
assert.deepEqual(config.browser.args, ['--arg1', '--arg2']);
});

it('should parse screenshot options', () => {
let options = { fullPage: true };
let config = parseCliOptions(options);

expect(config.screenshot.fullPage).toBe(true);
assert.equal(config.screenshot.fullPage, true);
});
});

Expand All @@ -72,8 +73,8 @@ describe('mergeConfigs', () => {

let merged = mergeConfigs(config1, config2);

expect(merged.concurrency).toBe(5);
expect(merged.include).toBe('button*');
assert.equal(merged.concurrency, 5);
assert.equal(merged.include, 'button*');
});

it('should deep merge browser config', () => {
Expand All @@ -82,8 +83,8 @@ describe('mergeConfigs', () => {

let merged = mergeConfigs(config1, config2);

expect(merged.browser.headless).toBe(false);
expect(merged.browser.args).toEqual(['--arg1']);
assert.equal(merged.browser.headless, false);
assert.deepEqual(merged.browser.args, ['--arg1']);
});

it('should override arrays instead of concatenating', () => {
Expand All @@ -96,8 +97,8 @@ describe('mergeConfigs', () => {

let merged = mergeConfigs(config1, config2);

expect(merged.viewports).toHaveLength(1);
expect(merged.viewports[0].name).toBe('desktop');
assert.equal(merged.viewports.length, 1);
assert.equal(merged.viewports[0].name, 'desktop');
});

it('should handle null/undefined configs', () => {
Expand All @@ -108,7 +109,7 @@ describe('mergeConfigs', () => {
undefined
);

expect(merged.concurrency).toBe(5);
assert.equal(merged.concurrency, 5);
});
});

Expand All @@ -125,15 +126,15 @@ describe('mergeStoryConfig', () => {

let merged = mergeStoryConfig(globalConfig, storyConfig);

expect(merged.viewports[0].name).toBe('mobile');
expect(merged.screenshot.fullPage).toBe(false);
assert.equal(merged.viewports[0].name, 'mobile');
assert.equal(merged.screenshot.fullPage, false);
});

it('should return global config if no story config', () => {
let globalConfig = { concurrency: 3 };
let merged = mergeStoryConfig(globalConfig, null);

expect(merged).toEqual(globalConfig);
assert.deepEqual(merged, globalConfig);
});

it('should merge beforeScreenshot hook', () => {
Expand All @@ -143,7 +144,7 @@ describe('mergeStoryConfig', () => {

let merged = mergeStoryConfig(globalConfig, storyConfig);

expect(merged.beforeScreenshot).toBe(hook);
assert.equal(merged.beforeScreenshot, hook);
});
});

Expand All @@ -158,9 +159,9 @@ describe('loadConfig', () => {

let config = await loadConfig('./storybook-static', {}, vizzlyConfig);

expect(config.concurrency).toBe(10);
expect(config.viewports).toHaveLength(1);
expect(config.viewports[0].name).toBe('custom');
assert.equal(config.concurrency, 10);
assert.equal(config.viewports.length, 1);
assert.equal(config.viewports[0].name, 'custom');
});

it('should prefer CLI options over vizzlyConfig.storybook', async () => {
Expand All @@ -178,16 +179,16 @@ describe('loadConfig', () => {
vizzlyConfig
);

expect(config.concurrency).toBe(5);
expect(config.include).toBe('from-cli');
assert.equal(config.concurrency, 5);
assert.equal(config.include, 'from-cli');
});

it('should use defaults when no vizzlyConfig provided', async () => {
let config = await loadConfig('./storybook-static', {}, {});

expect(config.concurrency).toBe(defaultConfig.concurrency);
expect(config.viewports).toHaveLength(2);
expect(config.viewports).toEqual(defaultConfig.viewports);
assert.equal(config.concurrency, defaultConfig.concurrency);
assert.equal(config.viewports.length, 2);
assert.deepEqual(config.viewports, defaultConfig.viewports);
});

it('should handle missing storybook key in vizzlyConfig', async () => {
Expand All @@ -197,7 +198,7 @@ describe('loadConfig', () => {

let config = await loadConfig('./storybook-static', {}, vizzlyConfig);

expect(config.concurrency).toBe(defaultConfig.concurrency);
assert.equal(config.concurrency, defaultConfig.concurrency);
});

it('should deep merge browser config from vizzlyConfig', async () => {
Expand All @@ -212,13 +213,13 @@ describe('loadConfig', () => {

let config = await loadConfig('./storybook-static', {}, vizzlyConfig);

expect(config.browser.headless).toBe(false);
expect(config.browser.args).toEqual(['--disable-gpu']);
assert.equal(config.browser.headless, false);
assert.deepEqual(config.browser.args, ['--disable-gpu']);
});

it('should set storybookPath from argument', async () => {
let config = await loadConfig('/path/to/storybook', {}, {});

expect(config.storybookPath).toBe('/path/to/storybook');
assert.equal(config.storybookPath, '/path/to/storybook');
});
});
Loading