⚠️ DEPRECATED: The built-in Stencil test runner is deprecated and only supports Jest v27-29. Please migrate to one of the recommended alternatives below.
Location: src/testing/
- Jest Stencil Runner: Updated Jest environment for Stencil
- WebdriverIO: Cross-browser e2e testing
- Playwright: Modern e2e testing framework
- Vitest: Fast unit testing with Vite
Note: This documentation is preserved for projects still using the deprecated test runner.
graph TD
subgraph "Test Runner"
Jest[Jest Core] --> Env[Stencil Environment]
Env --> MockDoc[Mock Document]
Env --> Transpile[TypeScript Transpilation]
end
subgraph "Test Types"
Unit[Unit Tests] --> Spec[Spec Tests]
E2E[E2E Tests] --> Puppeteer[Puppeteer]
end
subgraph "Test Utils"
Platform[Platform Mock]
Testing[Testing Utils]
Build[Build Context]
end
// stencil.config.ts
export const config: Config = {
testing: {
// Jest configuration
coverageDirectory: './coverage',
coverageReporters: ['json', 'lcov', 'text'],
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
// Test patterns
testMatch: ['**/*.(spec|e2e).ts?(x)'],
// Environment
testEnvironment: '@stencil/core/testing',
// Transforms
transform: {
'^.+\\.(ts|tsx|js|jsx|css)$': '@stencil/core/testing'
},
// Module resolution
moduleNameMapper: {
'@utils': '<rootDir>/src/utils'
}
}
};Component logic testing:
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>
<div>Hello, World!</div>
</my-component>
`);
});
it('updates on prop change', async () => {
const page = await newSpecPage({
components: [MyComponent],
html: '<my-component name="Test"></my-component>'
});
expect(page.rootInstance.name).toBe('Test');
page.root.name = 'Updated';
await page.waitForChanges();
expect(page.root.textContent).toContain('Updated');
});
});Browser-based testing:
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('emits events', async () => {
const page = await newE2EPage();
await page.setContent('<my-component></my-component>');
const eventSpy = await page.spyOnEvent('myEvent');
const button = await page.find('my-component >>> button');
await button.click();
expect(eventSpy).toHaveReceivedEvent();
expect(eventSpy).toHaveReceivedEventDetail({ value: 42 });
});
});Platform API mocking:
import { mockDocument, mockWindow } from '@stencil/core/testing';
const win = mockWindow();
const doc = mockDocument();
// Mock APIs
win.localStorage.setItem('key', 'value');
doc.createElement('div');Helper functions:
// Wait for async updates
await waitForChanges();
// Flush promises
await flushPromises();
// Mock fetch
mockFetch({
'/api/data': { status: 200, data: { id: 1 } }
});
// Mock console
const consoleSpy = mockConsole();Stencil's Jest environment setup:
// jest/jest-environment.ts
export class StencilEnvironment extends NodeEnvironment {
constructor(config: Config) {
super(config);
// Set up globals
this.global.fetch = mockFetch;
this.global.CSS = mockCSS;
this.global.CSSStyleSheet = MockCSSStyleSheet;
}
async setup() {
await super.setup();
// Initialize platform
const { win, doc } = mockPlatform();
this.global.window = win;
this.global.document = doc;
}
}TypeScript/JSX transformation:
// jest/jest-preprocessor.ts
export const process = (
sourceText: string,
sourcePath: string,
config: Config
) => {
if (sourcePath.endsWith('.tsx') || sourcePath.endsWith('.ts')) {
const transformed = transformSync(sourceText, {
filename: sourcePath,
presets: ['@babel/preset-typescript'],
plugins: [
['@babel/plugin-transform-typescript', { isTSX: true }],
'@babel/plugin-syntax-jsx',
'babel-plugin-transform-stencil-jsx'
]
});
return transformed.code;
}
return sourceText;
};describe('lifecycle', () => {
it('calls lifecycle methods', async () => {
let componentWillLoadCalled = false;
let componentDidLoadCalled = false;
@Component({ tag: 'test-lifecycle' })
class TestLifecycle {
componentWillLoad() {
componentWillLoadCalled = true;
}
componentDidLoad() {
componentDidLoadCalled = true;
}
render() {
return <div>Test</div>;
}
}
const page = await newSpecPage({
components: [TestLifecycle],
html: '<test-lifecycle></test-lifecycle>'
});
expect(componentWillLoadCalled).toBe(true);
expect(componentDidLoadCalled).toBe(true);
});
});it('emits custom events', async () => {
const page = await newSpecPage({
components: [MyComponent],
html: '<my-component></my-component>'
});
const eventSpy = jest.fn();
page.root.addEventListener('myEvent', eventSpy);
// Trigger event
page.rootInstance.emitEvent();
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
detail: { message: 'Hello' }
})
);
});it('loads async data', async () => {
mockFetch({
'/api/user': {
status: 200,
json: async () => ({ name: 'John' })
}
});
const page = await newSpecPage({
components: [UserComponent],
html: '<user-component></user-component>'
});
// Wait for async operations
await page.waitForChanges();
expect(page.root.textContent).toContain('John');
});- Jest Version Lock: Only supports Jest 27-29
- Performance: Slower than modern alternatives
- Browser Support: Limited real browser testing
- Module Support: Issues with ESM modules
- Debugging: Poor source map support
- Maintenance Burden: Complex Jest integration
- Performance Issues: Slow test execution
- Limited Features: Missing modern testing capabilities
- Better Alternatives: Superior tools now available
- Technical Debt: Accumulated complexity