@stencil/vitest 0.0.1-dev.20260109124515.90fb962

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.
Files changed (53) hide show
  1. package/README.md +296 -0
  2. package/dist/bin/stencil-test.d.ts +3 -0
  3. package/dist/bin/stencil-test.d.ts.map +1 -0
  4. package/dist/bin/stencil-test.js +341 -0
  5. package/dist/config.d.ts +73 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +302 -0
  8. package/dist/environments/env/happy-dom.d.ts +4 -0
  9. package/dist/environments/env/happy-dom.d.ts.map +1 -0
  10. package/dist/environments/env/happy-dom.js +15 -0
  11. package/dist/environments/env/jsdom.d.ts +4 -0
  12. package/dist/environments/env/jsdom.d.ts.map +1 -0
  13. package/dist/environments/env/jsdom.js +32 -0
  14. package/dist/environments/env/mock-doc.d.ts +4 -0
  15. package/dist/environments/env/mock-doc.d.ts.map +1 -0
  16. package/dist/environments/env/mock-doc.js +14 -0
  17. package/dist/environments/stencil.d.ts +28 -0
  18. package/dist/environments/stencil.d.ts.map +1 -0
  19. package/dist/environments/stencil.js +71 -0
  20. package/dist/environments/types.d.ts +6 -0
  21. package/dist/environments/types.d.ts.map +1 -0
  22. package/dist/environments/types.js +1 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +5 -0
  26. package/dist/setup/config-loader.d.ts +19 -0
  27. package/dist/setup/config-loader.d.ts.map +1 -0
  28. package/dist/setup/config-loader.js +92 -0
  29. package/dist/setup/happy-dom-setup.d.ts +12 -0
  30. package/dist/setup/happy-dom-setup.d.ts.map +1 -0
  31. package/dist/setup/happy-dom-setup.js +16 -0
  32. package/dist/setup/jsdom-setup.d.ts +30 -0
  33. package/dist/setup/jsdom-setup.d.ts.map +1 -0
  34. package/dist/setup/jsdom-setup.js +95 -0
  35. package/dist/setup/mock-doc-setup.d.ts +17 -0
  36. package/dist/setup/mock-doc-setup.d.ts.map +1 -0
  37. package/dist/setup/mock-doc-setup.js +90 -0
  38. package/dist/testing/html-serializer.d.ts +27 -0
  39. package/dist/testing/html-serializer.d.ts.map +1 -0
  40. package/dist/testing/html-serializer.js +152 -0
  41. package/dist/testing/matchers.d.ts +181 -0
  42. package/dist/testing/matchers.d.ts.map +1 -0
  43. package/dist/testing/matchers.js +460 -0
  44. package/dist/testing/render.d.ts +11 -0
  45. package/dist/testing/render.d.ts.map +1 -0
  46. package/dist/testing/render.js +118 -0
  47. package/dist/testing/snapshot-serializer.d.ts +17 -0
  48. package/dist/testing/snapshot-serializer.d.ts.map +1 -0
  49. package/dist/testing/snapshot-serializer.js +50 -0
  50. package/dist/types.d.ts +81 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +1 -0
  53. package/package.json +133 -0
