@stencil/vitest 1.10.0 → 1.11.1

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 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:
@@ -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
+ }
@@ -20,6 +20,16 @@ export declare function serializeHtml(input: HTMLElement | ShadowRoot | Document
20
20
  * Custom HTML prettifier
21
21
  */
22
22
  export declare function prettifyHtml(html: string): string;
23
+ /**
24
+ * Sort attributes alphabetically within every opening HTML tag in a string.
25
+ * Used to normalise expected HTML strings before comparison so that attribute
26
+ * order written by hand in tests does not have to match the order produced by
27
+ * the runtime (which can differ between dev and prod Stencil builds).
28
+ *
29
+ * Handles double-quoted attribute values and bare boolean attributes.
30
+ * Does NOT touch closing tags, self-closing tags, or text content.
31
+ */
32
+ export declare function sortAttributesInHtml(html: string): string;
23
33
  /**
24
34
  * Normalize HTML for comparison by removing extra whitespace
25
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"html-serializer.d.ts","sourceRoot":"","sources":["../../src/testing/html-serializer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkCH,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,gBAAgB,GAAG,MAAM,EAC3D,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAWR;AAyID;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuDjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD"}
1
+ {"version":3,"file":"html-serializer.d.ts","sourceRoot":"","sources":["../../src/testing/html-serializer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkCH,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,gBAAgB,GAAG,MAAM,EAC3D,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAWR;AA6ID;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuDjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuBzD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -69,10 +69,14 @@ function serializeElementWithShadow(element, excludeStyles, serializeShadowRoot
69
69
  const tagName = elem.localName || elem.tagName.toLowerCase();
70
70
  // Build opening tag with attributes
71
71
  let html = `<${tagName}`;
72
- // Add attributes
72
+ // Add attributes sorted alphabetically for deterministic output across dev/prod builds
73
73
  if (elem.attributes) {
74
- for (let i = 0; i < elem.attributes.length; i++) {
75
- const attr = elem.attributes[i];
74
+ const attrs = Array.from(elem.attributes).sort((a, b) => {
75
+ const nameA = a.prefix && a.localName ? `${a.prefix}:${a.localName}` : a.name;
76
+ const nameB = b.prefix && b.localName ? `${b.prefix}:${b.localName}` : b.name;
77
+ return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
78
+ });
79
+ for (const attr of attrs) {
76
80
  // Handle namespaced attributes (e.g., xlink:href)
77
81
  // Use prefix + localName if available, otherwise fall back to name
78
82
  let attrName = attr.name;
@@ -216,6 +220,35 @@ export function prettifyHtml(html) {
216
220
  }
217
221
  return lines.join('\n');
218
222
  }
223
+ /**
224
+ * Sort attributes alphabetically within every opening HTML tag in a string.
225
+ * Used to normalise expected HTML strings before comparison so that attribute
226
+ * order written by hand in tests does not have to match the order produced by
227
+ * the runtime (which can differ between dev and prod Stencil builds).
228
+ *
229
+ * Handles double-quoted attribute values and bare boolean attributes.
230
+ * Does NOT touch closing tags, self-closing tags, or text content.
231
+ */
232
+ export function sortAttributesInHtml(html) {
233
+ // Match opening tags: capture tag name and the rest of the attribute string
234
+ return html.replace(/<([a-zA-Z][a-zA-Z0-9:._-]*)((?:\s[^>]*)?)>/g, (_match, tagName, attrStr) => {
235
+ if (!attrStr.trim()) {
236
+ return `<${tagName}>`;
237
+ }
238
+ // Parse individual attributes (boolean attrs and attr="value" attrs)
239
+ const attrPattern = /\s+([^\s=>"/]+)(?:="([^"]*?)")?/g;
240
+ const attrs = [];
241
+ let m;
242
+ while ((m = attrPattern.exec(attrStr)) !== null) {
243
+ attrs.push({ name: m[1], value: m[2] !== undefined ? m[2] : null });
244
+ }
245
+ attrs.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
246
+ const sortedAttrStr = attrs
247
+ .map(({ name, value }) => (value === null ? ` ${name}` : ` ${name}="${value}"`))
248
+ .join('');
249
+ return `<${tagName}${sortedAttrStr}>`;
250
+ });
251
+ }
219
252
  /**
220
253
  * Normalize HTML for comparison by removing extra whitespace
221
254
  */
@@ -1 +1 @@
1
- {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../../src/testing/matchers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,UAAU,cAAc,CAAC,CAAC,GAAG,OAAO;IAClC,mDAAmD;IACnD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC;IAClC,qDAAqD;IACrD,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,gFAAgF;IAChF,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,2EAA2E;IAC3E,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,mEAAmE;IACnE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,sEAAsE;IACtE,iBAAiB,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IACjD,yFAAyF;IACzF,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IACnC,4EAA4E;IAC5E,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IACxC,sGAAsG;IACtG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IACrC,gHAAgH;IAChH,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1C,mDAAmD;IACnD,gBAAgB,IAAI,CAAC,CAAC;IACtB,sFAAsF;IACtF,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IACrC,+FAA+F;IAC/F,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1C,yDAAyD;IACzD,mBAAmB,IAAI,CAAC,CAAC;IACzB,6EAA6E;IAC7E,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IAC3C,uEAAuE;IACvE,yBAAyB,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC1C,wEAAwE;IACxE,8BAA8B,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC/C,uEAAuE;IACvE,6BAA6B,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC9C,kFAAkF;IAClF,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;CAC7D;AAGD,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,SAAS,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;KAAG;IACzD,UAAU,QAAQ,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;KAAG;IAC/D,UAAU,4BAA6B,SAAQ,cAAc;KAAG;CACjE;AAioBD,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../../src/testing/matchers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,UAAU,cAAc,CAAC,CAAC,GAAG,OAAO;IAClC,mDAAmD;IACnD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC;IAClC,qDAAqD;IACrD,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,gFAAgF;IAChF,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,2EAA2E;IAC3E,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,mEAAmE;IACnE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,sEAAsE;IACtE,iBAAiB,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IACjD,yFAAyF;IACzF,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IACnC,4EAA4E;IAC5E,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IACxC,sGAAsG;IACtG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IACrC,gHAAgH;IAChH,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1C,mDAAmD;IACnD,gBAAgB,IAAI,CAAC,CAAC;IACtB,sFAAsF;IACtF,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IACrC,+FAA+F;IAC/F,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1C,yDAAyD;IACzD,mBAAmB,IAAI,CAAC,CAAC;IACzB,6EAA6E;IAC7E,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IAC3C,uEAAuE;IACvE,yBAAyB,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC1C,wEAAwE;IACxE,8BAA8B,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC/C,uEAAuE;IACvE,6BAA6B,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC9C,kFAAkF;IAClF,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;CAC7D;AAGD,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,SAAS,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;KAAG;IACzD,UAAU,QAAQ,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;KAAG;IAC/D,UAAU,4BAA6B,SAAQ,cAAc;KAAG;CACjE;AAmoBD,OAAO,EAAE,CAAC"}
@@ -4,7 +4,7 @@
4
4
  * These extend Vitest's expect with Stencil-specific assertions
5
5
  */
6
6
  import { expect } from 'vitest';
7
- import { serializeHtml, normalizeHtml, prettifyHtml } from './html-serializer.js';
7
+ import { serializeHtml, normalizeHtml, prettifyHtml, sortAttributesInHtml } from './html-serializer.js';
8
8
  /**
9
9
  * Check if element has a class
10
10
  */
@@ -313,8 +313,9 @@ function toEqualHtml(received, expected) {
313
313
  expectedHtml = serializeHtml(expectedFragment, { serializeShadowRoot: true, pretty: false });
314
314
  }
315
315
  else {
316
- // For element comparisons, just normalize to preserve <mock:shadow-root> tags
317
- expectedHtml = expected.trim();
316
+ // For element comparisons, sort attributes so hand-written expected HTML is
317
+ // order-independent (dev vs prod builds can reflect props in different orders)
318
+ expectedHtml = sortAttributesInHtml(expected.trim());
318
319
  }
319
320
  expectedHtml = normalizeHtml(expectedHtml);
320
321
  receivedHtml = normalizeHtml(receivedHtml);
@@ -365,8 +366,9 @@ function toEqualLightHtml(received, expected) {
365
366
  expectedHtml = serializeHtml(expectedFragment, { serializeShadowRoot: false, pretty: false });
366
367
  }
367
368
  else {
368
- // For element comparisons, just normalize to preserve <mock:shadow-root> tags
369
- expectedHtml = expected.trim();
369
+ // For element comparisons, sort attributes so hand-written expected HTML is
370
+ // order-independent (dev vs prod builds can reflect props in different orders)
371
+ expectedHtml = sortAttributesInHtml(expected.trim());
370
372
  }
371
373
  expectedHtml = normalizeHtml(expectedHtml);
372
374
  receivedHtml = normalizeHtml(receivedHtml);
@@ -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,CAqK7B"}
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"}
@@ -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.10.0",
7
+ "version": "1.11.1",
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.10.0"
107
+ "vitest-environment-stencil": "1.11.1"
104
108
  },
105
109
  "devDependencies": {
106
110
  "@eslint/js": "^9.39.2",