diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 543d5c579b4f..d627fa416497 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -1,6 +1,7 @@ import { existsSync } from 'node:fs'; import * as fs from 'node:fs/promises'; import { writeFile } from 'node:fs/promises'; +import os from 'node:os'; import { babelParse, generate, traverse } from 'storybook/internal/babel'; import { AddonVitestService } from 'storybook/internal/cli'; @@ -165,9 +166,17 @@ export default async function postInstall(options: PostinstallOptions) { useRemotePkg: !!options.skipInstall, }); } else { + const platform = os.platform(); + const useWithDeps = platform === 'darwin' || platform === 'win32'; + const manualCommand = useWithDeps + ? 'npx playwright install chromium --with-deps' + : 'npx playwright install chromium'; + const linuxNote = !useWithDeps + ? '\n Note: add --with-deps to the command above if you are on Debian or Ubuntu.' + : ''; logger.warn(dedent` Playwright browser binaries installation skipped. Please run the following command manually later: - ${CLI_COLORS.cta('npx playwright install chromium --with-deps')} + ${CLI_COLORS.cta(manualCommand)}${linuxNote} `); } } diff --git a/code/core/src/cli/AddonVitestService.test.ts b/code/core/src/cli/AddonVitestService.test.ts index 17379adac94a..e54f5b54f907 100644 --- a/code/core/src/cli/AddonVitestService.test.ts +++ b/code/core/src/cli/AddonVitestService.test.ts @@ -1,4 +1,5 @@ import * as fs from 'node:fs/promises'; +import os from 'node:os'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -14,6 +15,7 @@ import { SupportedBuilder, SupportedFramework } from '../types'; import { AddonVitestService } from './AddonVitestService'; vi.mock('node:fs/promises', { spy: true }); +vi.mock('node:os', { spy: true }); vi.mock('storybook/internal/common', { spy: true }); vi.mock('storybook/internal/node-logger', { spy: true }); vi.mock('empathic/find', { spy: true }); @@ -391,7 +393,7 @@ describe('AddonVitestService', () => { vi.mocked(logger.warn).mockImplementation(() => {}); // Mock getPackageCommand to return a string vi.mocked(mockPackageManager.getPackageCommand).mockReturnValue( - 'npx playwright install chromium --with-deps' + 'npx playwright install chromium' ); }); @@ -416,25 +418,127 @@ describe('AddonVitestService', () => { }); it('should execute playwright install command', async () => { - type ChildProcessFactory = (signal?: AbortSignal) => ResultPromise; - let commandFactory: ChildProcessFactory | ChildProcessFactory[]; - vi.mocked(prompt.confirm).mockResolvedValue(true); - vi.mocked(prompt.executeTaskWithSpinner).mockImplementation( - async (factory: ChildProcessFactory | ChildProcessFactory[]) => { - commandFactory = Array.isArray(factory) ? factory[0] : factory; - // Simulate the child process completion - commandFactory(); + const originalCI = process.env.CI; + delete process.env.CI; + vi.mocked(os.platform).mockReturnValue('linux'); + try { + type ChildProcessFactory = (signal?: AbortSignal) => ResultPromise; + let commandFactory: ChildProcessFactory | ChildProcessFactory[]; + vi.mocked(prompt.confirm).mockResolvedValue(true); + vi.mocked(prompt.executeTaskWithSpinner).mockImplementation( + async (factory: ChildProcessFactory | ChildProcessFactory[]) => { + commandFactory = Array.isArray(factory) ? factory[0] : factory; + // Simulate the child process completion + commandFactory(); + } + ); + + await service.installPlaywright(); + + expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith({ + args: ['playwright', 'install', 'chromium'], + signal: undefined, + stdio: ['inherit', 'pipe', 'pipe'], + }); + } finally { + if (originalCI !== undefined) { + process.env.CI = originalCI; } - ); - - await service.installPlaywright(); - - expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith({ - args: ['playwright', 'install', 'chromium', '--with-deps'], - signal: undefined, - stdio: ['inherit', 'pipe', 'pipe'], - }); - }); + } + }); + + it('should warn about missing system dependencies after install on Linux', async () => { + const originalCI = process.env.CI; + delete process.env.CI; + vi.mocked(os.platform).mockReturnValue('linux'); + try { + type ChildProcessFactory = (signal?: AbortSignal) => ResultPromise; + vi.mocked(prompt.confirm).mockResolvedValue(true); + vi.mocked(prompt.executeTaskWithSpinner).mockImplementation( + async (factory: ChildProcessFactory | ChildProcessFactory[]) => { + const commandFactory = Array.isArray(factory) ? factory[0] : factory; + commandFactory(); + } + ); + + const { result } = await service.installPlaywright(); + + expect(result).toBe('installed'); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('installed without system dependencies') + ); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('run Storybook Test from the Storybook UI') + ); + } finally { + if (originalCI !== undefined) { + process.env.CI = originalCI; + } + } + }); + + it('should execute playwright install command with --with-deps in CI', async () => { + const originalCI = process.env.CI; + process.env.CI = 'true'; + vi.mocked(os.platform).mockReturnValue('linux'); + try { + type ChildProcessFactory = (signal?: AbortSignal) => ResultPromise; + let commandFactory: ChildProcessFactory | ChildProcessFactory[]; + vi.mocked(prompt.confirm).mockResolvedValue(true); + vi.mocked(prompt.executeTaskWithSpinner).mockImplementation( + async (factory: ChildProcessFactory | ChildProcessFactory[]) => { + commandFactory = Array.isArray(factory) ? factory[0] : factory; + commandFactory(); + } + ); + + await service.installPlaywright(); + + expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith({ + args: ['playwright', 'install', 'chromium', '--with-deps'], + signal: undefined, + stdio: ['inherit', 'pipe', 'pipe'], + }); + } finally { + if (originalCI === undefined) { + delete process.env.CI; + } else { + process.env.CI = originalCI; + } + } + }); + + it.each(['darwin', 'win32'] as const)( + 'should execute playwright install command with --with-deps on %s', + async (platform) => { + const originalCI = process.env.CI; + delete process.env.CI; + vi.mocked(os.platform).mockReturnValue(platform); + try { + type ChildProcessFactory = (signal?: AbortSignal) => ResultPromise; + let commandFactory: ChildProcessFactory | ChildProcessFactory[]; + vi.mocked(prompt.confirm).mockResolvedValue(true); + vi.mocked(prompt.executeTaskWithSpinner).mockImplementation( + async (factory: ChildProcessFactory | ChildProcessFactory[]) => { + commandFactory = Array.isArray(factory) ? factory[0] : factory; + commandFactory(); + } + ); + + await service.installPlaywright(); + + expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith({ + args: ['playwright', 'install', 'chromium', '--with-deps'], + signal: undefined, + stdio: ['inherit', 'pipe', 'pipe'], + }); + } finally { + if (originalCI !== undefined) { + process.env.CI = originalCI; + } + } + } + ); it('should capture error stack when installation fails', async () => { const error = new Error('Installation failed'); diff --git a/code/core/src/cli/AddonVitestService.ts b/code/core/src/cli/AddonVitestService.ts index 2212f74b9b4d..9fd7fc8c964f 100644 --- a/code/core/src/cli/AddonVitestService.ts +++ b/code/core/src/cli/AddonVitestService.ts @@ -1,4 +1,5 @@ import fs from 'node:fs/promises'; +import os from 'node:os'; import * as babel from 'storybook/internal/babel'; import type { JsPackageManager } from 'storybook/internal/common'; @@ -106,7 +107,10 @@ export class AddonVitestService { /** * Install Playwright browser binaries for @storybook/addon-vitest * - * Installs Chromium with dependencies via `npx playwright install chromium --with-deps` + * Installs Chromium via `npx playwright install chromium`. In CI environments and on + * macOS/Windows (officially supported platforms), also installs system-level browser dependencies + * via `--with-deps`. On other platforms (e.g. Linux), `--with-deps` is omitted to avoid requiring + * `sudo` — system packages are typically managed by the distro package manager. * * @param packageManager - The package manager to use for installation * @param prompt - The prompt instance for displaying progress @@ -123,7 +127,11 @@ export class AddonVitestService { ): Promise<{ errors: string[]; result: 'installed' | 'skipped' | 'aborted' | 'failed' }> { const errors: string[] = []; - const playwrightCommand = ['playwright', 'install', 'chromium', '--with-deps']; + const platform = os.platform(); + const useWithDeps = !!process.env.CI || platform === 'darwin' || platform === 'win32'; + const playwrightCommand = useWithDeps + ? ['playwright', 'install', 'chromium', '--with-deps'] + : ['playwright', 'install', 'chromium']; const playwrightCommandString = this.packageManager.getPackageCommand(playwrightCommand); let result: 'installed' | 'skipped' | 'aborted' | 'failed'; @@ -168,6 +176,14 @@ export class AddonVitestService { result = 'aborted'; } else { result = 'installed'; + if (!useWithDeps) { + logger.warn(dedent` + Playwright was installed without system dependencies. Depending on your operating system, you may need to install additional libraries for Playwright to work correctly. + To check for missing dependencies, run Storybook Test from the Storybook UI — it will report any libraries that need to be installed. + On MacOS, Windows, Debian and Ubuntu, you can install system dependencies manually by running: + ${CLI_COLORS.cta(this.packageManager.getPackageCommand(['playwright', 'install', 'chromium', '--with-deps']))} + `); + } } } else { logger.warn('Playwright installation skipped');