@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.
- package/README.md +296 -0
- package/dist/bin/stencil-test.d.ts +3 -0
- package/dist/bin/stencil-test.d.ts.map +1 -0
- package/dist/bin/stencil-test.js +341 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +302 -0
- package/dist/environments/env/happy-dom.d.ts +4 -0
- package/dist/environments/env/happy-dom.d.ts.map +1 -0
- package/dist/environments/env/happy-dom.js +15 -0
- package/dist/environments/env/jsdom.d.ts +4 -0
- package/dist/environments/env/jsdom.d.ts.map +1 -0
- package/dist/environments/env/jsdom.js +32 -0
- package/dist/environments/env/mock-doc.d.ts +4 -0
- package/dist/environments/env/mock-doc.d.ts.map +1 -0
- package/dist/environments/env/mock-doc.js +14 -0
- package/dist/environments/stencil.d.ts +28 -0
- package/dist/environments/stencil.d.ts.map +1 -0
- package/dist/environments/stencil.js +71 -0
- package/dist/environments/types.d.ts +6 -0
- package/dist/environments/types.d.ts.map +1 -0
- package/dist/environments/types.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/setup/config-loader.d.ts +19 -0
- package/dist/setup/config-loader.d.ts.map +1 -0
- package/dist/setup/config-loader.js +92 -0
- package/dist/setup/happy-dom-setup.d.ts +12 -0
- package/dist/setup/happy-dom-setup.d.ts.map +1 -0
- package/dist/setup/happy-dom-setup.js +16 -0
- package/dist/setup/jsdom-setup.d.ts +30 -0
- package/dist/setup/jsdom-setup.d.ts.map +1 -0
- package/dist/setup/jsdom-setup.js +95 -0
- package/dist/setup/mock-doc-setup.d.ts +17 -0
- package/dist/setup/mock-doc-setup.d.ts.map +1 -0
- package/dist/setup/mock-doc-setup.js +90 -0
- package/dist/testing/html-serializer.d.ts +27 -0
- package/dist/testing/html-serializer.d.ts.map +1 -0
- package/dist/testing/html-serializer.js +152 -0
- package/dist/testing/matchers.d.ts +181 -0
- package/dist/testing/matchers.d.ts.map +1 -0
- package/dist/testing/matchers.js +460 -0
- package/dist/testing/render.d.ts +11 -0
- package/dist/testing/render.d.ts.map +1 -0
- package/dist/testing/render.js +118 -0
- package/dist/testing/snapshot-serializer.d.ts +17 -0
- package/dist/testing/snapshot-serializer.d.ts.map +1 -0
- package/dist/testing/snapshot-serializer.js +50 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +133 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom matchers for Stencil component testing
|
|
3
|
+
*
|
|
4
|
+
* These extend Vitest's expect with Stencil-specific assertions
|
|
5
|
+
*/
|
|
6
|
+
import type { EventSpy } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Custom matchers interface
|
|
9
|
+
*/
|
|
10
|
+
interface CustomMatchers<R = unknown> {
|
|
11
|
+
toHaveClass(className: string): R;
|
|
12
|
+
toHaveClasses(classNames: string[]): R;
|
|
13
|
+
toMatchClasses(classNames: string[]): R;
|
|
14
|
+
toHaveAttribute(attribute: string, value?: string): R;
|
|
15
|
+
toEqualAttribute(attribute: string, value: string): R;
|
|
16
|
+
toEqualAttributes(expectedAttrs: Record<string, string>): R;
|
|
17
|
+
toHaveProperty(property: string, value?: any): R;
|
|
18
|
+
toHaveTextContent(text: string): R;
|
|
19
|
+
toEqualText(expectedText: string): R;
|
|
20
|
+
toBeVisible(): R;
|
|
21
|
+
toHaveShadowRoot(): R;
|
|
22
|
+
toEmitEvent(eventName: string): R;
|
|
23
|
+
toEqualHtml(expectedHtml: string): R;
|
|
24
|
+
toEqualLightHtml(expectedHtml: string): R;
|
|
25
|
+
toHaveReceivedEvent(): R;
|
|
26
|
+
toHaveReceivedEventTimes(count: number): R;
|
|
27
|
+
toHaveReceivedEventDetail(detail: any): R;
|
|
28
|
+
toHaveFirstReceivedEventDetail(detail: any): R;
|
|
29
|
+
toHaveLastReceivedEventDetail(detail: any): R;
|
|
30
|
+
toHaveNthReceivedEventDetail(index: number, detail: any): R;
|
|
31
|
+
}
|
|
32
|
+
declare global {
|
|
33
|
+
namespace Vi {
|
|
34
|
+
interface Assertion<T = any> extends CustomMatchers<T> {
|
|
35
|
+
}
|
|
36
|
+
interface AsymmetricMatchersContaining extends CustomMatchers {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if element has a class
|
|
42
|
+
*/
|
|
43
|
+
export declare function toHaveClass(received: HTMLElement, className: string): {
|
|
44
|
+
pass: boolean;
|
|
45
|
+
message: () => string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Check if element has multiple classes
|
|
49
|
+
* Checks if element has all of the specified CSS classes (order doesn't matter)
|
|
50
|
+
*/
|
|
51
|
+
export declare function toHaveClasses(received: HTMLElement, classNames: string[]): {
|
|
52
|
+
pass: boolean;
|
|
53
|
+
message: () => string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Check if element has exactly the specified CSS classes (no more, no less)
|
|
57
|
+
* Order doesn't matter, but the element must have exactly these classes
|
|
58
|
+
*/
|
|
59
|
+
export declare function toMatchClasses(received: HTMLElement, classNames: string[]): {
|
|
60
|
+
pass: boolean;
|
|
61
|
+
message: () => string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Check if element has an attribute
|
|
65
|
+
*/
|
|
66
|
+
export declare function toHaveAttribute(received: HTMLElement, attribute: string, value?: string): {
|
|
67
|
+
pass: boolean;
|
|
68
|
+
message: () => string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Check if element has a specific attribute with an exact value
|
|
72
|
+
*/
|
|
73
|
+
export declare function toEqualAttribute(received: HTMLElement, attribute: string, value: string): {
|
|
74
|
+
pass: boolean;
|
|
75
|
+
message: () => string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Check if element has all expected attributes with exact values
|
|
79
|
+
*/
|
|
80
|
+
export declare function toEqualAttributes(received: HTMLElement, expectedAttrs: Record<string, string>): {
|
|
81
|
+
pass: boolean;
|
|
82
|
+
message: () => string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Check if element has a property
|
|
86
|
+
*/
|
|
87
|
+
export declare function toHaveProperty(received: any, property: string, value?: any): {
|
|
88
|
+
pass: boolean;
|
|
89
|
+
message: () => string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Check if element has text content
|
|
93
|
+
*/
|
|
94
|
+
export declare function toHaveTextContent(received: HTMLElement, text: string): {
|
|
95
|
+
pass: boolean;
|
|
96
|
+
message: () => string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Check if element's text content exactly matches (after trimming)
|
|
100
|
+
*/
|
|
101
|
+
export declare function toEqualText(received: HTMLElement, expectedText: string): {
|
|
102
|
+
pass: boolean;
|
|
103
|
+
message: () => string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Check if element is visible
|
|
107
|
+
*/
|
|
108
|
+
export declare function toBeVisible(received: HTMLElement): {
|
|
109
|
+
pass: boolean;
|
|
110
|
+
message: () => string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Check if element has shadow root
|
|
114
|
+
*/
|
|
115
|
+
export declare function toHaveShadowRoot(received: HTMLElement): {
|
|
116
|
+
pass: boolean;
|
|
117
|
+
message: () => string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Custom matcher to check if an element's HTML matches the expected HTML
|
|
121
|
+
* Serializes the entire component tree including shadow DOM
|
|
122
|
+
*/
|
|
123
|
+
export declare function toEqualHtml(received: string | HTMLElement | ShadowRoot, expected: string): {
|
|
124
|
+
pass: boolean;
|
|
125
|
+
message: () => string;
|
|
126
|
+
}; /**
|
|
127
|
+
* Custom matcher to check if an element's Light DOM matches the expected HTML
|
|
128
|
+
* Does not serialize shadow DOM
|
|
129
|
+
*/
|
|
130
|
+
export declare function toEqualLightHtml(received: string | HTMLElement | ShadowRoot, expected: string): {
|
|
131
|
+
pass: boolean;
|
|
132
|
+
message: () => string;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Check if an EventSpy has received at least one event
|
|
136
|
+
*/
|
|
137
|
+
export declare function toHaveReceivedEvent(received: EventSpy): {
|
|
138
|
+
pass: boolean;
|
|
139
|
+
message: () => string;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Check if an EventSpy has received an event a specific number of times
|
|
143
|
+
*/
|
|
144
|
+
export declare function toHaveReceivedEventTimes(received: EventSpy, count: number): {
|
|
145
|
+
pass: boolean;
|
|
146
|
+
message: () => string;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Check if the last received event has the expected detail
|
|
150
|
+
*/
|
|
151
|
+
export declare function toHaveReceivedEventDetail(received: EventSpy, detail: any): {
|
|
152
|
+
pass: boolean;
|
|
153
|
+
message: () => string;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Check if the first received event has the expected detail
|
|
157
|
+
*/
|
|
158
|
+
export declare function toHaveFirstReceivedEventDetail(received: EventSpy, detail: any): {
|
|
159
|
+
pass: boolean;
|
|
160
|
+
message: () => string;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Check if the last received event has the expected detail (alias for toHaveReceivedEventDetail)
|
|
164
|
+
*/
|
|
165
|
+
export declare function toHaveLastReceivedEventDetail(received: EventSpy, detail: any): {
|
|
166
|
+
pass: boolean;
|
|
167
|
+
message: () => string;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Check if the event at a specific index has the expected detail
|
|
171
|
+
*/
|
|
172
|
+
export declare function toHaveNthReceivedEventDetail(received: EventSpy, index: number, detail: any): {
|
|
173
|
+
pass: boolean;
|
|
174
|
+
message: () => string;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Install custom matchers
|
|
178
|
+
*/
|
|
179
|
+
export declare function installMatchers(): void;
|
|
180
|
+
export {};
|
|
181
|
+
//# sourceMappingURL=matchers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../../src/testing/matchers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom matchers for Stencil component testing
|
|
3
|
+
*
|
|
4
|
+
* These extend Vitest's expect with Stencil-specific assertions
|
|
5
|
+
*/
|
|
6
|
+
import { expect } from 'vitest';
|
|
7
|
+
import { serializeHtml, normalizeHtml, prettifyHtml } from './html-serializer.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if element has a class
|
|
10
|
+
*/
|
|
11
|
+
export function toHaveClass(received, className) {
|
|
12
|
+
const pass = received.classList.contains(className);
|
|
13
|
+
return {
|
|
14
|
+
pass,
|
|
15
|
+
message: () => pass ? `Expected element not to have class "${className}"` : `Expected element to have class "${className}"`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if element has multiple classes
|
|
20
|
+
* Checks if element has all of the specified CSS classes (order doesn't matter)
|
|
21
|
+
*/
|
|
22
|
+
export function toHaveClasses(received, classNames) {
|
|
23
|
+
const missingClasses = [];
|
|
24
|
+
for (const className of classNames) {
|
|
25
|
+
if (!received.classList.contains(className)) {
|
|
26
|
+
missingClasses.push(className);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const pass = missingClasses.length === 0;
|
|
30
|
+
const actualClasses = Array.from(received.classList).join(', ');
|
|
31
|
+
return {
|
|
32
|
+
pass,
|
|
33
|
+
message: () => pass
|
|
34
|
+
? `Expected element not to have classes [${classNames.join(', ')}]`
|
|
35
|
+
: `Expected element to have classes [${classNames.join(', ')}], but missing [${missingClasses.join(', ')}]. Actual classes: [${actualClasses}]`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if element has exactly the specified CSS classes (no more, no less)
|
|
40
|
+
* Order doesn't matter, but the element must have exactly these classes
|
|
41
|
+
*/
|
|
42
|
+
export function toMatchClasses(received, classNames) {
|
|
43
|
+
// Get classes from the class attribute to support mock-doc
|
|
44
|
+
const classAttr = received.getAttribute('class') || '';
|
|
45
|
+
const actualClasses = classAttr.split(/\s+/).filter(Boolean).sort();
|
|
46
|
+
const expectedClasses = [...classNames].filter(Boolean).sort();
|
|
47
|
+
const pass = actualClasses.length === expectedClasses.length && actualClasses.every((cls, idx) => cls === expectedClasses[idx]);
|
|
48
|
+
return {
|
|
49
|
+
pass,
|
|
50
|
+
message: () => pass
|
|
51
|
+
? `Expected element not to have exactly classes [${classNames.join(', ')}]`
|
|
52
|
+
: `Expected element to have exactly classes [${classNames.join(', ')}], but got [${actualClasses.join(', ')}]`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if element has an attribute
|
|
57
|
+
*/
|
|
58
|
+
export function toHaveAttribute(received, attribute, value) {
|
|
59
|
+
const hasAttribute = received.hasAttribute(attribute);
|
|
60
|
+
if (!hasAttribute) {
|
|
61
|
+
return {
|
|
62
|
+
pass: false,
|
|
63
|
+
message: () => `Expected element to have attribute "${attribute}"`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
const actualValue = received.getAttribute(attribute);
|
|
68
|
+
const pass = actualValue === value;
|
|
69
|
+
return {
|
|
70
|
+
pass,
|
|
71
|
+
message: () => pass
|
|
72
|
+
? `Expected element not to have attribute "${attribute}" with value "${value}"`
|
|
73
|
+
: `Expected element to have attribute "${attribute}" with value "${value}", but got "${actualValue}"`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
pass: true,
|
|
78
|
+
message: () => `Expected element not to have attribute "${attribute}"`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if element has a specific attribute with an exact value
|
|
83
|
+
*/
|
|
84
|
+
export function toEqualAttribute(received, attribute, value) {
|
|
85
|
+
const actualValue = received.getAttribute(attribute);
|
|
86
|
+
const pass = actualValue === value;
|
|
87
|
+
return {
|
|
88
|
+
pass,
|
|
89
|
+
message: () => pass
|
|
90
|
+
? `Expected element attribute "${attribute}" not to equal "${value}"`
|
|
91
|
+
: `Expected element attribute "${attribute}" to equal "${value}", but got "${actualValue}"`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if element has all expected attributes with exact values
|
|
96
|
+
*/
|
|
97
|
+
export function toEqualAttributes(received, expectedAttrs) {
|
|
98
|
+
const mismatches = [];
|
|
99
|
+
const actualAttrs = {};
|
|
100
|
+
// Collect all actual attributes
|
|
101
|
+
for (let i = 0; i < received.attributes.length; i++) {
|
|
102
|
+
const attr = received.attributes[i];
|
|
103
|
+
actualAttrs[attr.name] = attr.value;
|
|
104
|
+
}
|
|
105
|
+
// Check expected attributes
|
|
106
|
+
for (const [name, expectedValue] of Object.entries(expectedAttrs)) {
|
|
107
|
+
const actualValue = received.getAttribute(name);
|
|
108
|
+
if (actualValue === null) {
|
|
109
|
+
mismatches.push(`missing "${name}"`);
|
|
110
|
+
}
|
|
111
|
+
else if (actualValue !== expectedValue) {
|
|
112
|
+
mismatches.push(`"${name}": expected "${expectedValue}", got "${actualValue}"`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const pass = mismatches.length === 0;
|
|
116
|
+
return {
|
|
117
|
+
pass,
|
|
118
|
+
message: () => pass
|
|
119
|
+
? `Expected element not to have attributes ${JSON.stringify(expectedAttrs)}`
|
|
120
|
+
: `Expected element attributes to match.\nMismatches: ${mismatches.join(', ')}\nExpected: ${JSON.stringify(expectedAttrs)}\nActual: ${JSON.stringify(actualAttrs)}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Check if element has a property
|
|
125
|
+
*/
|
|
126
|
+
export function toHaveProperty(received, property, value) {
|
|
127
|
+
const hasProperty = property in received;
|
|
128
|
+
if (!hasProperty) {
|
|
129
|
+
return {
|
|
130
|
+
pass: false,
|
|
131
|
+
message: () => `Expected element to have property "${property}"`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (value !== undefined) {
|
|
135
|
+
const actualValue = received[property];
|
|
136
|
+
const pass = actualValue === value;
|
|
137
|
+
return {
|
|
138
|
+
pass,
|
|
139
|
+
message: () => pass
|
|
140
|
+
? `Expected element not to have property "${property}" with value ${JSON.stringify(value)}`
|
|
141
|
+
: `Expected element to have property "${property}" with value ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
pass: true,
|
|
146
|
+
message: () => `Expected element not to have property "${property}"`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if element has text content
|
|
151
|
+
*/
|
|
152
|
+
export function toHaveTextContent(received, text) {
|
|
153
|
+
const actualText = received.textContent || '';
|
|
154
|
+
const pass = actualText.includes(text);
|
|
155
|
+
return {
|
|
156
|
+
pass,
|
|
157
|
+
message: () => pass
|
|
158
|
+
? `Expected element not to have text content "${text}"`
|
|
159
|
+
: `Expected element to have text content "${text}", but got "${actualText}"`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if element's text content exactly matches (after trimming)
|
|
164
|
+
*/
|
|
165
|
+
export function toEqualText(received, expectedText) {
|
|
166
|
+
const actualText = (received.textContent || '').trim();
|
|
167
|
+
const trimmedExpected = expectedText.trim();
|
|
168
|
+
const pass = actualText === trimmedExpected;
|
|
169
|
+
return {
|
|
170
|
+
pass,
|
|
171
|
+
message: () => pass
|
|
172
|
+
? `Expected element text not to equal "${trimmedExpected}"`
|
|
173
|
+
: `Expected element text to equal "${trimmedExpected}", but got "${actualText}"`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if element is visible
|
|
178
|
+
*/
|
|
179
|
+
export function toBeVisible(received) {
|
|
180
|
+
const style = window.getComputedStyle(received);
|
|
181
|
+
const pass = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
182
|
+
return {
|
|
183
|
+
pass,
|
|
184
|
+
message: () => (pass ? `Expected element not to be visible` : `Expected element to be visible`),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if element has shadow root
|
|
189
|
+
*/
|
|
190
|
+
export function toHaveShadowRoot(received) {
|
|
191
|
+
const pass = !!received.shadowRoot;
|
|
192
|
+
return {
|
|
193
|
+
pass,
|
|
194
|
+
message: () => (pass ? `Expected element not to have shadow root` : `Expected element to have shadow root`),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Parse HTML string to fragment
|
|
199
|
+
*/
|
|
200
|
+
function parseHtmlFragment(html) {
|
|
201
|
+
// Try mock-doc parser first
|
|
202
|
+
try {
|
|
203
|
+
const mockDoc = require('@stencil/core/mock-doc');
|
|
204
|
+
if (mockDoc.parseHtmlToFragment) {
|
|
205
|
+
return mockDoc.parseHtmlToFragment(html);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Fall through to standard parser
|
|
210
|
+
}
|
|
211
|
+
// Use standard DOM parser (works in jsdom/happy-dom)
|
|
212
|
+
const template = document.createElement('template');
|
|
213
|
+
template.innerHTML = html.trim();
|
|
214
|
+
return template.content;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Custom matcher to check if an element's HTML matches the expected HTML
|
|
218
|
+
* Serializes the entire component tree including shadow DOM
|
|
219
|
+
*/
|
|
220
|
+
export function toEqualHtml(received, expected) {
|
|
221
|
+
if (received == null) {
|
|
222
|
+
throw new Error(`expect.toEqualHtml() received value is "${received}"`);
|
|
223
|
+
}
|
|
224
|
+
if (typeof received.then === 'function') {
|
|
225
|
+
throw new TypeError(`Element must be a resolved value, not a promise, before it can be tested`);
|
|
226
|
+
}
|
|
227
|
+
let receivedHtml;
|
|
228
|
+
// Serialize the received value
|
|
229
|
+
if (typeof received === 'string') {
|
|
230
|
+
const fragment = parseHtmlFragment(received);
|
|
231
|
+
// For string inputs, use innerHTML to avoid template wrapper
|
|
232
|
+
receivedHtml = fragment.innerHTML || fragment.textContent || '';
|
|
233
|
+
}
|
|
234
|
+
else if (received.nodeType === 11) {
|
|
235
|
+
// Document fragment
|
|
236
|
+
receivedHtml = serializeHtml(received, { serializeShadowRoot: true });
|
|
237
|
+
}
|
|
238
|
+
else if (received.nodeType === 1) {
|
|
239
|
+
// Element node
|
|
240
|
+
receivedHtml = serializeHtml(received, { serializeShadowRoot: true });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
throw new TypeError(`expect.toEqualHtml() value should be an element, shadow root, or string`);
|
|
244
|
+
}
|
|
245
|
+
// Parse and serialize expected HTML for consistent formatting
|
|
246
|
+
// For expected HTML, just normalize whitespace without parsing through DOM
|
|
247
|
+
// to preserve custom elements like <mock:shadow-root>
|
|
248
|
+
let expectedHtml = normalizeHtml(expected.trim());
|
|
249
|
+
receivedHtml = normalizeHtml(receivedHtml);
|
|
250
|
+
// Debug logging
|
|
251
|
+
if (expectedHtml !== receivedHtml) {
|
|
252
|
+
console.log('Expected (normalized):', JSON.stringify(expectedHtml));
|
|
253
|
+
console.log('Received (normalized):', JSON.stringify(receivedHtml));
|
|
254
|
+
}
|
|
255
|
+
const pass = receivedHtml === expectedHtml;
|
|
256
|
+
return {
|
|
257
|
+
pass,
|
|
258
|
+
message: () => pass
|
|
259
|
+
? `Expected HTML not to equal:\n${prettifyHtml(expectedHtml)}`
|
|
260
|
+
: `Expected HTML to equal:\n${prettifyHtml(expectedHtml)}\n\nReceived:\n${prettifyHtml(receivedHtml)}`,
|
|
261
|
+
};
|
|
262
|
+
} /**
|
|
263
|
+
* Custom matcher to check if an element's Light DOM matches the expected HTML
|
|
264
|
+
* Does not serialize shadow DOM
|
|
265
|
+
*/
|
|
266
|
+
export function toEqualLightHtml(received, expected) {
|
|
267
|
+
if (received == null) {
|
|
268
|
+
throw new Error(`expect.toEqualLightHtml() received value is "${received}"`);
|
|
269
|
+
}
|
|
270
|
+
if (typeof received.then === 'function') {
|
|
271
|
+
throw new TypeError(`Element must be a resolved value, not a promise, before it can be tested`);
|
|
272
|
+
}
|
|
273
|
+
let receivedHtml;
|
|
274
|
+
// Serialize the received value (without shadow DOM)
|
|
275
|
+
if (typeof received === 'string') {
|
|
276
|
+
const fragment = parseHtmlFragment(received);
|
|
277
|
+
// For string inputs, use innerHTML to avoid template wrapper
|
|
278
|
+
receivedHtml = fragment.innerHTML || fragment.textContent || '';
|
|
279
|
+
}
|
|
280
|
+
else if (received.nodeType === 11) {
|
|
281
|
+
// Document fragment
|
|
282
|
+
receivedHtml = serializeHtml(received, { serializeShadowRoot: false });
|
|
283
|
+
}
|
|
284
|
+
else if (received.nodeType === 1) {
|
|
285
|
+
// Element node
|
|
286
|
+
receivedHtml = serializeHtml(received, { serializeShadowRoot: false });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
throw new TypeError(`expect.toEqualLightHtml() value should be an element, shadow root, or string`);
|
|
290
|
+
}
|
|
291
|
+
// For expected HTML, just normalize whitespace without parsing through DOM
|
|
292
|
+
// to preserve custom elements like <mock:shadow-root>
|
|
293
|
+
let expectedHtml = normalizeHtml(expected.trim());
|
|
294
|
+
receivedHtml = normalizeHtml(receivedHtml);
|
|
295
|
+
// Debug logging
|
|
296
|
+
if (expectedHtml !== receivedHtml) {
|
|
297
|
+
console.log('LightDOM Expected (normalized):', JSON.stringify(expectedHtml));
|
|
298
|
+
console.log('LightDOM Received (normalized):', JSON.stringify(receivedHtml));
|
|
299
|
+
}
|
|
300
|
+
const pass = receivedHtml === expectedHtml;
|
|
301
|
+
return {
|
|
302
|
+
pass,
|
|
303
|
+
message: () => pass
|
|
304
|
+
? `Expected Light DOM HTML not to equal:\n${prettifyHtml(expectedHtml)}`
|
|
305
|
+
: `Expected Light DOM HTML to equal:\n${prettifyHtml(expectedHtml)}\n\nReceived:\n${prettifyHtml(receivedHtml)}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Check if an EventSpy has received at least one event
|
|
310
|
+
*/
|
|
311
|
+
export function toHaveReceivedEvent(received) {
|
|
312
|
+
const pass = received.length > 0;
|
|
313
|
+
return {
|
|
314
|
+
pass,
|
|
315
|
+
message: () => pass
|
|
316
|
+
? `Expected event "${received.eventName}" not to have been received`
|
|
317
|
+
: `Expected event "${received.eventName}" to have been received, but it was not`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Check if an EventSpy has received an event a specific number of times
|
|
322
|
+
*/
|
|
323
|
+
export function toHaveReceivedEventTimes(received, count) {
|
|
324
|
+
const pass = received.length === count;
|
|
325
|
+
return {
|
|
326
|
+
pass,
|
|
327
|
+
message: () => pass
|
|
328
|
+
? `Expected event "${received.eventName}" not to have been received ${count} times`
|
|
329
|
+
: `Expected event "${received.eventName}" to have been received ${count} times, but it was received ${received.length} times`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Safely stringify a value, handling circular references
|
|
334
|
+
*/
|
|
335
|
+
function safeStringify(value) {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.stringify(value);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// If circular reference, just return a string representation
|
|
341
|
+
return String(value);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Safely compare two values, handling circular references
|
|
346
|
+
*/
|
|
347
|
+
function safeEquals(a, b) {
|
|
348
|
+
// Try JSON comparison first
|
|
349
|
+
try {
|
|
350
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// If circular reference, fall back to shallow comparison
|
|
354
|
+
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
355
|
+
const keysA = Object.keys(a);
|
|
356
|
+
const keysB = Object.keys(b);
|
|
357
|
+
if (keysA.length !== keysB.length)
|
|
358
|
+
return false;
|
|
359
|
+
return keysA.every((key) => a[key] === b[key]);
|
|
360
|
+
}
|
|
361
|
+
return a === b;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check if the last received event has the expected detail
|
|
366
|
+
*/
|
|
367
|
+
export function toHaveReceivedEventDetail(received, detail) {
|
|
368
|
+
if (received.length === 0) {
|
|
369
|
+
return {
|
|
370
|
+
pass: false,
|
|
371
|
+
message: () => `Expected event "${received.eventName}" to have been received with detail, but no events were received`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const lastEvent = received.lastEvent;
|
|
375
|
+
const pass = safeEquals(lastEvent.detail, detail);
|
|
376
|
+
return {
|
|
377
|
+
pass,
|
|
378
|
+
message: () => pass
|
|
379
|
+
? `Expected last event detail not to equal ${safeStringify(detail)}`
|
|
380
|
+
: `Expected last event detail to equal ${safeStringify(detail)}, but got ${safeStringify(lastEvent.detail)}`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if the first received event has the expected detail
|
|
385
|
+
*/
|
|
386
|
+
export function toHaveFirstReceivedEventDetail(received, detail) {
|
|
387
|
+
if (received.length === 0) {
|
|
388
|
+
return {
|
|
389
|
+
pass: false,
|
|
390
|
+
message: () => `Expected event "${received.eventName}" to have been received with detail, but no events were received`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const firstEvent = received.firstEvent;
|
|
394
|
+
const pass = safeEquals(firstEvent.detail, detail);
|
|
395
|
+
return {
|
|
396
|
+
pass,
|
|
397
|
+
message: () => pass
|
|
398
|
+
? `Expected first event detail not to equal ${safeStringify(detail)}`
|
|
399
|
+
: `Expected first event detail to equal ${safeStringify(detail)}, but got ${safeStringify(firstEvent.detail)}`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Check if the last received event has the expected detail (alias for toHaveReceivedEventDetail)
|
|
404
|
+
*/
|
|
405
|
+
export function toHaveLastReceivedEventDetail(received, detail) {
|
|
406
|
+
return toHaveReceivedEventDetail(received, detail);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if the event at a specific index has the expected detail
|
|
410
|
+
*/
|
|
411
|
+
export function toHaveNthReceivedEventDetail(received, index, detail) {
|
|
412
|
+
if (received.length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
pass: false,
|
|
415
|
+
message: () => `Expected event "${received.eventName}" to have been received with detail, but no events were received`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (index < 0 || index >= received.length) {
|
|
419
|
+
return {
|
|
420
|
+
pass: false,
|
|
421
|
+
message: () => `Expected event at index ${index}, but only ${received.length} events were received`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const event = received.events[index];
|
|
425
|
+
const pass = safeEquals(event.detail, detail);
|
|
426
|
+
return {
|
|
427
|
+
pass,
|
|
428
|
+
message: () => pass
|
|
429
|
+
? `Expected event at index ${index} detail not to equal ${safeStringify(detail)}`
|
|
430
|
+
: `Expected event at index ${index} detail to equal ${safeStringify(detail)}, but got ${safeStringify(event.detail)}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Install custom matchers
|
|
435
|
+
*/
|
|
436
|
+
export function installMatchers() {
|
|
437
|
+
expect.extend({
|
|
438
|
+
toHaveClass,
|
|
439
|
+
toHaveClasses,
|
|
440
|
+
toMatchClasses,
|
|
441
|
+
toHaveAttribute,
|
|
442
|
+
toEqualAttribute,
|
|
443
|
+
toEqualAttributes,
|
|
444
|
+
toHaveProperty,
|
|
445
|
+
toHaveTextContent,
|
|
446
|
+
toEqualText,
|
|
447
|
+
toBeVisible,
|
|
448
|
+
toHaveShadowRoot,
|
|
449
|
+
toEqualHtml,
|
|
450
|
+
toEqualLightHtml,
|
|
451
|
+
toHaveReceivedEvent,
|
|
452
|
+
toHaveReceivedEventTimes,
|
|
453
|
+
toHaveReceivedEventDetail,
|
|
454
|
+
toHaveFirstReceivedEventDetail,
|
|
455
|
+
toHaveLastReceivedEventDetail,
|
|
456
|
+
toHaveNthReceivedEventDetail,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
// Auto-install matchers when this module is imported
|
|
460
|
+
installMatchers();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RenderResult } from '../types.js';
|
|
2
|
+
interface RenderOptions {
|
|
3
|
+
clearStage?: boolean;
|
|
4
|
+
stageAttrs?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Render using Stencil's render
|
|
8
|
+
*/
|
|
9
|
+
export declare function render<T extends HTMLElement = HTMLElement>(vnode: any, options?: RenderOptions): Promise<RenderResult<T>>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +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;AAE1D,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAKD;;GAEG;AACH,wBAAsB,MAAM,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,KAAK,EAAE,GAAG,EACV,OAAO,GAAE,aAGR,GACA,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAyH1B"}
|