@afixt/test-utils 1.0.0 → 1.0.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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(ls:*)",
5
+ "Bash(npm install:*)",
6
+ "Bash(npm run docs:*)",
7
+ "Bash(npm test)",
8
+ "Bash(npm ls:*)",
9
+ "Bash(npm test:*)",
10
+ "Bash(npm run test:*)",
11
+ "Bash(git add:*)"
12
+ ]
13
+ },
14
+ "enableAllProjectMcpServers": false
15
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "scripts": {
6
6
  "test": "vitest run",
7
7
  "test:watch": "vitest",
8
8
  "test:coverage": "vitest run --coverage",
9
9
  "test:single": "vitest run --testNamePattern",
10
+ "test:browser": "vitest run --config vitest.config.browser.js",
11
+ "test:browser:watch": "vitest --config vitest.config.browser.js",
12
+ "test:all": "npm run test && npm run test:browser",
10
13
  "test:generate-stubs": "node test/generate-test-stubs.js",
11
14
  "docs": "jsdoc -c jsdoc.json",
12
15
  "docs:serve": "npx http-server ./docs"
@@ -17,6 +20,9 @@
17
20
  },
18
21
  "author": "Karl Groves <karl.groves@afixt.com>",
19
22
  "license": "UNLICENSED",
23
+ "engines": {
24
+ "node": ">=22.0.0"
25
+ },
20
26
  "dependencies": {
21
27
  "franc": "^6.2.0",
22
28
  "tesseract.js": "^6.0.0"
@@ -27,6 +33,8 @@
27
33
  "clean-jsdoc-theme": "^4.3.0",
28
34
  "jsdoc": "^4.0.4",
29
35
  "jsdom": "^26.0.0",
30
- "vitest": "^3.0.9"
36
+ "puppeteer": "^24.10.1",
37
+ "vitest": "^3.0.9",
38
+ "vitest-environment-puppeteer": "^11.0.3"
31
39
  }
32
40
  }
@@ -17,6 +17,7 @@ export function getCSSGeneratedContent(el, pseudoElement = 'both') {
17
17
  if (el.classList.contains('with-before')) return 'Before Content';
18
18
  if (el.classList.contains('with-both')) return pseudoElement === 'both' ? 'Before Text After Text' : 'Before Text';
19
19
  if (el.classList.contains('with-quotes')) return 'Quoted Text';
20
+ if (el.classList.contains('url-content')) return 'url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")';
20
21
  }
21
22
 