@@ -0,0 +1,92 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ /**
4
+ * Load Stencil configuration from a file path
5
+ * Uses jiti to handle TypeScript files in Node.js
6
+ */
7
+ export async function loadStencilConfig(configPath) {
8
+ const resolvedPath = resolve(process.cwd(), configPath);
9
+ if (!existsSync(resolvedPath)) {
10
+ console.warn(`Stencil config not found at ${resolvedPath}`);
11
+ return undefined;
12
+ }
13
+ try {
14
+ // Use jiti for loading TypeScript configs in Node.js
15
+ const { createJiti } = await import('jiti');
16
+ const jiti = createJiti(process.cwd(), {
17
+ interopDefault: true,
18
+ moduleCache: false,
19
+ });
20
+ const configModule = (await jiti.import(resolvedPath));
21
+ return configModule.config || configModule.default || configModule;
22
+ }
23
+ catch (error) {
24
+ console.error(`Failed to load Stencil config from ${resolvedPath}:`, error);
25
+ return undefined;
26
+ }
27
+ }
28
+ /**
29
+ * Get the source directory from Stencil config
30
+ */
31
+ export function getStencilSrcDir(config) {
32
+ return config?.srcDir || 'src';
33
+ }
34
+ /**
35
+ * Get all output directories from Stencil config for exclusion
36
+ */
37
+ export function getStencilOutputDirs(config) {
38
+ if (!config?.outputTargets) {
39
+ return ['dist', 'www', 'build', '.stencil'];
40
+ }
41
+ const outputDirs = new Set();
42
+ config.outputTargets.forEach((target) => {
43
+ // Add common output directories based on target type
44
+ if (target.dir) {
45
+ outputDirs.add(target.dir);
46
+ }
47
+ if (target.buildDir) {
48
+ outputDirs.add(target.buildDir);
49
+ }
50
+ // Handle Stencil default directories for output types that don't specify dir
51
+ // Based on Stencil's default behavior:
52
+ if (target.type === 'dist' || target.type === 'dist-custom-elements' || target.type === 'dist-hydrate-script') {
53
+ // These all default to 'dist' directory
54
+ if (!target.dir && !target.buildDir) {
55
+ outputDirs.add('dist');
56
+ }
57
+ }
58
+ if (target.type === 'www') {
59
+ // www defaults to 'www' directory
60
+ if (!target.dir && !target.buildDir) {
61
+ outputDirs.add('www');
62
+ }
63
+ }
64
+ // Note: esmLoaderPath is a relative import path like '../loader', not a directory
65
+ // We should NOT extract this as an exclude pattern
66
+ });
67
+ // Always include common output directories
68
+ outputDirs.add('.stencil');
69
+ // Filter out invalid paths (those that navigate up with ..)
70
+ const validDirs = Array.from(outputDirs).filter((dir) => !dir.includes('..'));
71
+ return validDirs.length > 0 ? validDirs : ['dist', 'www', 'build', '.stencil'];
72
+ }
73
+ /**
74
+ * Create resolve aliases from Stencil config
75
+ */
76
+ export function getStencilResolveAliases(config) {
77
+ const srcDir = getStencilSrcDir(config);
78
+ const aliases = {
79
+ '@': resolve(process.cwd(), srcDir),
80
+ };
81
+ // Add component alias if components directory exists
82
+ const componentsDir = resolve(process.cwd(), srcDir, 'components');
83
+ if (existsSync(componentsDir)) {
84
+ aliases['@components'] = componentsDir;
85
+ }
86
+ // Add utils alias if utils directory exists
87
+ const utilsDir = resolve(process.cwd(), srcDir, 'utils');
88
+ if (existsSync(utilsDir)) {
89
+ aliases['@utils'] = utilsDir;
90
+ }
91
+ return aliases;
92
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Setup file for happy-dom environment
3
+ * Auto-loaded when using a project named 'happy-dom' or containing 'happy-dom'
4
+ *
5
+ * Configures happy-dom with Stencil-specific setup
6
+ * happy-dom generally has better built-in support than jsdom, so fewer polyfills are needed
7
+ */
8
+ /**
9
+ * Main setup function for happy-dom environment
10
+ */
11
+ export declare function setup(): Promise<void>;
12
+ //# sourceMappingURL=happy-dom-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"happy-dom-setup.d.ts","sourceRoot":"","sources":["../../src/setup/happy-dom-setup.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,wBAAsB,KAAK,kBAG1B"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Setup file for happy-dom environment
3
+ * Auto-loaded when using a project named 'happy-dom' or containing 'happy-dom'
4
+ *
5
+ * Configures happy-dom with Stencil-specific setup
6
+ * happy-dom generally has better built-in support than jsdom, so fewer polyfills are needed
7
+ */
8
+ /**
9
+ * Main setup function for happy-dom environment
10
+ */
11
+ export async function setup() {
12
+ // happy-dom is generally more complete than jsdom out of the box
13
+ // Add polyfills here as needed when issues are discovered
14
+ }
15
+ // Auto-run setup
16
+ setup();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Setup jsdom environment for Stencil component testing
3
+ *
4
+ * This module provides polyfills and initialization for testing Stencil components
5
+ * in a jsdom environment. It handles:
6
+ * - Polyfilling adoptedStyleSheets for Shadow DOM
7
+ * - Polyfilling CSS support detection
8
+ * - Polyfilling requestAnimationFrame and related APIs
9
+ * - Loading and initializing Stencil lazy loader
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // vitest.config.ts
14
+ * export default defineVitestConfig({
15
+ * test: {
16
+ * setupFiles: ['@stencil/vitest/jsdom-setup'],
17
+ * },
18
+ * });
19
+ * ```
20
+ */
21
+ /**
22
+ * Apply polyfills to a jsdom window object for Stencil components
23
+ * This function is reused by both the setup file and the custom environment
24
+ */
25
+ export declare function applyJsdomPolyfills(window: Window & typeof globalThis): void;
26
+ /**
27
+ * Initialize jsdom environment for Stencil testing
28
+ */
29
+ export declare function setup(): Promise<void>;
30
+ //# sourceMappingURL=jsdom-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsdom-setup.d.ts","sourceRoot":"","sources":["../../src/setup/jsdom-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,UAAU,QA2DrE;AAED;;GAEG;AACH,wBAAsB,KAAK,kBAa1B"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Setup jsdom environment for Stencil component testing
3
+ *
4
+ * This module provides polyfills and initialization for testing Stencil components
5
+ * in a jsdom environment. It handles:
6
+ * - Polyfilling adoptedStyleSheets for Shadow DOM
7
+ * - Polyfilling CSS support detection
8
+ * - Polyfilling requestAnimationFrame and related APIs
9
+ * - Loading and initializing Stencil lazy loader
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // vitest.config.ts
14
+ * export default defineVitestConfig({
15
+ * test: {
16
+ * setupFiles: ['@stencil/vitest/jsdom-setup'],
17
+ * },
18
+ * });
19
+ * ```
20
+ */
21
+ /**
22
+ * Apply polyfills to a jsdom window object for Stencil components
23
+ * This function is reused by both the setup file and the custom environment
24
+ */
25
+ export function applyJsdomPolyfills(window) {
26
+ // Polyfill adoptedStyleSheets for Shadow DOM
27
+ if (!window.document.adoptedStyleSheets) {
28
+ Object.defineProperty(window.document, 'adoptedStyleSheets', {
29
+ value: [],
30
+ writable: true,
31
+ configurable: true,
32
+ });
33
+ }
34
+ if (typeof window.ShadowRoot !== 'undefined' &&
35
+ !Object.prototype.hasOwnProperty.call(window.ShadowRoot.prototype, 'adoptedStyleSheets')) {
36
+ Object.defineProperty(window.ShadowRoot.prototype, 'adoptedStyleSheets', {
37
+ get() {
38
+ if (!this._adoptedStyleSheets) {
39
+ this._adoptedStyleSheets = [];
40
+ }
41
+ return this._adoptedStyleSheets;
42
+ },
43
+ set(value) {
44
+ this._adoptedStyleSheets = value;
45
+ },
46
+ configurable: true,
47
+ });
48
+ }
49
+ // Polyfill CSS support
50
+ if (!window.CSS) {
51
+ window.CSS = {
52
+ supports: () => true,
53
+ };
54
+ }
55
+ // Polyfill scrollTo
56
+ window.scrollTo = () => { };
57
+ // Add requestAnimationFrame and related APIs
58
+ if (!window.requestAnimationFrame) {
59
+ window.requestAnimationFrame = (cb) => {
60
+ return setTimeout(cb, 0);
61
+ };
62
+ }
63
+ if (!window.cancelAnimationFrame) {
64
+ window.cancelAnimationFrame = (id) => {
65
+ clearTimeout(id);
66
+ };
67
+ }
68
+ if (!window.requestIdleCallback) {
69
+ window.requestIdleCallback = (cb) => {
70
+ return setTimeout(cb, 0);
71
+ };
72
+ }
73
+ if (!window.cancelIdleCallback) {
74
+ window.cancelIdleCallback = (id) => {
75
+ clearTimeout(id);
76
+ };
77
+ }
78
+ }
79
+ /**
80
+ * Initialize jsdom environment for Stencil testing
81
+ */
82
+ export async function setup() {
83
+ // Only run in jsdom environment (Node.js with DOM)
84
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
85
+ return;
86
+ }
87
+ // Skip if running in actual browser (Playwright, WebdriverIO, etc.)
88
+ // In actual browsers, process is undefined or doesn't have cwd
89
+ if (typeof process === 'undefined' || typeof process.cwd !== 'function') {
90
+ return;
91
+ }
92
+ applyJsdomPolyfills(window);
93
+ }
94
+ // Auto-run setup when imported
95
+ setup();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Setup for mock-doc environment
3
+ * This file is automatically loaded when using the mock-doc environment in Node.js
4
+ *
5
+ * IMPORTANT: This should only be imported/executed in Node.js runtime, not in browsers.
6
+ * The projects-based config ensures this is only loaded for node:mock-doc projects.
7
+ */
8
+ import { setupGlobal, teardownGlobal } from '@stencil/core/mock-doc';
9
+ /**
10
+ * Apply polyfills to a window object for Stencil components
11
+ * This function is reused by both the setup file and the custom environment
12
+ */
13
+ export declare function applyMockDocPolyfills(win: any): void;
14
+ declare let win: any;
15
+ declare let doc: any;
16
+ export { win, doc, setupGlobal, teardownGlobal };
17
+ //# sourceMappingURL=mock-doc-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-doc-setup.d.ts","sourceRoot":"","sources":["../../src/setup/mock-doc-setup.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAc,WAAW,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,QAmD7C;AAOD,QAAA,IAAI,GAAG,EAAE,GAAG,CAAC;AACb,QAAA,IAAI,GAAG,EAAE,GAAG,CAAC;AA6Bb,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Setup for mock-doc environment
3
+ * This file is automatically loaded when using the mock-doc environment in Node.js
4
+ *
5
+ * IMPORTANT: This should only be imported/executed in Node.js runtime, not in browsers.
6
+ * The projects-based config ensures this is only loaded for node:mock-doc projects.
7
+ */
8
+ import { MockWindow, setupGlobal, teardownGlobal } from '@stencil/core/mock-doc';
9
+ /**
10
+ * Apply polyfills to a window object for Stencil components
11
+ * This function is reused by both the setup file and the custom environment
12
+ */
13
+ export function applyMockDocPolyfills(win) {
14
+ // Set baseURI manually
15
+ Object.defineProperty(win.document, 'baseURI', {
16
+ value: 'http://localhost:3000/',
17
+ writable: false,
18
+ configurable: true,
19
+ });
20
+ // Setup global with mock-doc globals
21
+ setupGlobal(win);
22
+ // Add MessageEvent if it doesn't exist (needed by Node's undici)
23
+ if (!win.MessageEvent) {
24
+ win.MessageEvent = class MessageEvent extends win.Event {
25
+ constructor(type, eventInitDict) {
26
+ super(type, eventInitDict);
27
+ }
28
+ };
29
+ }
30
+ // Add AbortController if it doesn't exist
31
+ if (!win.AbortController) {
32
+ win.AbortController = class AbortController {
33
+ constructor() {
34
+ this.signal = {
35
+ aborted: false,
36
+ addEventListener: () => { },
37
+ removeEventListener: () => { },
38
+ dispatchEvent: () => true,
39
+ };
40
+ }
41
+ abort() {
42
+ this.signal.aborted = true;
43
+ }
44
+ };
45
+ }
46
+ // Add requestAnimationFrame and related APIs
47
+ win.requestAnimationFrame = (cb) => {
48
+ return setTimeout(cb, 0);
49
+ };
50
+ win.cancelAnimationFrame = (id) => {
51
+ clearTimeout(id);
52
+ };
53
+ win.requestIdleCallback = (cb) => {
54
+ return setTimeout(cb, 0);
55
+ };
56
+ win.cancelIdleCallback = (id) => {
57
+ clearTimeout(id);
58
+ };
59
+ }
60
+ // Only setup mock-doc if we're actually in Node.js (not a real browser)
61
+ // Check for Node.js-specific globals that don't exist in browsers
62
+ const isNodeEnvironment = typeof process !== 'undefined' && process?.versions?.node !== undefined && typeof window === 'undefined';
63
+ let win;
64
+ let doc;
65
+ if (!isNodeEnvironment) {
66
+ // We're in a real browser, skip mock-doc setup and export real globals
67
+ win = typeof window !== 'undefined' ? window : undefined;
68
+ doc = typeof document !== 'undefined' ? document : undefined;
69
+ }
70
+ else {
71
+ // We're in Node.js, setup mock-doc
72
+ // Create mock window with URL
73
+ win = new MockWindow('http://localhost:3000/');
74
+ doc = win.document;
75
+ // Apply polyfills
76
+ applyMockDocPolyfills(win);
77
+ // Assign to globalThis
78
+ globalThis.window = win;
79
+ globalThis.document = doc;
80
+ globalThis.HTMLElement = win.HTMLElement;
81
+ globalThis.CustomEvent = win.CustomEvent;
82
+ globalThis.Event = win.Event;
83
+ globalThis.Element = win.Element;
84
+ globalThis.Node = win.Node;
85
+ globalThis.DocumentFragment = win.DocumentFragment;
86
+ globalThis.requestAnimationFrame = win.requestAnimationFrame;
87
+ globalThis.cancelAnimationFrame = win.cancelAnimationFrame;
88
+ }
89
+ // Export the mock window for use in custom setup
90
+ export { win, doc, setupGlobal, teardownGlobal };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared HTML serialization utilities for Stencil components
3
+ * Used by both matchers and snapshot serializers
4
+ */
5
+ export interface SerializeOptions {
6
+ /** Whether to include shadow DOM in serialization */
7
+ serializeShadowRoot?: boolean;
8
+ /** Whether to prettify the output */
9
+ pretty?: boolean;
10
+ /** Whether to exclude style tags */
11
+ excludeStyles?: boolean;
12
+ }
13
+ /**
14
+ * Serialize HTML element to string
15
+ * Works across mock-doc, jsdom, and happy-dom environments
16
+ * Uses consistent <mock:shadow-root> format across all environments
17
+ */
18
+ export declare function serializeHtml(input: HTMLElement | ShadowRoot | DocumentFragment | string, options?: SerializeOptions): string;
19
+ /**
20
+ * Custom HTML prettifier
21
+ */
22
+ export declare function prettifyHtml(html: string): string;
23
+ /**
24
+ * Normalize HTML for comparison by removing extra whitespace
25
+ */
26
+ export declare function normalizeHtml(html: string): string;
27
+ //# sourceMappingURL=html-serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-serializer.d.ts","sourceRoot":"","sources":["../../src/testing/html-serializer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,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;AAuFD;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkDjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared HTML serialization utilities for Stencil components
3
+ * Used by both matchers and snapshot serializers
4
+ */
5
+ /**
6
+ * Serialize HTML element to string
7
+ * Works across mock-doc, jsdom, and happy-dom environments
8
+ * Uses consistent <mock:shadow-root> format across all environments
9
+ */
10
+ export function serializeHtml(input, options = {}) {
11
+ const { serializeShadowRoot = true, pretty = true, excludeStyles = true } = options;
12
+ // If input is already a string, just normalize and return
13
+ if (typeof input === 'string') {
14
+ return input;
15
+ }
16
+ // Use custom serialization for consistent <mock:shadow-root> format across all environments
17
+ const html = serializeElementWithShadow(input, excludeStyles, serializeShadowRoot);
18
+ return pretty ? prettifyHtml(html) : html;
19
+ }
20
+ /**
21
+ * Recursively serialize an element and its shadow DOM for jsdom/happy-dom
22
+ * Note: Pretty printing is done at the top level only, not during recursion
23
+ */
24
+ function serializeElementWithShadow(element, excludeStyles, serializeShadowRoot = true) {
25
+ // Handle DocumentFragment
26
+ if (element.nodeType === 11) {
27
+ let html = '';
28
+ const children = Array.from(element.childNodes);
29
+ for (const child of children) {
30
+ if (child.nodeType === 1) {
31
+ // Element node
32
+ html += serializeElementWithShadow(child, excludeStyles, serializeShadowRoot);
33
+ }
34
+ else if (child.nodeType === 3) {
35
+ // Text node - include to match mock-doc
36
+ const text = child.textContent;
37
+ if (text)
38
+ html += text;
39
+ }
40
+ }
41
+ return html;
42
+ }
43
+ const elem = element;
44
+ const tagName = elem.tagName.toLowerCase();
45
+ // Build opening tag with attributes
46
+ let html = `<${tagName}`;
47
+ // Add attributes
48
+ if (elem.attributes) {
49
+ for (let i = 0; i < elem.attributes.length; i++) {
50
+ const attr = elem.attributes[i];
51
+ html += ` ${attr.name}="${attr.value}"`;
52
+ }
53
+ }
54
+ html += '>';
55
+ // Add shadow DOM if present and requested
56
+ if (serializeShadowRoot && 'shadowRoot' in elem && elem.shadowRoot) {
57
+ // Use mock:shadow-root format to match mock-doc's output
58
+ html += '<mock:shadow-root>';
59
+ // Serialize shadow DOM children
60
+ const shadowChildren = Array.from(elem.shadowRoot.childNodes);
61
+ for (const child of shadowChildren) {
62
+ if (child.nodeType === 1) {
63
+ // Element node - check if it's a style tag
64
+ const childElem = child;
65
+ if (excludeStyles && childElem.tagName.toLowerCase() === 'style') {
66
+ continue; // Skip style tags
67
+ }
68
+ html += serializeElementWithShadow(childElem, excludeStyles, serializeShadowRoot);
69
+ }
70
+ else if (child.nodeType === 3) {
71
+ // Text node - include it (matches mock-doc behavior)
72
+ const text = child.textContent;
73
+ if (text)
74
+ html += text;
75
+ }
76
+ }
77
+ html += '</mock:shadow-root>';
78
+ }
79
+ // Add light DOM children
80
+ const children = Array.from(elem.childNodes);
81
+ for (const child of children) {
82
+ if (child.nodeType === 1) {
83
+ // Element node
84
+ html += serializeElementWithShadow(child, excludeStyles, serializeShadowRoot);
85
+ }
86
+ else if (child.nodeType === 3) {
87
+ // Text node - include it to match mock-doc behavior
88
+ const text = child.textContent;
89
+ if (text)
90
+ html += text;
91
+ }
92
+ }
93
+ html += `</${tagName}>`;
94
+ return html;
95
+ }
96
+ /**
97
+ * Custom HTML prettifier
98
+ */
99
+ export function prettifyHtml(html) {
100
+ const indentSize = 2;
101
+ let indentLevel = 0;
102
+ const lines = [];
103
+ // Normalize whitespace
104
+ html = html.replace(/\s+/g, ' ').replace(/>\s*</g, '><').trim();
105
+ // Split on tag boundaries while preserving tags
106
+ const parts = html.split(/(<[^>]+>)/);
107
+ for (let i = 0; i < parts.length; i++) {
108
+ const part = parts[i].trim();
109
+ if (!part)
110
+ continue;
111
+ if (part.startsWith('<')) {
112
+ // This is a tag
113
+ if (part.startsWith('</')) {
114
+ // Closing tag - decrease indent first
115
+ indentLevel = Math.max(0, indentLevel - 1);
116
+ // Check if we should merge with previous line (empty element)
117
+ const tagName = part.match(/<\/([^>\s]+)/)?.[1];
118
+ const lastLine = lines[lines.length - 1];
119
+ if (lastLine && tagName) {
120
+ const openTagPattern = new RegExp(`^\\s*<${tagName.replace(/:/g, '\\:')}[^>]*>$`);
121
+ if (openTagPattern.test(lastLine)) {
122
+ // Empty element - append closing tag to same line
123
+ lines[lines.length - 1] = lastLine + part;
124
+ continue;
125
+ }
126
+ }
127
+ lines.push(' '.repeat(indentLevel * indentSize) + part);
128
+ }
129
+ else if (part.endsWith('/>')) {
130
+ // Self-closing tag
131
+ lines.push(' '.repeat(indentLevel * indentSize) + part);
132
+ }
133
+ else {
134
+ // Opening tag
135
+ lines.push(' '.repeat(indentLevel * indentSize) + part);
136
+ // Increase indent for next content
137
+ indentLevel++;
138
+ }
139
+ }
140
+ else {
141
+ // Text content - add on its own line with current indentation
142
+ lines.push(' '.repeat(indentLevel * indentSize) + part);
143
+ }
144
+ }
145
+ return lines.join('\n');
146
+ }
147
+ /**
148
+ * Normalize HTML for comparison by removing extra whitespace
149
+ */
150
+ export function normalizeHtml(html) {
151
+ return html.replace(/\s+/g, ' ').replace(/>\s+</g, '><').trim();
152
+ }