Migration Guide
This guide helps you migrate from the Stencil Test Runner to @stencil/vitest.
Why Migrate?
The integrated Stencil Test Runner is deprecated as of Stencil v4.43 and will be removed in Stencil v5. The @stencil/vitest package offers several advantages:
- Flexible DOM environments: Choose between mock-doc, jsdom, or happy-dom
- True browser testing: Run tests in real browsers, not just Puppeteer
- Modern test runner: Vitest provides faster test execution and better DX
- Better isolation: Test against actual build outputs rather than internal compilation
- Active maintenance: Vitest is actively developed with regular updates
Installation
First, install the new dependencies:
- npm
- Yarn
- pnpm
npm install --save-dev @stencil/vitest vitest
yarn add --dev @stencil/vitest vitest
pnpm add --save-dev @stencil/vitest vitest
For browser testing:
- npm
- Yarn
- pnpm
npm install --save-dev @vitest/browser-playwright
yarn add --dev @vitest/browser-playwright
pnpm add --save-dev @vitest/browser-playwright
Configuration Changes
Before: stencil.config.ts
// stencil.config.ts
export const config: Config = {
testing: {
testPathIgnorePatterns: ['node_modules'],
setupFilesAfterEnv: ['./test-setup.ts'],
},
};
After: vitest.config.ts
// vitest.config.ts
import { defineVitestConfig } from '@stencil/vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineVitestConfig({
stencilConfig: './stencil.config.ts',
test: {
projects: [
{
test: {
name: 'spec',
include: ['src/**/*.spec.{ts,tsx}'],
environment: 'stencil',
setupFiles: ['./vitest-setup.ts'],
},
},
{
test: {
name: 'browser',
include: ['src/**/*.e2e.{ts,tsx}'],
setupFiles: ['./vitest-setup.ts'],
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
},
],
},
});
Test File Changes
Spec Tests
Before:
import { newSpecPage } from '@stencil/core/testing';
import { MyComponent } from './my-component';
describe('my-component', () => {
it('renders', async () => {
const page = await newSpecPage({
components: [MyComponent],
html: '<my-component></my-component>',
});
expect(page.root).toEqualHtml(`
<my-component>
<mock:shadow-root>
<div>Hello, World!</div>
</mock:shadow-root>
</my-component>
`);
});
});
After:
import { render, h, describe, it, expect } from '@stencil/vitest';
describe('my-component', () => {
it('renders', async () => {
const { root } = await render(<my-component />);
await expect(root).toEqualHtml(`
<my-component>
<mock:shadow-root>
<div>Hello, World!</div>
</mock:shadow-root>
</my-component>
`);
});
});
E2E Tests
Before:
import { newE2EPage } from '@stencil/core/testing';
describe('my-component e2e', () => {
it('renders', async () => {
const page = await newE2EPage();
await page.setContent('<my-component></my-component>');
const element = await page.find('my-component');
expect(element).toHaveClass('hydrated');
});
it('handles click', async () => {
const page = await newE2EPage();
await page.setContent('<my-component></my-component>');
const spy = await page.spyOnEvent('myEvent');
const button = await page.find('my-component >>> button');
await button.click();
expect(spy).toHaveReceivedEvent();
});
});
After:
import { render, h, describe, it, expect } from '@stencil/vitest';
describe('my-component e2e', () => {
it('renders', async () => {
const { root } = await render(<my-component />);
expect(root).toHaveClass('hydrated');
});
it('handles click', async () => {
const { root, spyOnEvent, waitForChanges } = await render(<my-component />);
const spy = spyOnEvent('myEvent');
const button = root.shadowRoot?.querySelector('button');
button?.click();
await waitForChanges();
expect(spy).toHaveReceivedEvent();
});
});