Files
stack/v1/packages/mosaic/framework/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md
T

5.5 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Choose Browser-Based Runner for Style and DOM Event Testing MEDIUM Node-based runners cannot test real CSS behavior, native DOM events, cookies, or computed styles capability
vue3
testing
component-testing
vitest
browser
jsdom

Choose Browser-Based Runner for Style and DOM Event Testing

Impact: MEDIUM - Node-based test runners (Vitest with jsdom/happy-dom) simulate the DOM but cannot test real CSS rendering, native browser events, cookies, computed styles, or cross-browser behavior. Use browser-based runners when these matter.

Use Vitest for most component tests (fast), but use Vitest Browser Mode when testing visual/DOM-dependent features.

Task Checklist

  • Use Vitest (node) for logic-focused component tests
  • Use Vitest Browser Mode for style-dependent tests
  • Use Vitest Browser Mode for native events (focus, drag, resize)
  • Use Vitest Browser Mode for cookies and computed CSS styles
  • Accept slower speed tradeoff for browser accuracy

When to Use Each Approach

Node-Based Runner (Vitest + happy-dom/jsdom)

Best for:

  • Pure logic testing
  • State management
  • Event emission
  • Props/slots behavior
  • Most component interactions
  • Fast CI/CD pipelines
// vitest.config.js
export default defineConfig({
  test: {
    environment: 'happy-dom', // or 'jsdom'
  },
});
// Fast but limited - fine for most tests
test('button emits click event', async () => {
  const wrapper = mount(Button);
  await wrapper.trigger('click');
  expect(wrapper.emitted('click')).toBeTruthy();
});

Vitest Browser Mode

Required for:

  • CSS computed styles verification
  • CSS transitions/animations
  • Real focus/blur behavior
  • Drag and drop
  • Cookie operations
  • Viewport-dependent behavior
  • Cross-browser validation

Vitest Browser Mode Setup

npm install -D @vitest/browser playwright
// vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      name: 'chromium',
      provider: 'playwright',
    },
  },
});
// Button.browser.test.js
import { render } from 'vitest-browser-vue';
import Button from './Button.vue';

test('has correct hover styling', async () => {
  const { getByRole } = render(Button, { props: { label: 'Click me' } });

  const button = getByRole('button');

  // Check initial style
  await expect.element(button).toHaveStyle({
    backgroundColor: 'rgb(59, 130, 246)', // blue
  });
});

test('maintains focus after click', async () => {
  const { getByRole } = render(Button);

  const button = getByRole('button');
  await button.click();

  await expect.element(button).toHaveFocus();
});

Examples: What Each Runner Can/Cannot Test

Styles - Browser Required

// Node runner: CANNOT verify actual CSS
test('danger button has red background', () => {
  const wrapper = mount(Button, { props: { variant: 'danger' } });
  // This only checks class exists, not actual color
  expect(wrapper.classes()).toContain('bg-red-500');
});

// Vitest Browser Mode: CAN verify computed styles
test('danger button renders red', async () => {
  const { getByRole } = render(Button, { props: { variant: 'danger' } });
  await expect.element(getByRole('button')).toHaveStyle({
    backgroundColor: 'rgb(239, 68, 68)',
  });
});

Computed CSS Styles - Browser Required

// Node runner: CANNOT get real computed styles
test('button has correct padding', () => {
  const wrapper = mount(Button);
  // getComputedStyle returns empty/default values in jsdom
  const style = window.getComputedStyle(wrapper.element);
  // style.padding will be empty string, not actual computed value
});

// Vitest Browser Mode: Real computed styles
test('button has correct padding', async () => {
  const { getByRole } = render(Button);
  const button = getByRole('button');

  await expect.element(button).toHaveStyle({
    padding: '12px 24px',
  });
});

Native Events - Browser Required

// Node runner: Synthetic events only
test('handles drag and drop', async () => {
  const wrapper = mount(DraggableList);
  // trigger('dragstart') is synthetic - may not work as expected
  await wrapper.find('.item').trigger('dragstart');
});

// Vitest Browser Mode: Real native events via userEvent
import { userEvent } from '@vitest/browser/context';

test('reorders items on drag', async () => {
  const { getByTestId } = render(DraggableList);

  const item = getByTestId('item-1');
  const target = getByTestId('item-3');

  await userEvent.dragAndDrop(item, target);

  // Assert reordering
});
// vitest.config.js - Separate test configurations

export default defineConfig({
  test: {
    // Default: Node environment for speed
    environment: 'happy-dom',

    // Browser tests in separate directory
    include: ['src/**/*.test.{js,ts}'],
  },
});

// Run browser tests separately
// npx vitest --browser.enabled

Directory Structure

tests/
├── unit/              # Fast node-based tests
│   ├── Button.test.js
│   └── useCounter.test.js
├── component/         # Slower browser-based tests
│   ├── Button.browser.test.js
│   └── DragDrop.browser.test.js
└── e2e/               # Full E2E tests (Playwright)
    └── user-flow.spec.ts

Reference