@stencil/vitest 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/plugin.d.ts +37 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +82 -0
- package/dist/testing/render.d.ts.map +1 -1
- package/dist/testing/render.js +17 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -440,6 +440,101 @@ expect(clickSpy.firstEvent?.detail).toEqual({ buttonId: 'my-button' });
|
|
|
440
440
|
expect(clickSpy.lastEvent?.detail).toEqual({ buttonId: 'my-button' });
|
|
441
441
|
```
|
|
442
442
|
|
|
443
|
+
## Stencil Vitest Plugin
|
|
444
|
+
|
|
445
|
+
The recommended testing approach in this package is to test against **pre-built dist outputs** — Stencil compiles your components once and tests run against those bundles. This is fast and reliable, but it does mean Vitest never sees individual component source files as discrete modules. As a result, `vi.mock()` cannot intercept imports made by your components, because the dependency is already bundled away before Vitest gets involved.
|
|
446
|
+
|
|
447
|
+
`stencilVitestPlugin` solves this by hooking into Vite's transform pipeline. Every `.tsx` file containing Stencil decorators is compiled on-the-fly via `transpileSync` before Vitest imports it, using `componentExport: 'customelement'`. This means each component file becomes its own entry in Vitest's module graph — and its imports are independently resolvable and mockable.
|
|
448
|
+
|
|
449
|
+
### Setup
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// vitest.config.ts
|
|
453
|
+
import { defineVitestConfig } from '@stencil/vitest/config';
|
|
454
|
+
import { stencilVitestPlugin } from '@stencil/vitest/plugin';
|
|
455
|
+
|
|
456
|
+
export default defineVitestConfig({
|
|
457
|
+
stencilConfig: './stencil.config.ts',
|
|
458
|
+
test: {
|
|
459
|
+
projects: [
|
|
460
|
+
{
|
|
461
|
+
plugins: [stencilVitestPlugin()],
|
|
462
|
+
test: {
|
|
463
|
+
name: 'plugin',
|
|
464
|
+
environment: 'stencil',
|
|
465
|
+
include: ['src/**/*.plugin.spec.{ts,tsx}'],
|
|
466
|
+
// No dist setup file needed — each component source file registers
|
|
467
|
+
// itself via customElements.define() the moment it is imported.
|
|
468
|
+
// Optional environment options
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Mocking component dependencies
|
|
477
|
+
|
|
478
|
+
With the plugin active, import the component source directly in your test. The plugin compiles it on-the-fly and the `customElements.define()` call at the end of the transformed output registers the element immediately.
|
|
479
|
+
|
|
480
|
+
Given an example component:
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
import { Component, Prop, h } from '@stencil/core';
|
|
484
|
+
import { capitalize } from '../../utils/index.js';
|
|
485
|
+
|
|
486
|
+
@Component( ... )
|
|
487
|
+
export class MyLabel {
|
|
488
|
+
@Prop() value: string = '';
|
|
489
|
+
|
|
490
|
+
render() {
|
|
491
|
+
return <span class="label">{capitalize(this.value)}</span>;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
It can then be imported and tested with mocked dependencies:
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
// my-label.plugin.spec.tsx
|
|
500
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
501
|
+
import { render, h } from '@stencil/vitest';
|
|
502
|
+
|
|
503
|
+
// vi.mock() is hoisted — the mock is in place before any imports resolve
|
|
504
|
+
vi.mock('../utils/index.js', () => ({
|
|
505
|
+
capitalize: vi.fn((s: string) => `[mocked:${s}]`),
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
// Importing the source file triggers the on-the-fly compile + define
|
|
509
|
+
import './my-label.tsx';
|
|
510
|
+
import { capitalize } from '../utils/index.js';
|
|
511
|
+
|
|
512
|
+
it('renders using the mocked utility', async () => {
|
|
513
|
+
vi.mocked(capitalize).mockReturnValue('Intercepted');
|
|
514
|
+
|
|
515
|
+
const { root } = await render(<my-label value="hello" />);
|
|
516
|
+
|
|
517
|
+
expect(root.shadowRoot!.querySelector('span')?.textContent).toBe('Intercepted');
|
|
518
|
+
expect(capitalize).toHaveBeenCalledWith('hello');
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Limitations
|
|
523
|
+
|
|
524
|
+
#### Class inheritance
|
|
525
|
+
|
|
526
|
+
In Stencil v4 `transpileSync` (used within the plugin) is a single-file compiler. When a component class `extends` a base class that lives in a separate file, `transpileSync` cannot follow the import to merge the parent's metadata and will throw an error.
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
// ❌ Will fail — base class is in a separate file
|
|
530
|
+
import { FormBase } from './form-base.js';
|
|
531
|
+
|
|
532
|
+
@Component({ tag: 'my-input', shadow: true })
|
|
533
|
+
export class MyInput extends FormBase { ... }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
> This limitation is specific to v4. Stencil v5's compiler can resolve multi-file inheritance chains.
|
|
537
|
+
|
|
443
538
|
## Snapshots
|
|
444
539
|
|
|
445
540
|
The package includes a custom snapshot serializer for Stencil components that properly handles shadow DOM:
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin } from 'vitest/config';
|
|
2
|
+
/**
|
|
3
|
+
* A Vite/Vitest plugin that transforms Stencil component source files (.tsx) on-the-fly,
|
|
4
|
+
* enabling module mocking and direct source imports during tests.
|
|
5
|
+
*
|
|
6
|
+
* The compiled output uses `componentExport: 'customelement'`, which appends a
|
|
7
|
+
* `customElements.define()` call at the end of the transformed file. The component
|
|
8
|
+
* registers itself the moment the module is imported — no dist loader or setup file
|
|
9
|
+
* required. Works with `@stencil/core` v4 and v5.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // vitest.config.ts
|
|
14
|
+
* import { defineVitestConfig } from '@stencil/vitest/config';
|
|
15
|
+
* import { stencilVitestPlugin } from '@stencil/vitest/plugin';
|
|
16
|
+
*
|
|
17
|
+
* export default defineVitestConfig({
|
|
18
|
+
* plugins: [stencilVitestPlugin()],
|
|
19
|
+
* test: {
|
|
20
|
+
* projects: [
|
|
21
|
+
* {
|
|
22
|
+
* test: {
|
|
23
|
+
* name: 'stencil',
|
|
24
|
+
* environment: 'stencil',
|
|
25
|
+
* include: ['**\/*.spec.tsx'],
|
|
26
|
+
* // No dist loader needed — import components from source directly
|
|
27
|
+
* },
|
|
28
|
+
* },
|
|
29
|
+
* ],
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @returns a Vite plugin configuration object
|
|
35
|
+
*/
|
|
36
|
+
export declare function stencilVitestPlugin(): Plugin;
|
|
37
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuD5C"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Vite/Vitest plugin that transforms Stencil component source files (.tsx) on-the-fly,
|
|
3
|
+
* enabling module mocking and direct source imports during tests.
|
|
4
|
+
*
|
|
5
|
+
* The compiled output uses `componentExport: 'customelement'`, which appends a
|
|
6
|
+
* `customElements.define()` call at the end of the transformed file. The component
|
|
7
|
+
* registers itself the moment the module is imported — no dist loader or setup file
|
|
8
|
+
* required. Works with `@stencil/core` v4 and v5.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // vitest.config.ts
|
|
13
|
+
* import { defineVitestConfig } from '@stencil/vitest/config';
|
|
14
|
+
* import { stencilVitestPlugin } from '@stencil/vitest/plugin';
|
|
15
|
+
*
|
|
16
|
+
* export default defineVitestConfig({
|
|
17
|
+
* plugins: [stencilVitestPlugin()],
|
|
18
|
+
* test: {
|
|
19
|
+
* projects: [
|
|
20
|
+
* {
|
|
21
|
+
* test: {
|
|
22
|
+
* name: 'stencil',
|
|
23
|
+
* environment: 'stencil',
|
|
24
|
+
* include: ['**\/*.spec.tsx'],
|
|
25
|
+
* // No dist loader needed — import components from source directly
|
|
26
|
+
* },
|
|
27
|
+
* },
|
|
28
|
+
* ],
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @returns a Vite plugin configuration object
|
|
34
|
+
*/
|
|
35
|
+
export function stencilVitestPlugin() {
|
|
36
|
+
return {
|
|
37
|
+
name: 'stencil-vitest-transform',
|
|
38
|
+
enforce: 'pre',
|
|
39
|
+
async transform(code, id) {
|
|
40
|
+
// Only transform .tsx files
|
|
41
|
+
if (!id.endsWith('.tsx')) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Quick check for Stencil decorator patterns before paying the compiler cost
|
|
45
|
+
const hasStencilDecorator = code.includes('@Component') ||
|
|
46
|
+
code.includes('@Prop') ||
|
|
47
|
+
code.includes('@State') ||
|
|
48
|
+
code.includes('@Event') ||
|
|
49
|
+
code.includes('@Method') ||
|
|
50
|
+
code.includes('@Watch') ||
|
|
51
|
+
code.includes('@Listen');
|
|
52
|
+
if (!hasStencilDecorator) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const { transpileSync } = await import('@stencil/core/compiler');
|
|
56
|
+
const result = transpileSync(code, {
|
|
57
|
+
file: id,
|
|
58
|
+
// 'customelement' appends a customElements.define() call so the component
|
|
59
|
+
// self-registers the moment this module is imported — no loader needed.
|
|
60
|
+
componentExport: 'customelement',
|
|
61
|
+
componentMetadata: 'compilerstatic',
|
|
62
|
+
currentDirectory: process.cwd(),
|
|
63
|
+
module: 'esm',
|
|
64
|
+
proxy: null,
|
|
65
|
+
sourceMap: false,
|
|
66
|
+
style: 'static',
|
|
67
|
+
styleImportData: 'queryparams',
|
|
68
|
+
target: 'es2022',
|
|
69
|
+
// Don't rewrite import paths — let Vite handle resolution via aliases
|
|
70
|
+
transformAliasedImportPaths: false,
|
|
71
|
+
});
|
|
72
|
+
const errors = result.diagnostics?.filter((d) => d.level === 'error') ?? [];
|
|
73
|
+
if (errors.length > 0) {
|
|
74
|
+
const messages = errors.map((d) => d.messageText).join('\n');
|
|
75
|
+
throw new Error(`[stencil-vitest-plugin] Transform error in ${id}:\n${messages}`);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
code: result.code,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/testing/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAExF,UAAU,aAAa;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AA+FD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,iBAAiB,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCtG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAY5F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,CAAC,GAAG,GAAG,EACvE,QAAQ,EAAE,GAAG,GAAG,MAAM,EACtB,OAAO,GAAE,aAGR,GACA,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/testing/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAExF,UAAU,aAAa;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AA+FD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,iBAAiB,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCtG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAY5F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,CAAC,GAAG,GAAG,EACvE,QAAQ,EAAE,GAAG,GAAG,MAAM,EACtB,OAAO,GAAE,aAGR,GACA,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAyL7B"}
|
package/dist/testing/render.js
CHANGED
|
@@ -159,6 +159,17 @@ export async function render(template, options = {
|
|
|
159
159
|
if (options.spyOn) {
|
|
160
160
|
setRenderSpyConfig(options.spyOn);
|
|
161
161
|
}
|
|
162
|
+
// Capture lifecycle errors (e.g. throws in componentWillLoad).
|
|
163
|
+
// Stencil's safeCall() catches all lifecycle hook errors and routes them to
|
|
164
|
+
// console.error instead of re-throwing.
|
|
165
|
+
let lifecycleError;
|
|
166
|
+
const origConsoleError = console.error;
|
|
167
|
+
console.error = (err, ...rest) => {
|
|
168
|
+
if (err instanceof Error && lifecycleError === undefined) {
|
|
169
|
+
lifecycleError = err;
|
|
170
|
+
}
|
|
171
|
+
origConsoleError(err, ...rest);
|
|
172
|
+
};
|
|
162
173
|
if (typeof template === 'string') {
|
|
163
174
|
// Handle string template - add as innerHTML
|
|
164
175
|
container.innerHTML = template;
|
|
@@ -220,6 +231,12 @@ export async function render(template, options = {
|
|
|
220
231
|
// Wait for Stencil's update cycle to complete
|
|
221
232
|
await waitForChanges();
|
|
222
233
|
}
|
|
234
|
+
// Restore console.error now that the lifecycle is done
|
|
235
|
+
console.error = origConsoleError;
|
|
236
|
+
// Re-throw any lifecycle error that Stencil's safeCall swallowed
|
|
237
|
+
if (lifecycleError !== undefined) {
|
|
238
|
+
throw lifecycleError;
|
|
239
|
+
}
|
|
223
240
|
// Clear per-render spy config after component is ready
|
|
224
241
|
if (options.spyOn) {
|
|
225
242
|
setRenderSpyConfig(null);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "https://github.com/stenciljs/vitest"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.11.0",
|
|
8
8
|
"description": "First-class testing utilities for Stencil design systems with Vitest",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/setup/happy-dom-setup.d.ts",
|
|
37
37
|
"import": "./dist/setup/happy-dom-setup.js"
|
|
38
38
|
},
|
|
39
|
+
"./plugin": {
|
|
40
|
+
"types": "./dist/plugin.d.ts",
|
|
41
|
+
"import": "./dist/plugin.js"
|
|
42
|
+
},
|
|
39
43
|
"./vitest": {
|
|
40
44
|
"types": "./dist/environments/stencil.d.ts",
|
|
41
45
|
"import": "./dist/environments/stencil.js"
|
|
@@ -100,7 +104,7 @@
|
|
|
100
104
|
"dependencies": {
|
|
101
105
|
"jiti": "^2.6.1",
|
|
102
106
|
"local-pkg": "^1.1.2",
|
|
103
|
-
"vitest-environment-stencil": "1.
|
|
107
|
+
"vitest-environment-stencil": "1.11.0"
|
|
104
108
|
},
|
|
105
109
|
"devDependencies": {
|
|
106
110
|
"@eslint/js": "^9.39.2",
|