@coherent.js/tooling 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/lsp/analysis/coherent-analyzer.d.ts +93 -0
- package/dist/lsp/analysis/coherent-analyzer.d.ts.map +1 -0
- package/dist/lsp/analysis/coherent-analyzer.js +288 -0
- package/dist/lsp/analysis/coherent-analyzer.js.map +1 -0
- package/dist/lsp/analysis/element-validator.d.ts +45 -0
- package/dist/lsp/analysis/element-validator.d.ts.map +1 -0
- package/dist/lsp/analysis/element-validator.js +84 -0
- package/dist/lsp/analysis/element-validator.js.map +1 -0
- package/dist/lsp/analysis/nesting-validator.d.ts +49 -0
- package/dist/lsp/analysis/nesting-validator.d.ts.map +1 -0
- package/dist/lsp/analysis/nesting-validator.js +68 -0
- package/dist/lsp/analysis/nesting-validator.js.map +1 -0
- package/dist/lsp/data/element-attributes.d.ts +92 -0
- package/dist/lsp/data/element-attributes.d.ts.map +1 -0
- package/dist/lsp/data/element-attributes.generated.json +7085 -0
- package/dist/lsp/data/element-attributes.js +282 -0
- package/dist/lsp/data/element-attributes.js.map +1 -0
- package/dist/lsp/data/nesting-rules.d.ts +67 -0
- package/dist/lsp/data/nesting-rules.d.ts.map +1 -0
- package/dist/lsp/data/nesting-rules.js +240 -0
- package/dist/lsp/data/nesting-rules.js.map +1 -0
- package/dist/lsp/providers/code-actions.d.ts +15 -0
- package/dist/lsp/providers/code-actions.d.ts.map +1 -0
- package/dist/lsp/providers/code-actions.js +191 -0
- package/dist/lsp/providers/code-actions.js.map +1 -0
- package/dist/lsp/providers/completion.d.ts +15 -0
- package/dist/lsp/providers/completion.d.ts.map +1 -0
- package/dist/lsp/providers/completion.js +247 -0
- package/dist/lsp/providers/completion.js.map +1 -0
- package/dist/lsp/providers/diagnostics.d.ts +26 -0
- package/dist/lsp/providers/diagnostics.d.ts.map +1 -0
- package/dist/lsp/providers/diagnostics.js +143 -0
- package/dist/lsp/providers/diagnostics.js.map +1 -0
- package/dist/lsp/providers/hover.d.ts +15 -0
- package/dist/lsp/providers/hover.d.ts.map +1 -0
- package/dist/lsp/providers/hover.js +215 -0
- package/dist/lsp/providers/hover.js.map +1 -0
- package/dist/lsp/server.d.ts +17 -0
- package/dist/lsp/server.d.ts.map +1 -0
- package/dist/lsp/server.js +82 -0
- package/dist/lsp/server.js.map +1 -0
- package/dist/testing/index.js +746 -0
- package/dist/testing/index.js.map +7 -0
- package/dist/testing/matchers.js +246 -0
- package/dist/testing/matchers.js.map +7 -0
- package/dist/testing/test-renderer.js +254 -0
- package/dist/testing/test-renderer.js.map +7 -0
- package/dist/testing/test-utils.js +262 -0
- package/dist/testing/test-utils.js.map +7 -0
- package/package.json +96 -0
- package/types/testing/index.d.ts +404 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// src/testing/test-renderer.js
|
|
2
|
+
import { render } from "@coherent.js/core";
|
|
3
|
+
var TestRendererResult = class {
|
|
4
|
+
constructor(component, html, container = null) {
|
|
5
|
+
this.component = component;
|
|
6
|
+
this.html = html;
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.queries = /* @__PURE__ */ new Map();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get element by test ID
|
|
12
|
+
* @param {string} testId - Test ID to search for
|
|
13
|
+
* @returns {Object|null} Element or null
|
|
14
|
+
*/
|
|
15
|
+
getByTestId(testId) {
|
|
16
|
+
const regex = new RegExp(`data-testid="${testId}"[^>]*>([^<]*)<`, "i");
|
|
17
|
+
const match = this.html.match(regex);
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error(`Unable to find element with testId: ${testId}`);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
text: match[1],
|
|
23
|
+
html: match[0],
|
|
24
|
+
testId,
|
|
25
|
+
exists: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Query element by test ID (returns null if not found)
|
|
30
|
+
* @param {string} testId - Test ID to search for
|
|
31
|
+
* @returns {Object|null} Element or null
|
|
32
|
+
*/
|
|
33
|
+
queryByTestId(testId) {
|
|
34
|
+
try {
|
|
35
|
+
return this.getByTestId(testId);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get element by text content
|
|
42
|
+
* @param {string|RegExp} text - Text to search for
|
|
43
|
+
* @returns {Object} Element
|
|
44
|
+
*/
|
|
45
|
+
getByText(text) {
|
|
46
|
+
const regex = typeof text === "string" ? new RegExp(`>([^<]*${text}[^<]*)<`, "i") : new RegExp(`>([^<]*)<`, "i");
|
|
47
|
+
const match = this.html.match(regex);
|
|
48
|
+
if (!match || typeof text === "string" && !match[1].includes(text)) {
|
|
49
|
+
throw new Error(`Unable to find element with text: ${text}`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
text: match[1],
|
|
53
|
+
html: match[0],
|
|
54
|
+
exists: true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Query element by text (returns null if not found)
|
|
59
|
+
* @param {string|RegExp} text - Text to search for
|
|
60
|
+
* @returns {Object|null} Element or null
|
|
61
|
+
*/
|
|
62
|
+
queryByText(text) {
|
|
63
|
+
try {
|
|
64
|
+
return this.getByText(text);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get element by class name
|
|
71
|
+
* @param {string} className - Class name to search for
|
|
72
|
+
* @returns {Object} Element
|
|
73
|
+
*/
|
|
74
|
+
getByClassName(className) {
|
|
75
|
+
const regex = new RegExp(`class="[^"]*${className}[^"]*"[^>]*>([^<]*)<`, "i");
|
|
76
|
+
const match = this.html.match(regex);
|
|
77
|
+
if (!match) {
|
|
78
|
+
throw new Error(`Unable to find element with className: ${className}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
text: match[1],
|
|
82
|
+
html: match[0],
|
|
83
|
+
className,
|
|
84
|
+
exists: true
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Query element by class name (returns null if not found)
|
|
89
|
+
* @param {string} className - Class name to search for
|
|
90
|
+
* @returns {Object|null} Element or null
|
|
91
|
+
*/
|
|
92
|
+
queryByClassName(className) {
|
|
93
|
+
try {
|
|
94
|
+
return this.getByClassName(className);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get all elements by tag name
|
|
101
|
+
* @param {string} tagName - Tag name to search for
|
|
102
|
+
* @returns {Array<Object>} Array of elements
|
|
103
|
+
*/
|
|
104
|
+
getAllByTagName(tagName) {
|
|
105
|
+
const regex = new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`, "gi");
|
|
106
|
+
const matches = [...this.html.matchAll(regex)];
|
|
107
|
+
return matches.map((match) => ({
|
|
108
|
+
text: match[1],
|
|
109
|
+
html: match[0],
|
|
110
|
+
tagName,
|
|
111
|
+
exists: true
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if element exists
|
|
116
|
+
* @param {string} selector - Selector (testId, text, className)
|
|
117
|
+
* @param {string} type - Type of selector ('testId', 'text', 'className')
|
|
118
|
+
* @returns {boolean} True if exists
|
|
119
|
+
*/
|
|
120
|
+
exists(selector, type = "testId") {
|
|
121
|
+
switch (type) {
|
|
122
|
+
case "testId":
|
|
123
|
+
return this.queryByTestId(selector) !== null;
|
|
124
|
+
case "text":
|
|
125
|
+
return this.queryByText(selector) !== null;
|
|
126
|
+
case "className":
|
|
127
|
+
return this.queryByClassName(selector) !== null;
|
|
128
|
+
default:
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get the rendered HTML
|
|
134
|
+
* @returns {string} HTML string
|
|
135
|
+
*/
|
|
136
|
+
getHTML() {
|
|
137
|
+
return this.html;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get the component
|
|
141
|
+
* @returns {Object} Component object
|
|
142
|
+
*/
|
|
143
|
+
getComponent() {
|
|
144
|
+
return this.component;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a snapshot of the rendered output
|
|
148
|
+
* @returns {string} Formatted HTML for snapshot testing
|
|
149
|
+
*/
|
|
150
|
+
toSnapshot() {
|
|
151
|
+
return this.html.replace(/>\s+</g, "><").trim();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Debug: print the rendered HTML
|
|
155
|
+
*/
|
|
156
|
+
debug() {
|
|
157
|
+
console.log("=== Rendered HTML ===");
|
|
158
|
+
console.log(this.html);
|
|
159
|
+
console.log("=== Component ===");
|
|
160
|
+
console.log(JSON.stringify(this.component, null, 2));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
function renderComponent(component, options = {}) {
|
|
164
|
+
const html = render(component, options);
|
|
165
|
+
return new TestRendererResult(component, html);
|
|
166
|
+
}
|
|
167
|
+
async function renderComponentAsync(component, props = {}, options = {}) {
|
|
168
|
+
const resolvedComponent = typeof component === "function" ? await component(props) : component;
|
|
169
|
+
const html = render(resolvedComponent, options);
|
|
170
|
+
return new TestRendererResult(resolvedComponent, html);
|
|
171
|
+
}
|
|
172
|
+
var TestRenderer = class {
|
|
173
|
+
constructor(component, options = {}) {
|
|
174
|
+
this.component = component;
|
|
175
|
+
this.options = options;
|
|
176
|
+
this.result = null;
|
|
177
|
+
this.renderCount = 0;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Render the component
|
|
181
|
+
* @returns {TestRendererResult} Render result
|
|
182
|
+
*/
|
|
183
|
+
render() {
|
|
184
|
+
this.renderCount++;
|
|
185
|
+
const html = render(this.component, this.options);
|
|
186
|
+
this.result = new TestRendererResult(this.component, html);
|
|
187
|
+
return this.result;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Update the component and re-render
|
|
191
|
+
* @param {Object} newComponent - Updated component
|
|
192
|
+
* @returns {TestRendererResult} Render result
|
|
193
|
+
*/
|
|
194
|
+
update(newComponent) {
|
|
195
|
+
this.component = newComponent;
|
|
196
|
+
return this.render();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get the current result
|
|
200
|
+
* @returns {TestRendererResult|null} Current result
|
|
201
|
+
*/
|
|
202
|
+
getResult() {
|
|
203
|
+
return this.result;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get render count
|
|
207
|
+
* @returns {number} Number of renders
|
|
208
|
+
*/
|
|
209
|
+
getRenderCount() {
|
|
210
|
+
return this.renderCount;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Unmount the component
|
|
214
|
+
*/
|
|
215
|
+
unmount() {
|
|
216
|
+
this.component = null;
|
|
217
|
+
this.result = null;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function createTestRenderer(component, options = {}) {
|
|
221
|
+
return new TestRenderer(component, options);
|
|
222
|
+
}
|
|
223
|
+
function shallowRender(component) {
|
|
224
|
+
const shallow = { ...component };
|
|
225
|
+
Object.keys(shallow).forEach((key) => {
|
|
226
|
+
if (shallow[key] && typeof shallow[key] === "object") {
|
|
227
|
+
if (shallow[key].children) {
|
|
228
|
+
shallow[key] = {
|
|
229
|
+
...shallow[key],
|
|
230
|
+
children: Array.isArray(shallow[key].children) ? shallow[key].children.map(() => ({ _shallow: true })) : { _shallow: true }
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return shallow;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/testing/test-utils.js
|
|
239
|
+
function fireEvent(element, eventType, eventData = {}) {
|
|
240
|
+
if (!element) {
|
|
241
|
+
throw new Error("Element is required for fireEvent");
|
|
242
|
+
}
|
|
243
|
+
const event = {
|
|
244
|
+
type: eventType,
|
|
245
|
+
target: element,
|
|
246
|
+
currentTarget: element,
|
|
247
|
+
preventDefault: () => {
|
|
248
|
+
},
|
|
249
|
+
stopPropagation: () => {
|
|
250
|
+
},
|
|
251
|
+
...eventData
|
|
252
|
+
};
|
|
253
|
+
const handlerName = `on${eventType}`;
|
|
254
|
+
if (element[handlerName] && typeof element[handlerName] === "function") {
|
|
255
|
+
element[handlerName](event);
|
|
256
|
+
}
|
|
257
|
+
return event;
|
|
258
|
+
}
|
|
259
|
+
var fireEvent_click = (element, eventData) => fireEvent(element, "click", eventData);
|
|
260
|
+
var fireEvent_change = (element, value) => fireEvent(element, "change", { target: { value } });
|
|
261
|
+
var fireEvent_input = (element, value) => fireEvent(element, "input", { target: { value } });
|
|
262
|
+
var fireEvent_keyDown = (element, key) => fireEvent(element, "keydown", { key });
|
|
263
|
+
var fireEvent_keyUp = (element, key) => fireEvent(element, "keyup", { key });
|
|
264
|
+
var fireEvent_focus = (element) => fireEvent(element, "focus");
|
|
265
|
+
var fireEvent_blur = (element) => fireEvent(element, "blur");
|
|
266
|
+
function waitFor(condition, options = {}) {
|
|
267
|
+
const { timeout = 1e3, interval = 50 } = options;
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
const check = () => {
|
|
271
|
+
try {
|
|
272
|
+
if (condition()) {
|
|
273
|
+
resolve();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
if (Date.now() - startTime >= timeout) {
|
|
279
|
+
reject(new Error(`Timeout waiting for condition after ${timeout}ms`));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
setTimeout(check, interval);
|
|
283
|
+
};
|
|
284
|
+
check();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async function waitForElement(queryFn, options = {}) {
|
|
288
|
+
let element = null;
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
element = queryFn();
|
|
291
|
+
return element !== null;
|
|
292
|
+
}, options);
|
|
293
|
+
return element;
|
|
294
|
+
}
|
|
295
|
+
async function waitForElementToBeRemoved(queryFn, options = {}) {
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
const element = queryFn();
|
|
298
|
+
return element === null;
|
|
299
|
+
}, options);
|
|
300
|
+
}
|
|
301
|
+
async function act(callback) {
|
|
302
|
+
await callback();
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
304
|
+
}
|
|
305
|
+
function createMock(implementation) {
|
|
306
|
+
const calls = [];
|
|
307
|
+
const results = [];
|
|
308
|
+
const mockFn = function(...args) {
|
|
309
|
+
calls.push(args);
|
|
310
|
+
let result;
|
|
311
|
+
let error;
|
|
312
|
+
try {
|
|
313
|
+
result = implementation ? implementation(...args) : void 0;
|
|
314
|
+
results.push({ type: "return", value: result });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
error = err;
|
|
317
|
+
results.push({ type: "throw", value: error });
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
};
|
|
322
|
+
mockFn.mock = {
|
|
323
|
+
calls,
|
|
324
|
+
results,
|
|
325
|
+
instances: []
|
|
326
|
+
};
|
|
327
|
+
mockFn.mockClear = () => {
|
|
328
|
+
calls.length = 0;
|
|
329
|
+
results.length = 0;
|
|
330
|
+
};
|
|
331
|
+
mockFn.mockReset = () => {
|
|
332
|
+
mockFn.mockClear();
|
|
333
|
+
implementation = void 0;
|
|
334
|
+
};
|
|
335
|
+
mockFn.mockImplementation = (fn) => {
|
|
336
|
+
implementation = fn;
|
|
337
|
+
return mockFn;
|
|
338
|
+
};
|
|
339
|
+
mockFn.mockReturnValue = (value) => {
|
|
340
|
+
implementation = () => value;
|
|
341
|
+
return mockFn;
|
|
342
|
+
};
|
|
343
|
+
mockFn.mockResolvedValue = (value) => {
|
|
344
|
+
implementation = () => Promise.resolve(value);
|
|
345
|
+
return mockFn;
|
|
346
|
+
};
|
|
347
|
+
mockFn.mockRejectedValue = (error) => {
|
|
348
|
+
implementation = () => Promise.reject(error);
|
|
349
|
+
return mockFn;
|
|
350
|
+
};
|
|
351
|
+
return mockFn;
|
|
352
|
+
}
|
|
353
|
+
function createSpy(object, method) {
|
|
354
|
+
const original = object[method];
|
|
355
|
+
const spy = createMock(original.bind(object));
|
|
356
|
+
object[method] = spy;
|
|
357
|
+
spy.mockRestore = () => {
|
|
358
|
+
object[method] = original;
|
|
359
|
+
};
|
|
360
|
+
return spy;
|
|
361
|
+
}
|
|
362
|
+
function cleanup() {
|
|
363
|
+
}
|
|
364
|
+
function within(container) {
|
|
365
|
+
return {
|
|
366
|
+
getByTestId: (testId) => container.getByTestId(testId),
|
|
367
|
+
queryByTestId: (testId) => container.queryByTestId(testId),
|
|
368
|
+
getByText: (text) => container.getByText(text),
|
|
369
|
+
queryByText: (text) => container.queryByText(text),
|
|
370
|
+
getByClassName: (className) => container.getByClassName(className),
|
|
371
|
+
queryByClassName: (className) => container.queryByClassName(className)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
var screen = {
|
|
375
|
+
_result: null,
|
|
376
|
+
setResult(result) {
|
|
377
|
+
this._result = result;
|
|
378
|
+
},
|
|
379
|
+
getByTestId(testId) {
|
|
380
|
+
if (!this._result) throw new Error("No component rendered");
|
|
381
|
+
return this._result.getByTestId(testId);
|
|
382
|
+
},
|
|
383
|
+
queryByTestId(testId) {
|
|
384
|
+
if (!this._result) return null;
|
|
385
|
+
return this._result.queryByTestId(testId);
|
|
386
|
+
},
|
|
387
|
+
getByText(text) {
|
|
388
|
+
if (!this._result) throw new Error("No component rendered");
|
|
389
|
+
return this._result.getByText(text);
|
|
390
|
+
},
|
|
391
|
+
queryByText(text) {
|
|
392
|
+
if (!this._result) return null;
|
|
393
|
+
return this._result.queryByText(text);
|
|
394
|
+
},
|
|
395
|
+
getByClassName(className) {
|
|
396
|
+
if (!this._result) throw new Error("No component rendered");
|
|
397
|
+
return this._result.getByClassName(className);
|
|
398
|
+
},
|
|
399
|
+
queryByClassName(className) {
|
|
400
|
+
if (!this._result) return null;
|
|
401
|
+
return this._result.queryByClassName(className);
|
|
402
|
+
},
|
|
403
|
+
debug() {
|
|
404
|
+
if (this._result) {
|
|
405
|
+
this._result.debug();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var userEvent = {
|
|
410
|
+
/**
|
|
411
|
+
* Simulate user typing
|
|
412
|
+
*/
|
|
413
|
+
type: async (element, text, options = {}) => {
|
|
414
|
+
const { delay = 0 } = options;
|
|
415
|
+
for (const char of text) {
|
|
416
|
+
fireEvent_keyDown(element, char);
|
|
417
|
+
fireEvent_input(element, element.value + char);
|
|
418
|
+
fireEvent_keyUp(element, char);
|
|
419
|
+
if (delay > 0) {
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
/**
|
|
425
|
+
* Simulate user click
|
|
426
|
+
*/
|
|
427
|
+
click: async (element) => {
|
|
428
|
+
fireEvent_focus(element);
|
|
429
|
+
fireEvent_click(element);
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
432
|
+
* Simulate user double click
|
|
433
|
+
*/
|
|
434
|
+
dblClick: async (element) => {
|
|
435
|
+
await userEvent.click(element);
|
|
436
|
+
await userEvent.click(element);
|
|
437
|
+
},
|
|
438
|
+
/**
|
|
439
|
+
* Simulate user clearing input
|
|
440
|
+
*/
|
|
441
|
+
clear: async (element) => {
|
|
442
|
+
fireEvent_input(element, "");
|
|
443
|
+
fireEvent_change(element, "");
|
|
444
|
+
},
|
|
445
|
+
/**
|
|
446
|
+
* Simulate user selecting option
|
|
447
|
+
*/
|
|
448
|
+
selectOptions: async (element, values) => {
|
|
449
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
450
|
+
fireEvent_change(element, valueArray[0]);
|
|
451
|
+
},
|
|
452
|
+
/**
|
|
453
|
+
* Simulate user tab navigation
|
|
454
|
+
*/
|
|
455
|
+
tab: async () => {
|
|
456
|
+
const activeElement = document.activeElement;
|
|
457
|
+
if (activeElement) {
|
|
458
|
+
fireEvent_keyDown(activeElement, "Tab");
|
|
459
|
+
fireEvent_blur(activeElement);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/testing/matchers.js
|
|
465
|
+
var customMatchers = {
|
|
466
|
+
/**
|
|
467
|
+
* Check if element has specific text
|
|
468
|
+
*/
|
|
469
|
+
toHaveText(received, expected) {
|
|
470
|
+
const pass = received && received.text === expected;
|
|
471
|
+
return {
|
|
472
|
+
pass,
|
|
473
|
+
message: () => pass ? `Expected element not to have text "${expected}"` : `Expected element to have text "${expected}", but got "${received?.text || "null"}"`
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
/**
|
|
477
|
+
* Check if element contains text
|
|
478
|
+
*/
|
|
479
|
+
toContainText(received, expected) {
|
|
480
|
+
const pass = received && received.text && received.text.includes(expected);
|
|
481
|
+
return {
|
|
482
|
+
pass,
|
|
483
|
+
message: () => pass ? `Expected element not to contain text "${expected}"` : `Expected element to contain text "${expected}", but got "${received?.text || "null"}"`
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
/**
|
|
487
|
+
* Check if element has specific class
|
|
488
|
+
*/
|
|
489
|
+
toHaveClass(received, expected) {
|
|
490
|
+
const pass = received && received.className && received.className.includes(expected);
|
|
491
|
+
return {
|
|
492
|
+
pass,
|
|
493
|
+
message: () => pass ? `Expected element not to have class "${expected}"` : `Expected element to have class "${expected}", but got "${received?.className || "null"}"`
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
/**
|
|
497
|
+
* Check if element exists
|
|
498
|
+
*/
|
|
499
|
+
toBeInTheDocument(received) {
|
|
500
|
+
const pass = received && received.exists === true;
|
|
501
|
+
return {
|
|
502
|
+
pass,
|
|
503
|
+
message: () => pass ? "Expected element not to be in the document" : "Expected element to be in the document"
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
/**
|
|
507
|
+
* Check if element is visible (has content)
|
|
508
|
+
*/
|
|
509
|
+
toBeVisible(received) {
|
|
510
|
+
const pass = received && received.text && received.text.trim().length > 0;
|
|
511
|
+
return {
|
|
512
|
+
pass,
|
|
513
|
+
message: () => pass ? "Expected element not to be visible" : "Expected element to be visible (have text content)"
|
|
514
|
+
};
|
|
515
|
+
},
|
|
516
|
+
/**
|
|
517
|
+
* Check if element is empty
|
|
518
|
+
*/
|
|
519
|
+
toBeEmpty(received) {
|
|
520
|
+
const pass = !received || !received.text || received.text.trim().length === 0;
|
|
521
|
+
return {
|
|
522
|
+
pass,
|
|
523
|
+
message: () => pass ? "Expected element not to be empty" : "Expected element to be empty"
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
/**
|
|
527
|
+
* Check if HTML contains specific string
|
|
528
|
+
*/
|
|
529
|
+
toContainHTML(received, expected) {
|
|
530
|
+
const html = received?.html || received;
|
|
531
|
+
const pass = typeof html === "string" && html.includes(expected);
|
|
532
|
+
return {
|
|
533
|
+
pass,
|
|
534
|
+
message: () => pass ? `Expected HTML not to contain "${expected}"` : `Expected HTML to contain "${expected}"`
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
/**
|
|
538
|
+
* Check if element has attribute
|
|
539
|
+
*/
|
|
540
|
+
toHaveAttribute(received, attribute, value) {
|
|
541
|
+
const html = received?.html || "";
|
|
542
|
+
const regex = new RegExp(`${attribute}="([^"]*)"`, "i");
|
|
543
|
+
const match = html.match(regex);
|
|
544
|
+
const pass = value !== void 0 ? match && match[1] === value : match !== null;
|
|
545
|
+
return {
|
|
546
|
+
pass,
|
|
547
|
+
message: () => {
|
|
548
|
+
if (value !== void 0) {
|
|
549
|
+
return pass ? `Expected element not to have attribute ${attribute}="${value}"` : `Expected element to have attribute ${attribute}="${value}", but got "${match?.[1] || "none"}"`;
|
|
550
|
+
}
|
|
551
|
+
return pass ? `Expected element not to have attribute ${attribute}` : `Expected element to have attribute ${attribute}`;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
/**
|
|
556
|
+
* Check if component matches snapshot
|
|
557
|
+
*/
|
|
558
|
+
toMatchSnapshot(received) {
|
|
559
|
+
const _snapshot = received?.toSnapshot ? received.toSnapshot() : received;
|
|
560
|
+
return {
|
|
561
|
+
pass: true,
|
|
562
|
+
message: () => "Snapshot comparison"
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
/**
|
|
566
|
+
* Check if element has specific tag name
|
|
567
|
+
*/
|
|
568
|
+
toHaveTagName(received, tagName) {
|
|
569
|
+
const html = received?.html || "";
|
|
570
|
+
const regex = new RegExp(`<${tagName}[^>]*>`, "i");
|
|
571
|
+
const pass = regex.test(html);
|
|
572
|
+
return {
|
|
573
|
+
pass,
|
|
574
|
+
message: () => pass ? `Expected element not to have tag name "${tagName}"` : `Expected element to have tag name "${tagName}"`
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
/**
|
|
578
|
+
* Check if render result contains element
|
|
579
|
+
*/
|
|
580
|
+
toContainElement(received, element) {
|
|
581
|
+
const html = received?.html || received;
|
|
582
|
+
const elementHtml = element?.html || element;
|
|
583
|
+
const pass = typeof html === "string" && html.includes(elementHtml);
|
|
584
|
+
return {
|
|
585
|
+
pass,
|
|
586
|
+
message: () => pass ? "Expected not to contain element" : "Expected to contain element"
|
|
587
|
+
};
|
|
588
|
+
},
|
|
589
|
+
/**
|
|
590
|
+
* Check if mock was called
|
|
591
|
+
*/
|
|
592
|
+
toHaveBeenCalled(received) {
|
|
593
|
+
const pass = received?.mock?.calls?.length > 0;
|
|
594
|
+
return {
|
|
595
|
+
pass,
|
|
596
|
+
message: () => pass ? "Expected mock not to have been called" : "Expected mock to have been called"
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
/**
|
|
600
|
+
* Check if mock was called with specific args
|
|
601
|
+
*/
|
|
602
|
+
toHaveBeenCalledWith(received, ...expectedArgs) {
|
|
603
|
+
const calls = received?.mock?.calls || [];
|
|
604
|
+
const pass = calls.some(
|
|
605
|
+
(call) => call.length === expectedArgs.length && call.every((arg, i) => arg === expectedArgs[i])
|
|
606
|
+
);
|
|
607
|
+
return {
|
|
608
|
+
pass,
|
|
609
|
+
message: () => pass ? `Expected mock not to have been called with ${JSON.stringify(expectedArgs)}` : `Expected mock to have been called with ${JSON.stringify(expectedArgs)}`
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
/**
|
|
613
|
+
* Check if mock was called N times
|
|
614
|
+
*/
|
|
615
|
+
toHaveBeenCalledTimes(received, times) {
|
|
616
|
+
const callCount = received?.mock?.calls?.length || 0;
|
|
617
|
+
const pass = callCount === times;
|
|
618
|
+
return {
|
|
619
|
+
pass,
|
|
620
|
+
message: () => pass ? `Expected mock not to have been called ${times} times` : `Expected mock to have been called ${times} times, but was called ${callCount} times`
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
/**
|
|
624
|
+
* Check if component rendered successfully
|
|
625
|
+
*/
|
|
626
|
+
toRenderSuccessfully(received) {
|
|
627
|
+
const pass = received && received.html && received.html.length > 0;
|
|
628
|
+
return {
|
|
629
|
+
pass,
|
|
630
|
+
message: () => pass ? "Expected component not to render successfully" : "Expected component to render successfully"
|
|
631
|
+
};
|
|
632
|
+
},
|
|
633
|
+
/**
|
|
634
|
+
* Check if HTML is valid
|
|
635
|
+
*/
|
|
636
|
+
toBeValidHTML(received) {
|
|
637
|
+
const html = received?.html || received;
|
|
638
|
+
const openTags = (html.match(/<[^/][^>]*>/g) || []).length;
|
|
639
|
+
const closeTags = (html.match(/<\/[^>]+>/g) || []).length;
|
|
640
|
+
const selfClosing = (html.match(/<[^>]+\/>/g) || []).length;
|
|
641
|
+
const pass = openTags === closeTags + selfClosing;
|
|
642
|
+
return {
|
|
643
|
+
pass,
|
|
644
|
+
message: () => pass ? "Expected HTML not to be valid" : `Expected HTML to be valid (open: ${openTags}, close: ${closeTags}, self-closing: ${selfClosing})`
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
function extendExpect(expect) {
|
|
649
|
+
if (expect && expect.extend) {
|
|
650
|
+
expect.extend(customMatchers);
|
|
651
|
+
} else {
|
|
652
|
+
console.warn("Could not extend expect - expect.extend not available");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
var assertions = {
|
|
656
|
+
/**
|
|
657
|
+
* Assert element has text
|
|
658
|
+
*/
|
|
659
|
+
assertHasText(element, text) {
|
|
660
|
+
if (!element || element.text !== text) {
|
|
661
|
+
throw new Error(`Expected element to have text "${text}", but got "${element?.text || "null"}"`);
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
/**
|
|
665
|
+
* Assert element exists
|
|
666
|
+
*/
|
|
667
|
+
assertExists(element) {
|
|
668
|
+
if (!element || !element.exists) {
|
|
669
|
+
throw new Error("Expected element to exist");
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
/**
|
|
673
|
+
* Assert element has class
|
|
674
|
+
*/
|
|
675
|
+
assertHasClass(element, className) {
|
|
676
|
+
if (!element || !element.className || !element.className.includes(className)) {
|
|
677
|
+
throw new Error(`Expected element to have class "${className}"`);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
/**
|
|
681
|
+
* Assert HTML contains string
|
|
682
|
+
*/
|
|
683
|
+
assertContainsHTML(html, substring) {
|
|
684
|
+
const htmlString = html?.html || html;
|
|
685
|
+
if (!htmlString || !htmlString.includes(substring)) {
|
|
686
|
+
throw new Error(`Expected HTML to contain "${substring}"`);
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
/**
|
|
690
|
+
* Assert component rendered
|
|
691
|
+
*/
|
|
692
|
+
assertRendered(result) {
|
|
693
|
+
if (!result || !result.html || result.html.length === 0) {
|
|
694
|
+
throw new Error("Expected component to render");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
// src/testing/index.js
|
|
700
|
+
var index_default = {
|
|
701
|
+
// Renderer
|
|
702
|
+
renderComponent,
|
|
703
|
+
renderComponentAsync,
|
|
704
|
+
createTestRenderer,
|
|
705
|
+
shallowRender,
|
|
706
|
+
// Utilities
|
|
707
|
+
fireEvent,
|
|
708
|
+
waitFor,
|
|
709
|
+
waitForElement,
|
|
710
|
+
waitForElementToBeRemoved,
|
|
711
|
+
act,
|
|
712
|
+
createMock,
|
|
713
|
+
createSpy,
|
|
714
|
+
cleanup,
|
|
715
|
+
within,
|
|
716
|
+
screen,
|
|
717
|
+
userEvent,
|
|
718
|
+
// Matchers
|
|
719
|
+
customMatchers,
|
|
720
|
+
extendExpect,
|
|
721
|
+
assertions
|
|
722
|
+
};
|
|
723
|
+
export {
|
|
724
|
+
TestRenderer,
|
|
725
|
+
TestRendererResult,
|
|
726
|
+
act,
|
|
727
|
+
assertions,
|
|
728
|
+
cleanup,
|
|
729
|
+
createMock,
|
|
730
|
+
createSpy,
|
|
731
|
+
createTestRenderer,
|
|
732
|
+
customMatchers,
|
|
733
|
+
index_default as default,
|
|
734
|
+
extendExpect,
|
|
735
|
+
fireEvent,
|
|
736
|
+
renderComponent,
|
|
737
|
+
renderComponentAsync,
|
|
738
|
+
screen,
|
|
739
|
+
shallowRender,
|
|
740
|
+
userEvent,
|
|
741
|
+
waitFor,
|
|
742
|
+
waitForElement,
|
|
743
|
+
waitForElementToBeRemoved,
|
|
744
|
+
within
|
|
745
|
+
};
|
|
746
|
+
//# sourceMappingURL=index.js.map
|