22
23
  if (pseudoElement === 'after' || pseudoElement === 'both') {
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const eventListenersMap = new WeakMap();
4
+
5
+ const originalAddEventListener = EventTarget.prototype.addEventListener;
6
+ const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
7
+
8
+ // Override addEventListener to track event listeners
9
+ EventTarget.prototype.addEventListener = function (type, listener, options) {
10
+ if (!eventListenersMap.has(this)) {
11
+ eventListenersMap.set(this, {});
12
+ }
13
+ const listeners = eventListenersMap.get(this);
14
+
15
+ if (!listeners[type]) {
16
+ listeners[type] = [];
17
+ }
18
+ listeners[type].push({ listener, options });
19
+
20
+ return originalAddEventListener.call(this, type, listener, options);
21
+ };
22
+
23
+ // Override removeEventListener to remove from tracking
24
+ EventTarget.prototype.removeEventListener = function (type, listener, options) {
25
+ if (eventListenersMap.has(this)) {
26
+ const listeners = eventListenersMap.get(this);
27
+ if (listeners[type]) {
28
+ listeners[type] = listeners[type].filter((l) => l.listener !== listener);
29
+ }
30
+ }
31
+ return originalRemoveEventListener.call(this, type, listener, options);
32
+ };
33
+
34
+ // Function to get XPath of an element
35
+ const getXPath = (element) => {
36
+ if (element.id) {
37
+ return `//*[@id="${element.id}"]`;
38
+ }
39
+ let path = '';
40
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
41
+ let index = 0;
42
+ let sibling = element;
43
+ while (sibling) {
44
+ if (sibling.nodeName === element.nodeName) {
45
+ index++;
46
+ }
47
+ sibling = sibling.previousElementSibling;
48
+ }
49
+ path = `/${element.nodeName.toLowerCase()}[${index}]` + path;
50
+ element = element.parentNode;
51
+ }
52
+ return path;
53
+ };
54
+
55
+ // Function to get all event listeners on an element
56
+ const getEventListeners = (element) => {
57
+ return eventListenersMap.get(element) || {};
58
+ };
59
+
60
+ /**
61
+ * List all event listeners for an element and its children
62
+ * @param {Element} rootElement - The root element to start listing from (defaults to document)
63
+ * @returns {Array} Array of event listener objects with element, xpath, and event properties
64
+ */
65
+ const listEventListeners = (rootElement = document) => {
66
+ const eventListeners = [];
67
+
68
+ /**
69
+ * Process an element to extract its event listeners
70
+ * @param {Element} el - The element to process
71
+ */
72
+ function processElement(el) {
73
+ const listeners = getEventListeners(el);
74
+ if (Object.keys(listeners).length > 0) {
75
+ Object.keys(listeners).forEach((eventName) => {
76
+ eventListeners.push({
77
+ element: el.tagName.toLowerCase(),
78
+ xpath: getXPath(el),
79
+ event: eventName
80
+ });
81
+ });
82
+ }
83
+ }
84
+
85
+ // Process root element
86
+ processElement(rootElement);
87
+
88
+ // Process child elements
89
+ rootElement.querySelectorAll('*').forEach(processElement);
90
+
91
+ return eventListeners;
92
+ };
93
+
94
+ // Export functions
95
+ module.exports = { listEventListeners, getEventListeners };
@@ -0,0 +1,68 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock getComputedStyle to simulate real browser behavior for CSS tests
4
+ const originalGetComputedStyle = window.getComputedStyle;
5
+
6
+ window.getComputedStyle = vi.fn((element, pseudoElement) => {
7
+ // Handle pseudo-elements
8
+ if (pseudoElement) {
9
+ const classList = element.classList;
10
+
11
+ // Helper function to create mock computed style
12
+ const createMockStyle = (contentValue) => ({
13
+ content: contentValue,
14
+ getPropertyValue: (prop) => prop === 'content' ? contentValue : ''
15
+ });
16
+
17
+ // Simulate ::before pseudo-element content
18
+ if (pseudoElement === '::before') {
19
+ if (classList.contains('with-before')) {
20
+ return createMockStyle('"Before Content"');
21
+ }
22
+ if (classList.contains('with-both')) {
23
+ return createMockStyle('"Before Text"');
24
+ }
25
+ if (classList.contains('with-quotes')) {
26
+ return createMockStyle('"\'Quoted Text\'"');
27
+ }
28
+ if (classList.contains('empty-content')) {
29
+ return createMockStyle('""');
30
+ }
31
+ if (classList.contains('no-content')) {
32
+ return createMockStyle('none');
33
+ }
34
+ if (classList.contains('url-content')) {
35
+ return createMockStyle('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
36
+ }
37
+ // Default: no content
38
+ return createMockStyle('none');
39
+ }
40
+
41
+ // Simulate ::after pseudo-element content
42
+ if (pseudoElement === '::after') {
43
+ if (classList.contains('with-after')) {
44
+ return createMockStyle('"After Content"');
45
+ }
46
+ if (classList.contains('with-both')) {
47
+ return createMockStyle('"After Text"');
48
+ }
49
+ // Default: no content
50
+ return createMockStyle('none');
51
+ }
52
+ }
53
+
54
+ // For regular elements, try to use original if available or return default styles
55
+ try {
56
+ return originalGetComputedStyle.call(window, element, pseudoElement);
57
+ } catch (e) {
58
+ // Fallback for JSDOM limitations
59
+ return {
60
+ display: 'block',
61
+ visibility: 'visible',
62
+ opacity: '1',
63
+ position: 'static',
64
+ width: 'auto',
65
+ height: 'auto'
66
+ };
67
+ }
68
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { getCSSGeneratedContent } from '../src/getCSSGeneratedContent';
3
+
4
+ describe('getCSSGeneratedContent (Browser)', () => {
5
+ beforeEach(() => {
6
+ document.body.innerHTML = '';
7
+
8
+ // Create a style element for pseudo-elements
9
+ const style = document.createElement('style');
10
+ style.innerHTML = `
11
+ .with-before::before {
12
+ content: "Before Content";
13
+ }
14
+ .with-after::after {
15
+ content: "After Content";
16
+ }
17
+ .with-both::before {
18
+ content: "Before Text";
19
+ }
20
+ .with-both::after {
21
+ content: "After Text";
22
+ }
23
+ .with-quotes::before {
24
+ content: "'Quoted Text'";
25
+ }
26
+ .empty-content::before {
27
+ content: "";
28
+ }
29
+ .no-content::before {
30
+ content: none;
31
+ }
32
+ .url-content::before {
33
+ content: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
34
+ }
35
+ `;
36
+ document.head.appendChild(style);
37
+ });
38
+
39
+ describe('::before pseudo-element', () => {
40
+ it('should detect text content in ::before pseudo-element', () => {
41
+ const div = document.createElement('div');
42
+ div.className = 'with-before';
43
+ document.body.appendChild(div);
44
+
45
+ const result = getCSSGeneratedContent(div);
46
+ expect(result).toBe('Before Content');
47
+ });
48
+
49
+ it('should detect quoted text content in ::before pseudo-element', () => {
50
+ const div = document.createElement('div');
51
+ div.className = 'with-quotes';
52
+ document.body.appendChild(div);
53
+
54
+ const result = getCSSGeneratedContent(div);
55
+ expect(result).toBe('Quoted Text');
56
+ });
57
+
58
+ it('should return false for empty content in ::before', () => {
59
+ const div = document.createElement('div');
60
+ div.className = 'empty-content';
61
+ document.body.appendChild(div);
62
+
63
+ const result = getCSSGeneratedContent(div);
64
+ expect(result).toBe(false);
65
+ });
66
+
67
+ it('should return false for content: none in ::before', () => {
68
+ const div = document.createElement('div');
69
+ div.className = 'no-content';
70
+ document.body.appendChild(div);
71
+
72
+ const result = getCSSGeneratedContent(div);
73
+ expect(result).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('::after pseudo-element', () => {
78
+ it('should detect text content in ::after pseudo-element', () => {
79
+ const div = document.createElement('div');
80
+ div.className = 'with-after';
81
+ document.body.appendChild(div);
82
+
83
+ const result = getCSSGeneratedContent(div);
84
+ expect(result).toBe('After Content');
85
+ });
86
+ });
87
+
88
+ describe('multiple pseudo-elements', () => {
89
+ it('should detect content when both ::before and ::after have content', () => {
90
+ const div = document.createElement('div');
91
+ div.className = 'with-both';
92
+ document.body.appendChild(div);
93
+
94
+ const result = getCSSGeneratedContent(div);
95
+ expect(result).toBe('Before Text After Text');
96
+ });
97
+ });
98
+
99
+ describe('no generated content', () => {
100
+ it('should return false for elements with no generated content', () => {
101
+ const div = document.createElement('div');
102
+ document.body.appendChild(div);
103
+
104
+ const result = getCSSGeneratedContent(div);
105
+ expect(result).toBe(false);
106
+ });
107
+
108
+ it('should return false for elements that do not exist', () => {
109
+ const result = getCSSGeneratedContent(null);
110
+ expect(result).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('complex content types', () => {
115
+ it('should handle URL content', () => {
116
+ const div = document.createElement('div');
117
+ div.className = 'url-content';
118
+ document.body.appendChild(div);
119
+
120
+ const result = getCSSGeneratedContent(div);
121
+ // URL content is preserved as-is (no quote stripping for URLs)
122
+ expect(result).toBe('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+
3
+ describe('listEventListeners', () => {
4
+ let listEventListeners;
5
+ let getEventListeners;
6
+ let originalAddEventListener;
7
+ let originalRemoveEventListener;
8
+
9
+ beforeEach(async () => {
10
+ // Store original prototypes before importing the module
11
+ originalAddEventListener = EventTarget.prototype.addEventListener;
12
+ originalRemoveEventListener = EventTarget.prototype.removeEventListener;
13
+
14
+ // Clear the body for each test
15
+ document.body.innerHTML = '';
16
+
17
+ // Dynamically import the module
18
+ const module = await import('../src/listEventListeners.js?' + Date.now());
19
+ listEventListeners = module.listEventListeners;
20
+ getEventListeners = module.getEventListeners;
21
+ });
22
+
23
+ afterEach(() => {
24
+ // Restore original methods
25
+ EventTarget.prototype.addEventListener = originalAddEventListener;
26
+ EventTarget.prototype.removeEventListener = originalRemoveEventListener;
27
+ });
28
+
29
+ describe('addEventListener override', () => {
30
+ it('should track added event listeners', () => {
31
+ const element = document.createElement('button');
32
+ const listener = vi.fn();
33
+
34
+ element.addEventListener('click', listener);
35
+
36
+ const listeners = getEventListeners(element);
37
+ expect(listeners.click).toBeDefined();
38
+ expect(listeners.click.length).toBe(1);
39
+ expect(listeners.click[0].listener).toBe(listener);
40
+ });
41
+
42
+ it('should track multiple listeners for the same event', () => {
43
+ const element = document.createElement('div');
44
+ const listener1 = vi.fn();
45
+ const listener2 = vi.fn();
46
+
47
+ element.addEventListener('click', listener1);
48
+ element.addEventListener('click', listener2);
49
+
50
+ const listeners = getEventListeners(element);
51
+ expect(listeners.click.length).toBe(2);
52
+ expect(listeners.click[0].listener).toBe(listener1);
53
+ expect(listeners.click[1].listener).toBe(listener2);
54
+ });
55
+
56
+ it('should track listeners for different event types', () => {
57
+ const element = document.createElement('input');
58
+ const clickListener = vi.fn();
59
+ const focusListener = vi.fn();
60
+
61
+ element.addEventListener('click', clickListener);
62
+ element.addEventListener('focus', focusListener);
63
+
64
+ const listeners = getEventListeners(element);
65
+ expect(listeners.click).toBeDefined();
66
+ expect(listeners.focus).toBeDefined();
67
+ expect(listeners.click[0].listener).toBe(clickListener);
68
+ expect(listeners.focus[0].listener).toBe(focusListener);
69
+ });
70
+
71
+ it('should track options parameter', () => {
72
+ const element = document.createElement('div');
73
+ const listener = vi.fn();
74
+ const options = { capture: true, once: true };
75
+
76
+ element.addEventListener('click', listener, options);
77
+
78
+ const listeners = getEventListeners(element);
79
+ expect(listeners.click[0].options).toEqual(options);
80
+ });
81
+
82
+ it('should call original addEventListener', () => {
83
+ const element = document.createElement('button');
84
+ const listener = vi.fn();
85
+
86
+ element.addEventListener('click', listener);
87
+
88
+ // Verify the event listener was actually attached by triggering it
89
+ element.click();
90
+ expect(listener).toHaveBeenCalled();
91
+ });
92
+ });
93
+
94
+ describe('removeEventListener override', () => {
95
+ it('should remove tracked event listeners', () => {
96
+ const element = document.createElement('button');
97
+ const listener = vi.fn();
98
+
99
+ element.addEventListener('click', listener);
100
+ element.removeEventListener('click', listener);
101
+
102
+ const listeners = getEventListeners(element);
103
+ expect(listeners.click.length).toBe(0);
104
+ });
105
+
106
+ it('should only remove the specified listener', () => {
107
+ const element = document.createElement('div');
108
+ const listener1 = vi.fn();
109
+ const listener2 = vi.fn();
110
+
111
+ element.addEventListener('click', listener1);
112
+ element.addEventListener('click', listener2);
113
+ element.removeEventListener('click', listener1);
114
+
115
+ const listeners = getEventListeners(element);
116
+ expect(listeners.click.length).toBe(1);
117
+ expect(listeners.click[0].listener).toBe(listener2);
118
+ });
119
+
120
+ it('should handle removing non-existent listeners gracefully', () => {
121
+ const element = document.createElement('button');
122
+ const listener = vi.fn();
123
+
124
+ expect(() => {
125
+ element.removeEventListener('click', listener);
126
+ }).not.toThrow();
127
+ });
128
+
129
+ it('should call original removeEventListener', () => {
130
+ const element = document.createElement('button');
131
+ const listener = vi.fn();
132
+
133
+ element.addEventListener('click', listener);
134
+ element.removeEventListener('click', listener);
135
+
136
+ // Verify the event listener was actually removed by triggering it
137
+ element.click();
138
+ expect(listener).not.toHaveBeenCalled();
139
+ });
140
+ });
141
+
142
+ describe('getXPath', () => {
143
+ it('should return XPath for element with id', () => {
144
+ const element = document.createElement('div');
145
+ element.id = 'test-id';
146
+ document.body.appendChild(element);
147
+
148
+ const result = listEventListeners(document.body);
149
+ element.addEventListener('click', () => {});
150
+
151
+ const listeners = listEventListeners(document.body);
152
+ expect(listeners[0].xpath).toBe('//*[@id="test-id"]');
153
+ });
154
+
155
+ it('should generate XPath for elements without id', () => {
156
+ const container = document.createElement('div');
157
+ const child = document.createElement('span');
158
+ container.appendChild(child);
159
+ document.body.appendChild(container);
160
+
161
+ child.addEventListener('click', () => {});
162
+
163
+ const listeners = listEventListeners(document.body);
164
+ const spanListener = listeners.find(l => l.element === 'span');
165
+ expect(spanListener.xpath).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/span\[1\]$/);
166
+ });
167
+
168
+ it('should handle multiple siblings correctly', () => {
169
+ const container = document.createElement('div');
170
+ const span1 = document.createElement('span');
171
+ const span2 = document.createElement('span');
172
+ const span3 = document.createElement('span');
173
+
174
+ container.appendChild(span1);
175
+ container.appendChild(span2);
176
+ container.appendChild(span3);
177
+ document.body.appendChild(container);
178
+
179
+ span2.addEventListener('click', () => {});
180
+
181
+ const listeners = listEventListeners(document.body);
182
+ const spanListener = listeners.find(l => l.element === 'span');
183
+ expect(spanListener.xpath).toMatch(/span\[2\]/);
184
+ });
185
+ });
186
+
187
+ describe('getEventListeners', () => {
188
+ it('should return empty object for elements without listeners', () => {
189
+ const element = document.createElement('div');
190
+ const listeners = getEventListeners(element);
191
+
192
+ expect(listeners).toEqual({});
193
+ });
194
+
195
+ it('should return all event listeners for an element', () => {
196
+ const element = document.createElement('button');
197
+ const clickListener = vi.fn();
198
+ const focusListener = vi.fn();
199
+
200
+ element.addEventListener('click', clickListener);
201
+ element.addEventListener('focus', focusListener);
202
+
203
+ const listeners = getEventListeners(element);
204
+ expect(Object.keys(listeners)).toHaveLength(2);
205
+ expect(listeners.click).toBeDefined();
206
+ expect(listeners.focus).toBeDefined();
207
+ });
208
+ });
209
+
210
+ describe('listEventListeners', () => {
211
+ it('should return empty array when no event listeners exist', () => {
212
+ const result = listEventListeners(document.body);
213
+ expect(result).toEqual([]);
214
+ });
215
+
216
+ it('should list event listeners on root element', () => {
217
+ const listener = vi.fn();
218
+ document.body.addEventListener('click', listener);
219
+
220
+ const result = listEventListeners(document.body);
221
+ expect(result).toHaveLength(1);
222
+ expect(result[0]).toEqual({
223
+ element: 'body',
224
+ xpath: expect.any(String),
225
+ event: 'click'
226
+ });
227
+ });
228
+
229
+ it('should list event listeners on child elements', () => {
230
+ const div = document.createElement('div');
231
+ const button = document.createElement('button');
232
+ div.appendChild(button);
233
+ document.body.appendChild(div);
234
+
235
+ button.addEventListener('click', vi.fn());
236
+
237
+ const result = listEventListeners(document.body);
238
+ expect(result).toHaveLength(1);
239
+ expect(result[0]).toEqual({
240
+ element: 'button',
241
+ xpath: expect.any(String),
242
+ event: 'click'
243
+ });
244
+ });
245
+
246
+ it('should list multiple event types on same element', () => {
247
+ const input = document.createElement('input');
248
+ document.body.appendChild(input);
249
+
250
+ input.addEventListener('click', vi.fn());
251
+ input.addEventListener('focus', vi.fn());
252
+ input.addEventListener('blur', vi.fn());
253
+
254
+ const result = listEventListeners(document.body);
255
+ expect(result).toHaveLength(3);
256
+
257
+ const events = result.map(r => r.event).sort();
258
+ expect(events).toEqual(['blur', 'click', 'focus']);
259
+ });
260
+
261
+ it('should list listeners from multiple elements', () => {
262
+ const div1 = document.createElement('div');
263
+ const div2 = document.createElement('div');
264
+ const button = document.createElement('button');
265
+
266
+ div1.appendChild(button);
267
+ document.body.appendChild(div1);
268
+ document.body.appendChild(div2);
269
+
270
+ div1.addEventListener('mouseover', vi.fn());
271
+ div2.addEventListener('mouseout', vi.fn());
272
+ button.addEventListener('click', vi.fn());
273
+
274
+ const result = listEventListeners(document.body);
275
+ expect(result).toHaveLength(3);
276
+
277
+ const elements = result.map(r => r.element).sort();
278
+ expect(elements).toEqual(['button', 'div', 'div']);
279
+ });
280
+
281
+ it('should use document as default root element', () => {
282
+ const button = document.createElement('button');
283
+ document.body.appendChild(button);
284
+ button.addEventListener('click', vi.fn());
285
+
286
+ const result = listEventListeners();
287
+ const buttonListener = result.find(l => l.element === 'button');
288
+ expect(buttonListener).toBeDefined();
289
+ expect(buttonListener.event).toBe('click');
290
+ });
291
+
292
+ it('should work with custom root element', () => {
293
+ const container = document.createElement('div');
294
+ const innerDiv = document.createElement('div');
295
+ const button = document.createElement('button');
296
+
297
+ innerDiv.appendChild(button);
298
+ container.appendChild(innerDiv);
299
+ document.body.appendChild(container);
300
+
301
+ // Add listeners to elements inside and outside container
302
+ button.addEventListener('click', vi.fn());
303
+ document.body.addEventListener('scroll', vi.fn());
304
+
305
+ const result = listEventListeners(container);
306
+ expect(result).toHaveLength(1);
307
+ expect(result[0].element).toBe('button');
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'jsdom',
7
+ include: ['test/**/*.browser.test.js'],
8
+ exclude: ['test/_template.test.js'],
9
+ setupFiles: ['./test/browser-setup.js'],
10
+ coverage: {
11
+ provider: 'v8',
12
+ reporter: ['text', 'json', 'html'],
13
+ exclude: ['**/node_modules/**', 'test/**'],
14
+ reportsDirectory: './coverage-browser',
15
+ },
16
+ },
17
+ });