@coherent.js/testing 1.0.0-beta.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Thomas Drouvin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,446 @@
1
+ # @coherent.js/testing
2
+
3
+ Complete testing utilities for Coherent.js applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @coherent.js/testing
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - ✅ **Test Renderer** - Render components in test environment
14
+ - ✅ **Query Utilities** - Find elements by testId, text, className
15
+ - ✅ **Event Simulation** - Simulate user interactions
16
+ - ✅ **Async Testing** - Wait for conditions and elements
17
+ - ✅ **Mock Functions** - Create mocks and spies
18
+ - ✅ **Custom Matchers** - Coherent.js-specific assertions
19
+ - ✅ **Snapshot Testing** - Component snapshot support
20
+ - ✅ **User Events** - Realistic user interaction simulation
21
+
22
+ ## Quick Start
23
+
24
+ ```javascript
25
+ import { describe, it, expect } from 'vitest';
26
+ import { renderComponent, extendExpect } from '@coherent.js/testing';
27
+
28
+ // Extend expect with custom matchers
29
+ extendExpect(expect);
30
+
31
+ describe('MyComponent', () => {
32
+ it('should render correctly', () => {
33
+ const component = {
34
+ div: {
35
+ 'data-testid': 'my-div',
36
+ text: 'Hello World'
37
+ }
38
+ };
39
+
40
+ const { getByTestId } = renderComponent(component);
41
+
42
+ expect(getByTestId('my-div')).toHaveText('Hello World');
43
+ });
44
+ });
45
+ ```
46
+
47
+ ## API Reference
48
+
49
+ ### Rendering
50
+
51
+ #### `renderComponent(component, options)`
52
+
53
+ Render a component for testing.
54
+
55
+ ```javascript
56
+ const { getByTestId, getByText } = renderComponent({
57
+ div: { 'data-testid': 'test', text: 'Hello' }
58
+ });
59
+ ```
60
+
61
+ #### `renderComponentAsync(component, props, options)`
62
+
63
+ Render an async component.
64
+
65
+ ```javascript
66
+ const result = await renderComponentAsync(asyncComponent, { name: 'World' });
67
+ ```
68
+
69
+ #### `createTestRenderer(component, options)`
70
+
71
+ Create a test renderer for component updates.
72
+
73
+ ```javascript
74
+ const renderer = createTestRenderer(component);
75
+ renderer.render();
76
+ renderer.update(newComponent);
77
+ ```
78
+
79
+ #### `shallowRender(component)`
80
+
81
+ Shallow render (top-level only).
82
+
83
+ ```javascript
84
+ const shallow = shallowRender(component);
85
+ ```
86
+
87
+ ### Queries
88
+
89
+ #### `getByTestId(testId)`
90
+
91
+ Get element by test ID (throws if not found).
92
+
93
+ ```javascript
94
+ const element = getByTestId('submit-btn');
95
+ ```
96
+
97
+ #### `queryByTestId(testId)`
98
+
99
+ Query element by test ID (returns null if not found).
100
+
101
+ ```javascript
102
+ const element = queryByTestId('submit-btn');
103
+ ```
104
+
105
+ #### `getByText(text)`
106
+
107
+ Get element by text content.
108
+
109
+ ```javascript
110
+ const element = getByText('Submit');
111
+ ```
112
+
113
+ #### `getByClassName(className)`
114
+
115
+ Get element by class name.
116
+
117
+ ```javascript
118
+ const element = getByClassName('btn-primary');
119
+ ```
120
+
121
+ #### `getAllByTagName(tagName)`
122
+
123
+ Get all elements by tag name.
124
+
125
+ ```javascript
126
+ const items = getAllByTagName('li');
127
+ ```
128
+
129
+ ### Events
130
+
131
+ #### `fireEvent(element, eventType, eventData)`
132
+
133
+ Simulate an event.
134
+
135
+ ```javascript
136
+ fireEvent(button, 'click');
137
+ fireEvent(input, 'change', { target: { value: 'test' } });
138
+ ```
139
+
140
+ #### `userEvent`
141
+
142
+ Realistic user interactions.
143
+
144
+ ```javascript
145
+ await userEvent.click(button);
146
+ await userEvent.type(input, 'Hello');
147
+ await userEvent.clear(input);
148
+ ```
149
+
150
+ ### Async Utilities
151
+
152
+ #### `waitFor(condition, options)`
153
+
154
+ Wait for a condition to be true.
155
+
156
+ ```javascript
157
+ await waitFor(() => getByText('Loaded').exists, { timeout: 2000 });
158
+ ```
159
+
160
+ #### `waitForElement(queryFn, options)`
161
+
162
+ Wait for an element to appear.
163
+
164
+ ```javascript
165
+ const element = await waitForElement(() => queryByTestId('loaded'));
166
+ ```
167
+
168
+ #### `waitForElementToBeRemoved(queryFn, options)`
169
+
170
+ Wait for an element to disappear.
171
+
172
+ ```javascript
173
+ await waitForElementToBeRemoved(() => queryByTestId('loading'));
174
+ ```
175
+
176
+ #### `act(callback)`
177
+
178
+ Batch updates.
179
+
180
+ ```javascript
181
+ await act(async () => {
182
+ // Perform updates
183
+ });
184
+ ```
185
+
186
+ ### Mocks & Spies
187
+
188
+ #### `createMock(implementation)`
189
+
190
+ Create a mock function.
191
+
192
+ ```javascript
193
+ const mock = createMock((x) => x * 2);
194
+ mock(5); // returns 10
195
+
196
+ expect(mock).toHaveBeenCalledWith(5);
197
+ expect(mock).toHaveBeenCalledTimes(1);
198
+ ```
199
+
200
+ #### `createSpy(object, method)`
201
+
202
+ Spy on an object method.
203
+
204
+ ```javascript
205
+ const spy = createSpy(obj, 'method');
206
+ obj.method('test');
207
+ expect(spy).toHaveBeenCalledWith('test');
208
+ spy.mockRestore();
209
+ ```
210
+
211
+ ### Custom Matchers
212
+
213
+ Extend expect with Coherent.js-specific matchers:
214
+
215
+ ```javascript
216
+ import { extendExpect } from '@coherent.js/testing';
217
+ extendExpect(expect);
218
+ ```
219
+
220
+ Available matchers:
221
+
222
+ - `toHaveText(text)` - Element has exact text
223
+ - `toContainText(text)` - Element contains text
224
+ - `toHaveClass(className)` - Element has class
225
+ - `toBeInTheDocument()` - Element exists
226
+ - `toBeVisible()` - Element has visible content
227
+ - `toBeEmpty()` - Element is empty
228
+ - `toContainHTML(html)` - HTML contains string
229
+ - `toHaveAttribute(attr, value)` - Element has attribute
230
+ - `toHaveTagName(tagName)` - Element has tag name
231
+ - `toRenderSuccessfully()` - Component rendered
232
+ - `toBeValidHTML()` - HTML is valid
233
+ - `toHaveBeenCalled()` - Mock was called
234
+ - `toHaveBeenCalledWith(...args)` - Mock called with args
235
+ - `toHaveBeenCalledTimes(n)` - Mock called n times
236
+
237
+ ### Utilities
238
+
239
+ #### `within(container)`
240
+
241
+ Scope queries to a container.
242
+
243
+ ```javascript
244
+ const container = getByTestId('container');
245
+ const scoped = within(container);
246
+ scoped.getByText('Inner text');
247
+ ```
248
+
249
+ #### `screen`
250
+
251
+ Global query utility.
252
+
253
+ ```javascript
254
+ screen.setResult(result);
255
+ screen.getByTestId('test');
256
+ screen.debug();
257
+ ```
258
+
259
+ #### `cleanup()`
260
+
261
+ Clean up after tests.
262
+
263
+ ```javascript
264
+ afterEach(() => {
265
+ cleanup();
266
+ });
267
+ ```
268
+
269
+ ## Examples
270
+
271
+ ### Testing a Button Component
272
+
273
+ ```javascript
274
+ import { renderComponent, fireEvent, createMock } from '@coherent.js/testing';
275
+
276
+ it('should handle click events', () => {
277
+ const handleClick = createMock();
278
+
279
+ const button = {
280
+ button: {
281
+ 'data-testid': 'my-btn',
282
+ text: 'Click me',
283
+ onclick: handleClick
284
+ }
285
+ };
286
+
287
+ const { getByTestId } = renderComponent(button);
288
+ fireEvent(getByTestId('my-btn'), 'click');
289
+
290
+ expect(handleClick).toHaveBeenCalled();
291
+ });
292
+ ```
293
+
294
+ ### Testing Async Components
295
+
296
+ ```javascript
297
+ import { renderComponentAsync, waitForElement } from '@coherent.js/testing';
298
+
299
+ it('should load data', async () => {
300
+ const AsyncComponent = async () => {
301
+ const data = await fetchData();
302
+ return { div: { text: data.message } };
303
+ };
304
+
305
+ const result = await renderComponentAsync(AsyncComponent);
306
+ const element = await waitForElement(() => result.queryByText('Loaded'));
307
+
308
+ expect(element).toBeInTheDocument();
309
+ });
310
+ ```
311
+
312
+ ### Testing Forms
313
+
314
+ ```javascript
315
+ import { renderComponent, userEvent } from '@coherent.js/testing';
316
+
317
+ it('should submit form', async () => {
318
+ const handleSubmit = createMock();
319
+
320
+ const form = {
321
+ form: {
322
+ onsubmit: handleSubmit,
323
+ children: [
324
+ { input: { 'data-testid': 'name-input', type: 'text' } },
325
+ { button: { 'data-testid': 'submit-btn', text: 'Submit' } }
326
+ ]
327
+ }
328
+ };
329
+
330
+ const { getByTestId } = renderComponent(form);
331
+
332
+ await userEvent.type(getByTestId('name-input'), 'John');
333
+ await userEvent.click(getByTestId('submit-btn'));
334
+
335
+ expect(handleSubmit).toHaveBeenCalled();
336
+ });
337
+ ```
338
+
339
+ ### Snapshot Testing
340
+
341
+ ```javascript
342
+ import { renderComponent } from '@coherent.js/testing';
343
+
344
+ it('should match snapshot', () => {
345
+ const component = {
346
+ div: {
347
+ className: 'card',
348
+ children: [
349
+ { h2: { text: 'Title' } },
350
+ { p: { text: 'Content' } }
351
+ ]
352
+ }
353
+ };
354
+
355
+ const result = renderComponent(component);
356
+ expect(result.toSnapshot()).toMatchSnapshot();
357
+ });
358
+ ```
359
+
360
+ ## Best Practices
361
+
362
+ ### 1. Use Test IDs
363
+
364
+ Add `data-testid` attributes for reliable querying:
365
+
366
+ ```javascript
367
+ const component = {
368
+ button: {
369
+ 'data-testid': 'submit-button',
370
+ text: 'Submit'
371
+ }
372
+ };
373
+ ```
374
+
375
+ ### 2. Clean Up After Tests
376
+
377
+ Always clean up to avoid test interference:
378
+
379
+ ```javascript
380
+ afterEach(() => {
381
+ cleanup();
382
+ });
383
+ ```
384
+
385
+ ### 3. Use Custom Matchers
386
+
387
+ Extend expect for better assertions:
388
+
389
+ ```javascript
390
+ extendExpect(expect);
391
+ expect(element).toHaveText('Hello');
392
+ ```
393
+
394
+ ### 4. Test User Interactions
395
+
396
+ Use `userEvent` for realistic interactions:
397
+
398
+ ```javascript
399
+ await userEvent.click(button);
400
+ await userEvent.type(input, 'text');
401
+ ```
402
+
403
+ ### 5. Wait for Async Updates
404
+
405
+ Use `waitFor` for async operations:
406
+
407
+ ```javascript
408
+ await waitFor(() => getByText('Loaded').exists);
409
+ ```
410
+
411
+ ## Integration with Testing Frameworks
412
+
413
+ ### Vitest
414
+
415
+ ```javascript
416
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
417
+ import { renderComponent, extendExpect, cleanup } from '@coherent.js/testing';
418
+
419
+ extendExpect(expect);
420
+
421
+ describe('MyComponent', () => {
422
+ afterEach(cleanup);
423
+
424
+ it('should work', () => {
425
+ // Test code
426
+ });
427
+ });
428
+ ```
429
+
430
+ ### Jest
431
+
432
+ ```javascript
433
+ import { renderComponent, extendExpect, cleanup } from '@coherent.js/testing';
434
+
435
+ extendExpect(expect);
436
+
437
+ afterEach(cleanup);
438
+
439
+ test('should work', () => {
440
+ // Test code
441
+ });
442
+ ```
443
+
444
+ ## License
445
+
446
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,746 @@
1
+ // src/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/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/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/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
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@coherent.js/testing",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "Testing utilities for Coherent.js applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./renderer": "./dist/test-renderer.js",
10
+ "./utils": "./dist/test-utils.js",
11
+ "./matchers": "./dist/matchers.js"
12
+ },
13
+ "keywords": [
14
+ "coherent",
15
+ "testing",
16
+ "test-utils",
17
+ "component-testing"
18
+ ],
19
+ "author": "Coherent.js Team",
20
+ "license": "MIT",
21
+ "peerDependencies": {
22
+ "@coherent.js/core": "1.0.0-beta.2"
23
+ },
24
+ "devDependencies": {
25
+ "vitest": "^1.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/Tomdrouv1/coherent.js.git"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "types": "./types/index.d.ts",
35
+ "files": [
36
+ "LICENSE",
37
+ "README.md",
38
+ "types/"
39
+ ],
40
+ "scripts": {
41
+ "build": "node build.mjs",
42
+ "clean": "rm -rf dist",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest"
45
+ }
46
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Coherent.js Testing Utilities TypeScript Definitions
3
+ * @module @coherent.js/testing
4
+ */
5
+
6
+ // ===== Test Renderer Types =====
7
+
8
+ export interface RenderOptions {
9
+ wrapper?: any;
10
+ context?: Record<string, any>;
11
+ props?: Record<string, any>;
12
+ }
13
+
14
+ export interface TestRendererResult {
15
+ html: string;
16
+ component: any;
17
+ container: HTMLElement | null;
18
+ rerender(component: any): void;
19
+ unmount(): void;
20
+ debug(): void;
21
+ }
22
+
23
+ export class TestRenderer {
24
+ render(component: any, options?: RenderOptions): TestRendererResult;
25
+ renderAsync(component: any, options?: RenderOptions): Promise<TestRendererResult>;
26
+ shallow(component: any): TestRendererResult;
27
+ cleanup(): void;
28
+ }
29
+
30
+ export function renderComponent(component: any, options?: RenderOptions): TestRendererResult;
31
+ export function renderComponentAsync(component: any, options?: RenderOptions): Promise<TestRendererResult>;
32
+ export function createTestRenderer(): TestRenderer;
33
+ export function shallowRender(component: any): TestRendererResult;
34
+
35
+ // ===== Test Utilities Types =====
36
+
37
+ export interface EventOptions {
38
+ bubbles?: boolean;
39
+ cancelable?: boolean;
40
+ composed?: boolean;
41
+ [key: string]: any;
42
+ }
43
+
44
+ export const fireEvent: {
45
+ (element: Element, event: Event): boolean;
46
+ click(element: Element, options?: EventOptions): boolean;
47
+ change(element: Element, options?: EventOptions & { target?: { value?: any } }): boolean;
48
+ input(element: Element, options?: EventOptions & { target?: { value?: any } }): boolean;
49
+ submit(element: Element, options?: EventOptions): boolean;
50
+ keyDown(element: Element, options?: EventOptions & { key?: string; code?: string }): boolean;
51
+ keyUp(element: Element, options?: EventOptions & { key?: string; code?: string }): boolean;
52
+ focus(element: Element, options?: EventOptions): boolean;
53
+ blur(element: Element, options?: EventOptions): boolean;
54
+ mouseEnter(element: Element, options?: EventOptions): boolean;
55
+ mouseLeave(element: Element, options?: EventOptions): boolean;
56
+ [key: string]: any;
57
+ };
58
+
59
+ export interface WaitOptions {
60
+ timeout?: number;
61
+ interval?: number;
62
+ }
63
+
64
+ export function waitFor<T>(callback: () => T | Promise<T>, options?: WaitOptions): Promise<T>;
65
+ export function waitForElement(selector: string, options?: WaitOptions): Promise<Element>;
66
+ export function waitForElementToBeRemoved(selector: string | Element, options?: WaitOptions): Promise<void>;
67
+
68
+ export function act<T>(callback: () => T | Promise<T>): Promise<T>;
69
+
70
+ export interface Mock<T extends (...args: any[]) => any = (...args: any[]) => any> {
71
+ (...args: Parameters<T>): ReturnType<T>;
72
+ mock: {
73
+ calls: Parameters<T>[];
74
+ results: Array<{ type: 'return' | 'throw'; value: any }>;
75
+ instances: any[];
76
+ };
77
+ mockClear(): void;
78
+ mockReset(): void;
79
+ mockRestore(): void;
80
+ mockImplementation(fn: T): this;
81
+ mockReturnValue(value: ReturnType<T>): this;
82
+ mockReturnValueOnce(value: ReturnType<T>): this;
83
+ mockResolvedValue(value: ReturnType<T> extends Promise<infer U> ? U : never): this;
84
+ mockRejectedValue(error: any): this;
85
+ }
86
+
87
+ export function createMock<T extends (...args: any[]) => any>(implementation?: T): Mock<T>;
88
+ export function createSpy<T extends (...args: any[]) => any>(object: any, method: string): Mock<T>;
89
+
90
+ export function cleanup(): void;
91
+
92
+ export interface Within {
93
+ getByText(text: string | RegExp): Element;
94
+ getByRole(role: string, options?: { name?: string | RegExp }): Element;
95
+ getByLabelText(text: string | RegExp): Element;
96
+ getByPlaceholderText(text: string | RegExp): Element;
97
+ getByTestId(testId: string): Element;
98
+ queryByText(text: string | RegExp): Element | null;
99
+ queryByRole(role: string, options?: { name?: string | RegExp }): Element | null;
100
+ queryAllByText(text: string | RegExp): Element[];
101
+ findByText(text: string | RegExp): Promise<Element>;
102
+ findAllByText(text: string | RegExp): Promise<Element[]>;
103
+ }
104
+
105
+ export function within(element: Element): Within;
106
+ export const screen: Within;
107
+
108
+ export const userEvent: {
109
+ click(element: Element): Promise<void>;
110
+ dblClick(element: Element): Promise<void>;
111
+ type(element: Element, text: string, options?: { delay?: number }): Promise<void>;
112
+ clear(element: Element): Promise<void>;
113
+ selectOptions(element: Element, values: string | string[]): Promise<void>;
114
+ tab(options?: { shift?: boolean }): Promise<void>;
115
+ hover(element: Element): Promise<void>;
116
+ unhover(element: Element): Promise<void>;
117
+ upload(element: Element, files: File | File[]): Promise<void>;
118
+ paste(element: Element, text: string): Promise<void>;
119
+ };
120
+
121
+ // ===== Matchers Types =====
122
+
123
+ export interface CustomMatchers<R = void> {
124
+ toHaveHTML(html: string): R;
125
+ toContainHTML(html: string): R;
126
+ toHaveTextContent(text: string | RegExp): R;
127
+ toHaveAttribute(attr: string, value?: string): R;
128
+ toHaveClass(className: string): R;
129
+ toBeInTheDocument(): R;
130
+ toBeVisible(): R;
131
+ toBeDisabled(): R;
132
+ toBeEnabled(): R;
133
+ toHaveValue(value: any): R;
134
+ toHaveStyle(style: Record<string, any>): R;
135
+ toHaveFocus(): R;
136
+ toBeChecked(): R;
137
+ toBeValid(): R;
138
+ toBeInvalid(): R;
139
+ }
140
+
141
+ export const customMatchers: CustomMatchers;
142
+
143
+ export function extendExpect(matchers: Record<string, (...args: any[]) => any>): void;
144
+
145
+ export const assertions: {
146
+ assertElement(element: any): asserts element is Element;
147
+ assertHTMLElement(element: any): asserts element is HTMLElement;
148
+ assertInDocument(element: Element | null): asserts element is Element;
149
+ assertVisible(element: Element): void;
150
+ assertHasAttribute(element: Element, attr: string): void;
151
+ assertHasClass(element: Element, className: string): void;
152
+ };
153
+
154
+ // Extend Jest/Vitest expect
155
+ declare global {
156
+ namespace Vi {
157
+ interface Matchers<R = void> extends CustomMatchers<R> {}
158
+ interface AsymmetricMatchers extends CustomMatchers {}
159
+ }
160
+ namespace jest {
161
+ interface Matchers<R = void> extends CustomMatchers<R> {}
162
+ interface Expect extends CustomMatchers {}
163
+ }
164
+ }