Vitest Browser Mode Setup
apps/docs/src/content/docs/components/testing/vitest-setup Click to copy apps/docs/src/content/docs/components/testing/vitest-setup Vitest Browser Mode Setup
Section titled “Vitest Browser Mode Setup”Testing web components requires a real browser environment. Shadow DOM behavior, focus management, form participation, and accessibility features cannot be accurately tested in synthetic DOM environments like jsdom or happy-dom. HELiX uses Vitest browser mode with Playwright to run tests in actual Chromium, ensuring behavior matches production.
This guide covers the complete Vitest browser mode setup: configuration structure, provider selection, headless vs headed testing, test environment configuration, and the production-ready setup used in hx-library.
Reading note: The configuration samples below are illustrative — the shipped Vitest 3 Playwright provider has a slightly different option shape than the snippets show, browser mode is headless by default (no
--headedCLI flag on the current Vitest browser CLI), coverage is disabled by default inpackages/hx-library/vitest.config.ts(the active blocking threshold when enabled is 50%, not 80%), and the Browser Context API exposesuserEvent/page.elementLocatorrather thanpage.click/page.fill/page.keyboard. Use the recipes here as a pattern catalog and consult the livepackages/hx-library/vitest.config.tsfor the canonical shape. The internal “Test Theater” page in the Admin Dashboard reads from generated artifacts underapps/admin/.cache/and.reports/— the.cache/test-results.jsonpath mentioned earlier in some drafts is one of several inputs, not the single source.
Why Browser Mode?
Section titled “Why Browser Mode?”Vitest browser mode runs tests in a real browser, not a Node.js environment with a simulated DOM. This provides critical advantages for web component testing:
Real Browser Testing vs Synthetic DOM
Section titled “Real Browser Testing vs Synthetic DOM”| Feature | Browser Mode | jsdom/happy-dom |
|---|---|---|
| Shadow DOM | Native browser implementation | Simulated, incomplete |
| Focus management | Real focus behavior | Approximated |
| Form participation | ElementInternals, formdata events | Not supported |
| ARIA | Real screen reader behavior | Simulated |
| CSS encapsulation | Full Shadow DOM isolation | Partial |
| Custom events | Native bubbling/composed | Simulated |
| Keyboard events | Real browser dispatch | Synthetic |
Healthcare Mandate
Section titled “Healthcare Mandate”In enterprise healthcare environments, untested behavior is unacceptable. Testing in a synthetic DOM environment creates false confidence. Browser mode testing catches:
- Shadow DOM slot projection edge cases
- Focus trap behavior in modals
- Form validation and submission flows
- Keyboard navigation across shadow boundaries
- ARIA attribute implementation accuracy
If it doesn’t work in a real browser, it doesn’t work. Browser mode ensures production parity.
Installation
Section titled “Installation”Vitest browser mode requires three packages:
npm install -D vitest @vitest/browser playwrightPackage breakdown:
vitest— Core test runner with browser mode support@vitest/browser— Browser mode plugin and context APIplaywright— Browser automation provider (Chromium, Firefox, WebKit)
Provider Options
Section titled “Provider Options”Vitest supports three browser providers:
playwright(recommended) — Full browser automation, Chrome DevTools Protocol, headless supportwebdriverio— Cross-browser testing, W3C WebDriver standardpreview(default) — Vite dev server preview, no headless support, simulated events only
HELiX uses Playwright for its robust Chrome DevTools Protocol integration, headless mode support, and accurate event dispatch.
Configuration Structure
Section titled “Configuration Structure”Basic vitest.config.ts
Section titled “Basic vitest.config.ts”A minimal browser mode configuration:
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { browser: { enabled: true, provider: 'playwright', instances: [{ browser: 'chromium' }], }, },});Required fields:
browser.enabled— Activates browser mode (defaults tofalse)browser.provider— Which browser automation provider to usebrowser.instances— Array of browser configurations (must have at least one)
Browser Instances
Section titled “Browser Instances”The instances array defines which browsers to test against. Each instance is a separate browser context:
instances: [{ browser: 'chromium' }, { browser: 'firefox' }, { browser: 'webkit' }];Supported browsers (Playwright):
'chromium'— Chrome/Edge engine (recommended for web components)'firefox'— Firefox engine'webkit'— Safari engine
Best practice: Use Chromium for local development, add Firefox/WebKit for cross-browser validation in CI.
Headless vs Headed Mode
Section titled “Headless vs Headed Mode”Headless mode runs the browser in the background without a visible UI. Headed mode opens a visible browser window.
browser: { headless: true, // Headless mode (CI default)}When to use each mode:
| Mode | Use Case | Command |
|---|---|---|
| Headless | CI pipelines, automated runs | npm run test |
| Headed | Debugging, watching tests locally | npm run test:watch |
Important: The preview provider does not support headless mode. Always use playwright or webdriverio for CI.
Headless Mode in CI
Section titled “Headless Mode in CI”Vitest automatically enables headless mode in CI environments (detected via process.env.CI). You can also force it:
browser: { headless: process.env.CI === 'true', // Auto-enable in CI}Viewing UI in Headless Mode
Section titled “Viewing UI in Headless Mode”If you want to use Vitest’s UI while running tests headlessly:
npm install -D @vitest/uinpx vitest --ui --browser.headlessThe browser runs headlessly, but Vitest UI displays test results in a web interface.
Provider Configuration
Section titled “Provider Configuration”Playwright Provider
Section titled “Playwright Provider”The Playwright provider offers advanced configuration options:
import { defineConfig } from 'vitest/config';import { playwright } from '@vitest/browser/providers/playwright';
export default defineConfig({ test: { browser: { provider: playwright({ // Launch options passed to playwright.chromium.launch() launchOptions: { slowMo: 50, // Slow down operations by 50ms (useful for debugging) channel: 'chrome-beta', // Use Chrome Beta instead of Chromium devtools: true, // Open DevTools automatically }, // Timeout for browser actions (clicks, fills, etc.) actionTimeout: 5000, // 5 seconds (default: 10000) }), enabled: true, instances: [{ browser: 'chromium' }], }, },});Common launch options:
slowMo— Delay all operations by N milliseconds (debugging)channel— Use a different Chromium build (chrome,chrome-beta,msedge)devtools— Auto-open Chrome DevTools (headed mode only)headless— Override instance-level headless setting
Common action options:
actionTimeout— Max time to wait for actions likepage.click()(default: 10s)navigationTimeout— Max time to wait for page loads (default: 30s)
Per-Instance Configuration
Section titled “Per-Instance Configuration”You can override provider settings per browser instance:
instances: [ { browser: 'chromium', headless: false, // Open visible Chromium }, { browser: 'firefox', headless: true, // Run Firefox headlessly },];Test Environment Configuration
Section titled “Test Environment Configuration”File Matching
Section titled “File Matching”Use include to specify which files contain tests:
test: { include: ['src/components/**/*.test.ts'],}Pattern matching:
**/*.test.ts— All files ending in.test.tssrc/components/**/*.spec.ts— Only.spec.tsfiles insrc/components/tests/**/*.{test,spec}.{js,ts}— Multiple patterns and extensions
Globals API
Section titled “Globals API”Vitest can inject globals like describe, it, expect without explicit imports:
test: { globals: true, // Auto-inject describe, it, expect, vi}With globals enabled:
// No imports neededdescribe('hx-button', () => { it('renders', async () => { expect(true).toBe(true); });});Without globals (explicit imports):
import { describe, it, expect } from 'vitest';
describe('hx-button', () => { it('renders', async () => { expect(true).toBe(true); });});HELiX uses explicit imports to avoid TypeScript configuration complexity and improve IDE autocomplete.
Test Isolation
Section titled “Test Isolation”Vitest runs all browser tests in a single page with test isolation via iframes:
- One browser instance per
instancesentry - One page opened per browser
- Each test file runs in its own iframe within that page
- Tests are isolated from each other automatically
Cleanup best practice: Use afterEach(cleanup) to remove fixtures between tests:
import { afterEach } from 'vitest';import { cleanup } from '../../test-utils.js';
afterEach(cleanup);Viewport Configuration
Section titled “Viewport Configuration”Configure the default viewport size:
browser: { viewport: { width: 1280, height: 720, },}Override per test:
import { page } from '@vitest/browser/context';
it('renders on mobile', async () => { await page.viewport(375, 667); // iPhone SE const el = await fixture('<hx-button>Click</hx-button>'); // Test mobile layout});Reporting and Coverage
Section titled “Reporting and Coverage”Test Reporters
Section titled “Test Reporters”Vitest supports multiple test reporters:
test: { reporters: ['verbose', 'json'],}Common reporters:
'default'— Basic console output'verbose'— Detailed test names and timing'json'— JSON output for CI/CD integration'html'— HTML test results report'junit'— JUnit XML format (Jenkins, CircleCI)'dot'— Minimal dot output
JSON Output
Section titled “JSON Output”Use outputFile to write reporter output to disk:
test: { reporters: ['verbose', 'json'], outputFile: { json: '.cache/test-results.json', },}HELiX uses this for the Admin Dashboard Test Theater, which reads .cache/test-results.json to display live test results.
Coverage Configuration
Section titled “Coverage Configuration”Vitest integrates with @vitest/coverage-v8 for code coverage:
npm install -D @vitest/coverage-v8Coverage configuration:
test: { coverage: { provider: 'v8', // Use V8 coverage (faster than Istanbul) enabled: true, // Auto-enable coverage include: ['src/components/**/*.ts'], // Only track component code exclude: [ 'src/components/**/*.test.ts', // Exclude test files 'src/components/**/*.stories.ts', // Exclude Storybook stories 'src/components/**/*.styles.ts', // Exclude style templates 'src/components/**/index.ts', // Exclude re-exports ], reporter: ['text', 'json-summary'], // Console + JSON output reportsDirectory: '.cache/coverage', // Coverage output dir },}Coverage providers:
v8— V8 JavaScript engine coverage (faster, more accurate)istanbul— Istanbul coverage (more configurable, slower)
Coverage Thresholds
Section titled “Coverage Thresholds”Enforce minimum coverage percentages:
coverage: { thresholds: { lines: 80, functions: 80, branches: 80, statements: 80, },}When coverage is enabled, the active blocking threshold is 50% (see coverage-config.json and the pnpm test:smart coverage path). Coverage is reported as informational on every CI run today but is not gating the merge until the #1556 coverage-config follow-up lands and the threshold is raised.
HELiX Production Configuration
Section titled “HELiX Production Configuration”Complete vitest.config.ts
Section titled “Complete vitest.config.ts”The production configuration used in packages/hx-library:
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { // ─── Browser Mode ─── browser: { enabled: true, provider: 'playwright', headless: true, // Headless in CI, headed with --headed flag instances: [{ browser: 'chromium' }], },
// ─── Test File Matching ─── include: ['src/components/**/*.test.ts'],
// ─── Reporting ─── reporters: ['verbose', 'json'], outputFile: { json: '.cache/test-results.json' },
// ─── Globals ─── globals: true, // Auto-inject describe, it, expect
// ─── Coverage ─── coverage: { provider: 'v8', enabled: true, include: ['src/components/**/*.ts'], exclude: [ 'src/components/**/*.test.ts', 'src/components/**/*.stories.ts', 'src/components/**/*.styles.ts', 'src/components/**/index.ts', ], reporter: ['text', 'json-summary'], reportsDirectory: '.cache/coverage', }, },});Package.json Scripts
Section titled “Package.json Scripts”Test scripts in packages/hx-library/package.json:
{ "scripts": { "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage" }}Script breakdown:
npm run test— Run all tests once (CI mode)npm run test:watch— Watch mode, re-run on file changesnpm run test:ui— Open Vitest UI in browsernpm run test:coverage— Run tests + generate coverage report
Dependencies
Section titled “Dependencies”Required devDependencies:
{ "devDependencies": { "vitest": "^3.0.0", "@vitest/browser": "^3.0.0", "@vitest/coverage-v8": "^3.2.4", "playwright": "^1.50.0" }}Test Execution Flow
Section titled “Test Execution Flow”Local Development
Section titled “Local Development”Headed mode with watch:
npm run test:watch- Opens visible Chromium window
- Watches for file changes
- Re-runs tests on save
- Displays Vitest UI at http://localhost:63315
Headless mode (CI simulation):
npm run test- Runs Chromium headlessly
- Runs all tests once
- Outputs results to console and
.cache/test-results.json - Exits with code 0 (pass) or 1 (fail)
CI Pipeline
Section titled “CI Pipeline”In GitHub Actions or other CI environments:
- name: Run tests run: npm run test env: CI: trueVitest automatically:
- Enables headless mode (detects
CI=true) - Runs all tests once
- Generates JSON output to
.cache/test-results.json - Generates coverage report to
.cache/coverage/ - Reports coverage as informational (50% threshold when enabled; not currently a blocking gate — see #1556)
Browser Context API
Section titled “Browser Context API”Vitest exposes the Playwright page object for advanced interactions:
import { page } from '@vitest/browser/context';
it('handles keyboard navigation', async () => { const el = await fixture('<hx-button>Click</hx-button>'); const btn = shadowQuery(el, 'button')!;
await page.keyboard.press('Tab'); // Tab to button await page.keyboard.press('Enter'); // Activate button
// Assert event fired});Available APIs:
page.click(selector)— Click an elementpage.fill(selector, value)— Fill an inputpage.keyboard.press(key)— Simulate keyboard inputpage.viewport(width, height)— Change viewport sizepage.screenshot()— Take a screenshot (debugging)
Common Patterns
Section titled “Common Patterns”Debugging Tests
Section titled “Debugging Tests”Run single test file in headed mode:
npx vitest run src/components/hx-button/hx-button.test.ts --headedEnable slow motion (delay operations):
browser: { provider: playwright({ launchOptions: { slowMo: 100 }, // 100ms delay per action }),}Take screenshots on failure:
import { page } from '@vitest/browser/context';
it('renders correctly', async () => { const el = await fixture('<hx-button>Click</hx-button>'); await page.screenshot({ path: 'debug.png' }); expect(el.shadowRoot).toBeTruthy();});Running Specific Tests
Section titled “Running Specific Tests”Run only tests matching pattern:
npx vitest run --grep "hx-button"Run only tests in specific file:
npx vitest run src/components/hx-button/hx-button.test.tsRun only tests with .only():
it.only('runs this test', async () => { // Only this test runs});Cross-Browser Testing
Section titled “Cross-Browser Testing”Test against multiple browsers in CI:
browser: { instances: [ { browser: 'chromium' }, { browser: 'firefox' }, { browser: 'webkit' }, // Safari ],}CI matrix testing:
strategy: matrix: browser: [chromium, firefox, webkit]
steps: - run: npx vitest --browser.instances[0].browser=${{ matrix.browser }}Troubleshooting
Section titled “Troubleshooting”Tests Run in Node.js Instead of Browser
Section titled “Tests Run in Node.js Instead of Browser”Symptom: document is not defined or window is not defined errors.
Cause: browser.enabled: false or missing browser config.
Fix:
test: { browser: { enabled: true, // Must be true provider: 'playwright', instances: [{ browser: 'chromium' }], },}Playwright Fails to Launch Browser
Section titled “Playwright Fails to Launch Browser”Symptom: browserType.launch: Executable doesn't exist error.
Cause: Playwright browsers not installed.
Fix:
npx playwright install chromiumOr install all browsers:
npx playwright installHeadless Mode Not Working
Section titled “Headless Mode Not Working”Symptom: Browser window opens even with headless: true.
Cause: Using preview provider (does not support headless).
Fix: Switch to playwright or webdriverio:
browser: { provider: 'playwright', // Not 'preview' headless: true,}Coverage Not Generating
Section titled “Coverage Not Generating”Symptom: No coverage report in .cache/coverage/.
Cause: coverage.enabled: false or missing @vitest/coverage-v8.
Fix:
npm install -D @vitest/coverage-v8coverage: { enabled: true, provider: 'v8',}Tests Fail in CI But Pass Locally
Section titled “Tests Fail in CI But Pass Locally”Symptom: Tests pass in headed mode but fail in headless CI.
Cause: Timing issues or viewport size differences.
Fix: Use await updateComplete and oneEvent() for async operations. Avoid hardcoded timeouts:
// BAD: Timing-dependentawait new Promise((resolve) => setTimeout(resolve, 100));expect(el.getAttribute('aria-expanded')).toBe('true');
// GOOD: Wait for actual conditionawait el.updateComplete;expect(el.getAttribute('aria-expanded')).toBe('true');Performance Considerations
Section titled “Performance Considerations”Test Execution Speed
Section titled “Test Execution Speed”Browser mode is slower than jsdom because it launches a real browser. Optimize with:
- Shared browser instance — Vitest reuses one browser per
instancesentry - Parallel test files — Vitest runs multiple test files in parallel (default)
- Headless mode — Slightly faster than headed mode
- Minimal fixtures — Only create necessary DOM elements per test
CI Optimization
Section titled “CI Optimization”GitHub Actions example:
- name: Install Playwright browsers run: npx playwright install --with-deps chromium
- name: Run tests run: npm run test env: CI: trueOnly install Chromium (not all browsers) to save CI time.
Next Steps
Section titled “Next Steps”Now that you understand Vitest browser mode configuration, learn how to write tests:
- Storybook standards — canonical Storybook story patterns for HELiX components (the per-page testing guides referenced in earlier drafts haven’t been written yet)
- Testing Extended Components — Vitest + axe-core patterns for consumer packages extending HELiX
Sources
Section titled “Sources”This documentation references official Vitest browser mode documentation: