@afixt/test-utils 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/settings.local.json +3 -2
  2. package/.github/workflows/test.yml +26 -0
  3. package/BROWSER_TESTING.md +109 -0
  4. package/CLAUDE.md +10 -0
  5. package/package.json +6 -8
  6. package/playwright.config.js +27 -0
  7. package/src/getCSSGeneratedContent.js +9 -5
  8. package/src/getImageText.js +4 -1
  9. package/src/testContrast.js +5 -1
  10. package/test/__screenshots__/getImageText.test.js/getImageText-should-be-an-async-function-1.png +0 -0
  11. package/test/__screenshots__/getImageText.test.js/getImageText-should-be-defined-and-exported-from-the-module-1.png +0 -0
  12. package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-empty-string-input-gracefully-1.png +0 -0
  13. package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-invalid-image-paths-gracefully-1.png +0 -0
  14. package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-null-or-undefined-input-gracefully-1.png +0 -0
  15. package/test/__screenshots__/getImageText.test.js/getImageText-should-log-errors-in-non-test-environments-1.png +0 -0
  16. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-call-original-addEventListener-1.png +0 -0
  17. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-added-event-listeners-1.png +0 -0
  18. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-listeners-for-different-event-types-1.png +0 -0
  19. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-multiple-listeners-for-the-same-event-1.png +0 -0
  20. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-options-parameter-1.png +0 -0
  21. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getEventListeners-should-return-all-event-listeners-for-an-element-1.png +0 -0
  22. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getEventListeners-should-return-empty-object-for-elements-without-listeners-1.png +0 -0
  23. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-generate-XPath-for-elements-without-id-1.png +0 -0
  24. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-handle-multiple-siblings-correctly-1.png +0 -0
  25. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-return-XPath-for-element-with-id-1.png +0 -0
  26. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-event-listeners-on-child-elements-1.png +0 -0
  27. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-event-listeners-on-root-element-1.png +0 -0
  28. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-listeners-from-multiple-elements-1.png +0 -0
  29. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-multiple-event-types-on-same-element-1.png +0 -0
  30. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-return-empty-array-when-no-event-listeners-exist-1.png +0 -0
  31. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-use-document-as-default-root-element-1.png +0 -0
  32. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-work-with-custom-root-element-1.png +0 -0
  33. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-call-original-removeEventListener-1.png +0 -0
  34. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-handle-removing-non-existent-listeners-gracefully-1.png +0 -0
  35. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-only-remove-the-specified-listener-1.png +0 -0
  36. package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-remove-tracked-event-listeners-1.png +0 -0
  37. package/test/arrayUtils.test.js +22 -0
  38. package/test/domUtils.test.js +124 -0
  39. package/test/getImageText.test.js +37 -3
  40. package/test/getStyleObject.test.js +19 -1
  41. package/test/hasCSSGeneratedContent.test.js +7 -2
  42. package/test/hasValidAriaRole.test.js +64 -2
  43. package/test/isFocusable.test.js +94 -1
  44. package/test/isVisible.test.js +121 -3
  45. package/test/playwright/css-pseudo-elements.spec.js +155 -0
  46. package/test/playwright/fixtures/css-pseudo-elements.html +77 -0
  47. package/test/setup.js +9 -1
  48. package/test/stringUtils.test.js +44 -2
  49. package/test/testContrast.test.js +439 -2
  50. package/test/testLang.test.js +152 -11
  51. package/todo.md +7 -146
  52. package/vitest.config.js +8 -1
  53. package/test/browser-setup.js +0 -68
  54. package/vitest.config.browser.js +0 -17
@@ -84,15 +84,77 @@ describe('hasValidAriaRole', () => {
84
84
  it('should recognize various valid roles', () => {
85
85
  // Test a sample of different valid roles
86
86
  const validRoles = ['alert', 'button', 'checkbox', 'dialog', 'navigation', 'main', 'region'];
87
-
87
+
88
88
  validRoles.forEach(role => {
89
89
  // Arrange
90
90
  const element = document.createElement('div');
91
91
  element.setAttribute('role', role);
92
92
  document.body.appendChild(element);
93
-
93
+
94
94
  // Act & Assert
95
95
  expect(hasValidAriaRole(element)).toBe(true);
96
96
  });
97
97
  });
98
+
99
+ it('should recognize all widget roles', () => {
100
+ const widgetRoles = [
101
+ 'alertdialog', 'gridcell', 'link', 'log', 'marquee', 'menuitem',
102
+ 'menuitemcheckbox', 'menuitemradio', 'option', 'progressbar', 'radio',
103
+ 'scrollbar', 'searchbox', 'slider', 'spinbutton', 'status', 'switch',
104
+ 'tab', 'tabpanel', 'textbox', 'tooltip', 'treeitem'
105
+ ];
106
+
107
+ widgetRoles.forEach(role => {
108
+ const element = document.createElement('div');
109
+ element.setAttribute('role', role);
110
+ expect(hasValidAriaRole(element)).toBe(true);
111
+ });
112
+ });
113
+
114
+ it('should recognize all composite widget roles', () => {
115
+ const compositeRoles = [
116
+ 'combobox', 'grid', 'listbox', 'menu', 'menubar', 'radiogroup',
117
+ 'tablist', 'tree', 'treegrid'
118
+ ];
119
+
120
+ compositeRoles.forEach(role => {
121
+ const element = document.createElement('div');
122
+ element.setAttribute('role', role);
123
+ expect(hasValidAriaRole(element)).toBe(true);
124
+ });
125
+ });
126
+
127
+ it('should recognize all document structure roles', () => {
128
+ const structureRoles = [
129
+ 'article', 'cell', 'columnheader', 'definition', 'directory', 'document',
130
+ 'feed', 'figure', 'group', 'heading', 'img', 'list', 'listitem', 'math',
131
+ 'none', 'note', 'presentation', 'row', 'rowgroup', 'rowheader',
132
+ 'separator', 'table', 'term', 'toolbar'
133
+ ];
134
+
135
+ structureRoles.forEach(role => {
136
+ const element = document.createElement('div');
137
+ element.setAttribute('role', role);
138
+ expect(hasValidAriaRole(element)).toBe(true);
139
+ });
140
+ });
141
+
142
+ it('should recognize all landmark roles', () => {
143
+ const landmarkRoles = [
144
+ 'application', 'banner', 'complementary', 'contentinfo', 'form',
145
+ 'search'
146
+ ];
147
+
148
+ landmarkRoles.forEach(role => {
149
+ const element = document.createElement('div');
150
+ element.setAttribute('role', role);
151
+ expect(hasValidAriaRole(element)).toBe(true);
152
+ });
153
+ });
154
+
155
+ it('should recognize timer role', () => {
156
+ const element = document.createElement('div');
157
+ element.setAttribute('role', 'timer');
158
+ expect(hasValidAriaRole(element)).toBe(true);
159
+ });
98
160
  });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { isFocusable } from '../src/isFocusable.js';
3
3
 
4
4
  describe('isFocusable', () => {
@@ -179,4 +179,97 @@ describe('isFocusable', () => {
179
179
  // Act & Assert
180
180
  expect(isFocusable(object)).toBe(true);
181
181
  });
182
+
183
+ it('should return false for disabled object elements', () => {
184
+ // Arrange
185
+ const object = document.createElement('object');
186
+ object.disabled = true;
187
+ document.body.appendChild(object);
188
+
189
+ // Act & Assert
190
+ expect(isFocusable(object)).toBe(false);
191
+ });
192
+
193
+ it('should handle elements with tabindex="0" on form controls', () => {
194
+ // Arrange
195
+ const input = document.createElement('input');
196
+ input.setAttribute('tabindex', '0');
197
+ document.body.appendChild(input);
198
+
199
+ // Act & Assert
200
+ expect(isFocusable(input)).toBe(true);
201
+ });
202
+
203
+ it('should handle elements with positive tabindex on form controls', () => {
204
+ // Arrange
205
+ const button = document.createElement('button');
206
+ button.setAttribute('tabindex', '1');
207
+ document.body.appendChild(button);
208
+
209
+ // Act & Assert
210
+ expect(isFocusable(button)).toBe(true);
211
+ });
212
+
213
+ it('should return false for form controls with negative tabindex', () => {
214
+ // Arrange
215
+ const input = document.createElement('input');
216
+ input.setAttribute('tabindex', '-1');
217
+ document.body.appendChild(input);
218
+
219
+ // Act & Assert
220
+ expect(isFocusable(input)).toBe(false);
221
+ });
222
+
223
+ it('should return false for disabled select elements', () => {
224
+ // Arrange
225
+ const select = document.createElement('select');
226
+ select.disabled = true;
227
+ document.body.appendChild(select);
228
+
229
+ // Act & Assert
230
+ expect(isFocusable(select)).toBe(false);
231
+ });
232
+
233
+ it('should return false for disabled textarea elements', () => {
234
+ // Arrange
235
+ const textarea = document.createElement('textarea');
236
+ textarea.disabled = true;
237
+ document.body.appendChild(textarea);
238
+
239
+ // Act & Assert
240
+ expect(isFocusable(textarea)).toBe(false);
241
+ });
242
+
243
+ it('should handle anchors with tabindex >= 0 and href', () => {
244
+ // Arrange
245
+ const anchor = document.createElement('a');
246
+ anchor.setAttribute('href', 'https://example.com');
247
+ anchor.setAttribute('tabindex', '0');
248
+ document.body.appendChild(anchor);
249
+
250
+ // Act & Assert
251
+ expect(isFocusable(anchor)).toBe(true);
252
+ });
253
+
254
+ it('should return false for anchors with negative tabindex even with href', () => {
255
+ // Arrange
256
+ const anchor = document.createElement('a');
257
+ anchor.setAttribute('href', 'https://example.com');
258
+ anchor.setAttribute('tabindex', '-1');
259
+ document.body.appendChild(anchor);
260
+
261
+ // Act & Assert
262
+ expect(isFocusable(anchor)).toBe(false);
263
+ });
264
+
265
+ it('should return false for area with negative tabindex even with href', () => {
266
+ // Arrange
267
+ const area = document.createElement('area');
268
+ area.setAttribute('href', '#map');
269
+ area.setAttribute('tabindex', '-1');
270
+ document.body.appendChild(area);
271
+
272
+ // Act & Assert
273
+ expect(isFocusable(area)).toBe(false);
274
+ });
182
275
  });
@@ -84,10 +84,10 @@ describe('isVisible', () => {
84
84
  <div id="label" style="display:none">Hidden Label</div>
85
85
  <button aria-labelledby="label">Button</button>
86
86
  `;
87
-
87
+
88
88
  const label = document.getElementById('label');
89
89
  const button = document.querySelector('button');
90
-
90
+
91
91
  window.getComputedStyle.mockImplementation((el) => {
92
92
  if (el === label) {
93
93
  return { display: 'none' };
@@ -97,8 +97,126 @@ describe('isVisible', () => {
97
97
  }
98
98
  return { display: 'block' };
99
99
  });
100
-
100
+
101
101
  // The label is hidden but should be considered visible because it's referenced
102
102
  expect(isVisible(label)).toBe(true);
103
103
  });
104
+
105
+ it('should return false for non-Element objects', () => {
106
+ expect(isVisible({})).toBe(false);
107
+ expect(isVisible('string')).toBe(false);
108
+ expect(isVisible(123)).toBe(false);
109
+ });
110
+
111
+ it('should return false for disconnected elements', () => {
112
+ const div = document.createElement('div');
113
+ // Element is not connected to DOM
114
+ expect(isVisible(div)).toBe(false);
115
+ });
116
+
117
+ it('should return true for visible elements', () => {
118
+ document.body.innerHTML = `<div id="test">Visible</div>`;
119
+ const element = document.getElementById('test');
120
+ expect(isVisible(element)).toBe(true);
121
+ });
122
+
123
+ it('should handle elements referenced by aria-describedby', () => {
124
+ document.body.innerHTML = `
125
+ <div id="desc" style="display:none">Description</div>
126
+ <input aria-describedby="desc" />
127
+ `;
128
+
129
+ const desc = document.getElementById('desc');
130
+ const input = document.querySelector('input');
131
+
132
+ window.getComputedStyle.mockImplementation((el) => {
133
+ if (el === desc) {
134
+ return { display: 'none' };
135
+ }
136
+ if (el === input) {
137
+ return { display: 'block' };
138
+ }
139
+ return { display: 'block' };
140
+ });
141
+
142
+ // Description is hidden but should be considered visible because it's referenced
143
+ expect(isVisible(desc)).toBe(true);
144
+ });
145
+
146
+ it('should handle multiple ancestors with display:none', () => {
147
+ document.body.innerHTML = `
148
+ <div style="display:none">
149
+ <div>
150
+ <div>
151
+ <span id="deeply-hidden">Hidden</span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ `;
156
+ const element = document.getElementById('deeply-hidden');
157
+ expect(isVisible(element)).toBe(false);
158
+ });
159
+
160
+ it('should handle aria-hidden="false"', () => {
161
+ document.body.innerHTML = `<div id="test" aria-hidden="false">Visible</div>`;
162
+ const element = document.getElementById('test');
163
+
164
+ // aria-hidden="false" should not affect visibility in either mode
165
+ expect(isVisible(element)).toBe(true);
166
+ expect(isVisible(element, true)).toBe(true);
167
+ });
168
+
169
+ it('should handle elements with no aria-hidden attribute in strict mode', () => {
170
+ document.body.innerHTML = `<div id="test">No aria-hidden</div>`;
171
+ const element = document.getElementById('test');
172
+
173
+ expect(isVisible(element, true)).toBe(true);
174
+ });
175
+
176
+ it('should handle multiple levels of parent aria-hidden in strict mode', () => {
177
+ document.body.innerHTML = `
178
+ <div aria-hidden="true">
179
+ <div>
180
+ <span id="child">Child</span>
181
+ </div>
182
+ </div>
183
+ `;
184
+ const element = document.getElementById('child');
185
+
186
+ expect(isVisible(element, true)).toBe(false);
187
+ });
188
+
189
+ it('should check specific inherently non-visible elements', () => {
190
+ const testCases = [
191
+ { tag: 'script', html: '<script id="test">alert("test")</script>' },
192
+ { tag: 'style', html: '<style id="test">body {}</style>' },
193
+ { tag: 'input[type="hidden"]', html: '<input type="hidden" id="test" />' }
194
+ ];
195
+
196
+ testCases.forEach(({ tag, html }) => {
197
+ document.body.innerHTML = html;
198
+ const element = document.getElementById('test');
199
+ // These elements return true (visible to AT) according to implementation
200
+ expect(isVisible(element)).toBe(true);
201
+ });
202
+ });
203
+
204
+ it('should handle element with no id referenced by aria-labelledby', () => {
205
+ document.body.innerHTML = `
206
+ <div style="display:none">No ID</div>
207
+ <button aria-labelledby="nonexistent">Button</button>
208
+ `;
209
+
210
+ const div = document.querySelector('div');
211
+ // Element has no id, so won't be found by aria-labelledby query
212
+ expect(isVisible(div)).toBe(false);
213
+ });
214
+
215
+ it('should handle aria-hidden with different values in strict mode', () => {
216
+ document.body.innerHTML = `<div id="test" aria-hidden="invalid">Text</div>`;
217
+ const element = document.getElementById('test');
218
+
219
+ // aria-hidden with value other than "true" should not hide in strict mode
220
+ expect(isVisible(element, true)).toBe(true);
221
+ });
104
222
  });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Standalone Playwright tests for CSS pseudo-element functionality
3
+ * These tests were skipped in JSDOM due to lack of ::before and ::after support
4
+ *
5
+ * Run with: npm run test:playwright:css
6
+ */
7
+
8
+ const { test, expect } = require('@playwright/test');
9
+ const path = require('path');
10
+
11
+ test.describe('CSS Pseudo-element Support', () => {
12
+ test.beforeEach(async ({ page }) => {
13
+ // Load the test fixture HTML
14
+ const fixturePath = path.join(__dirname, 'fixtures', 'css-pseudo-elements.html');
15
+ await page.goto(`file://${fixturePath}`);
16
+
17
+ // Inject the functions into the page
18
+ await page.addScriptTag({
19
+ content: `
20
+ // Fix getComputedStyle usage and make it work in browser
21
+ function getGeneratedContent(el) {
22
+ if (!el) return false;
23
+
24
+ // Get pseudo-element content properly
25
+ const beforeStyle = window.getComputedStyle(el, '::before');
26
+ const afterStyle = window.getComputedStyle(el, '::after');
27
+
28
+ let before = beforeStyle.getPropertyValue('content') || '';
29
+ let after = afterStyle.getPropertyValue('content') || '';
30
+ const inner = el.textContent || '';
31
+
32
+ // Remove quotes from CSS content values
33
+ before = before.replace(/^["']|["']$/g, '');
34
+ after = after.replace(/^["']|["']$/g, '');
35
+
36
+ // Filter out 'none' values
37
+ if (before === 'none') before = '';
38
+ if (after === 'none') after = '';
39
+
40
+ const result = [before, inner, after].filter(Boolean).join(' ').trim();
41
+ return result || false;
42
+ }
43
+
44
+ function hasCSSGeneratedContent(el) {
45
+ if (!el) return false;
46
+ const content = getGeneratedContent(el);
47
+ return content !== false;
48
+ }
49
+
50
+ // Make functions globally available
51
+ window.getGeneratedContent = getGeneratedContent;
52
+ window.hasCSSGeneratedContent = hasCSSGeneratedContent;
53
+ `
54
+ });
55
+ });
56
+
57
+ test.describe('getGeneratedContent', () => {
58
+ test('should return ::before content when present', async ({ page }) => {
59
+ const result = await page.evaluate(() => {
60
+ const element = document.getElementById('with-before');
61
+ return window.getGeneratedContent(element);
62
+ });
63
+
64
+ expect(result).toContain('Before Content');
65
+ });
66
+
67
+ test('should return ::after content when present', async ({ page }) => {
68
+ const result = await page.evaluate(() => {
69
+ const element = document.getElementById('with-after');
70
+ return window.getGeneratedContent(element);
71
+ });
72
+
73
+ expect(result).toContain('After Content');
74
+ });
75
+
76
+ test('should combine ::before, text content, and ::after', async ({ page }) => {
77
+ const result = await page.evaluate(() => {
78
+ const element = document.getElementById('with-both');
79
+ return window.getGeneratedContent(element);
80
+ });
81
+
82
+ expect(result).toContain('Before');
83
+ expect(result).toContain('Text Content');
84
+ expect(result).toContain('After');
85
+ });
86
+
87
+ test('should handle quoted content values in CSS', async ({ page }) => {
88
+ const result = await page.evaluate(() => {
89
+ const element = document.getElementById('with-quotes');
90
+ return window.getGeneratedContent(element);
91
+ });
92
+
93
+ expect(result).toBeTruthy();
94
+ expect(result).toContain('Quoted');
95
+ });
96
+
97
+ test('should handle CSS content with special characters', async ({ page }) => {
98
+ const result = await page.evaluate(() => {
99
+ const element = document.getElementById('with-special-chars');
100
+ return window.getGeneratedContent(element);
101
+ });
102
+
103
+ expect(result).toBeTruthy();
104
+ });
105
+
106
+ test('should trim whitespace from the combined result', async ({ page }) => {
107
+ const result = await page.evaluate(() => {
108
+ const element = document.getElementById('with-whitespace');
109
+ return window.getGeneratedContent(element);
110
+ });
111
+
112
+ expect(result).toBeTruthy();
113
+ expect(result.startsWith(' ')).toBe(false);
114
+ expect(result.endsWith(' ')).toBe(false);
115
+ });
116
+
117
+ test('should handle nested elements with generated content', async ({ page }) => {
118
+ const result = await page.evaluate(() => {
119
+ const element = document.getElementById('nested-content');
120
+ return window.getGeneratedContent(element);
121
+ });
122
+
123
+ expect(result).toBeTruthy();
124
+ });
125
+
126
+ test('should handle content with HTML entities in CSS', async ({ page }) => {
127
+ const result = await page.evaluate(() => {
128
+ const element = document.getElementById('with-entities');
129
+ return window.getGeneratedContent(element);
130
+ });
131
+
132
+ expect(result).toBeTruthy();
133
+ });
134
+ });
135
+
136
+ test.describe('hasCSSGeneratedContent', () => {
137
+ test('should correctly identify elements with ::before content', async ({ page }) => {
138
+ const result = await page.evaluate(() => {
139
+ const element = document.getElementById('with-before');
140
+ return window.hasCSSGeneratedContent(element);
141
+ });
142
+
143
+ expect(result).toBe(true);
144
+ });
145
+
146
+ test('should correctly identify elements with ::after content', async ({ page }) => {
147
+ const result = await page.evaluate(() => {
148
+ const element = document.getElementById('with-after');
149
+ return window.hasCSSGeneratedContent(element);
150
+ });
151
+
152
+ expect(result).toBe(true);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,77 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CSS Pseudo-element Test Fixtures</title>
7
+ <style>
8
+ #with-before::before {
9
+ content: "Before Content";
10
+ }
11
+
12
+ #with-after::after {
13
+ content: "After Content";
14
+ }
15
+
16
+ #with-both::before {
17
+ content: "Before ";
18
+ }
19
+
20
+ #with-both::after {
21
+ content: " After";
22
+ }
23
+
24
+ #with-quotes::before {
25
+ content: "'Quoted Content'";
26
+ }
27
+
28
+ #with-special-chars::before {
29
+ content: "★ Special → Chars ©";
30
+ }
31
+
32
+ #with-whitespace::before {
33
+ content: " Content with spaces ";
34
+ }
35
+
36
+ #nested-content::before {
37
+ content: "Outer Before";
38
+ }
39
+
40
+ #nested-content .inner::before {
41
+ content: "Inner Before";
42
+ }
43
+
44
+ #with-entities::before {
45
+ content: "\00A9 \2022 \2713";
46
+ }
47
+
48
+ #empty-content::before {
49
+ content: "";
50
+ }
51
+
52
+ #no-content::before {
53
+ content: none;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div id="with-before">Element with before content</div>
59
+ <div id="with-after">Element with after content</div>
60
+ <div id="with-both">Text Content</div>
61
+ <div id="with-quotes">Quoted</div>
62
+ <div id="with-special-chars">Special</div>
63
+ <div id="with-whitespace">Whitespace</div>
64
+ <div id="nested-content">
65
+ <span class="inner">Inner text</span>
66
+ </div>
67
+ <div id="with-entities">Entities</div>
68
+ <div id="empty-content">Empty</div>
69
+ <div id="no-content">None</div>
70
+
71
+ <!-- Load the functions we need directly in the page -->
72
+ <script type="module">
73
+ // We'll inject the test functions via Playwright
74
+ window.testReady = true;
75
+ </script>
76
+ </body>
77
+ </html>
package/test/setup.js CHANGED
@@ -1,13 +1,21 @@
1
1
  // Test setup for Vitest
2
2
  import { afterEach, vi } from 'vitest';
3
3
 
4
+ // Mock Tesseract.js to avoid worker initialization errors in test environment
5
+ vi.mock('tesseract.js', () => ({
6
+ default: {
7
+ recognize: vi.fn(() => Promise.reject(new Error('Mocked tesseract error'))),
8
+ },
9
+ recognize: vi.fn(() => Promise.reject(new Error('Mocked tesseract error'))),
10
+ }));
11
+
4
12
  // Cleanup DOM after each test
5
13
  afterEach(() => {
6
14
  // Clean up the DOM
7
15
  if (typeof document !== 'undefined' && document.body) {
8
16
  document.body.innerHTML = '';
9
17
  }
10
-
18
+
11
19
  // Reset any mocked functions
12
20
  vi.restoreAllMocks();
13
21
  });
@@ -330,11 +330,53 @@ describe('stringUtils', () => {
330
330
  const div = document.createElement('div');
331
331
  const span = document.createElement('span');
332
332
  const p = document.createElement('p');
333
-
333
+
334
334
  div.appendChild(span);
335
335
  span.appendChild(p);
336
-
336
+
337
337
  expect(stringUtils.hasText(div)).toBe(false);
338
338
  });
339
339
  });
340
+
341
+ describe('getAllText edge cases', () => {
342
+ it('should handle text nodes with empty nodeValue but non-empty textContent', () => {
343
+ const div = document.createElement('div');
344
+ const textNode = document.createTextNode(' ');
345
+ div.appendChild(textNode);
346
+
347
+ // This tests the else branch where nodeValue.trim() is empty
348
+ const result = stringUtils.getAllText(div);
349
+ expect(typeof result).toBe('string');
350
+ });
351
+
352
+ it('should handle mixed content with whitespace text nodes', () => {
353
+ const div = document.createElement('div');
354
+ div.innerHTML = ' <span>Text</span> ';
355
+
356
+ const result = stringUtils.getAllText(div);
357
+ expect(result).toContain('Text');
358
+ });
359
+
360
+ it('should handle elements with both aria-label and text content', () => {
361
+ const div = document.createElement('div');
362
+ const button = document.createElement('button');
363
+ button.textContent = 'Visual Text';
364
+ button.setAttribute('aria-label', 'Accessible Label');
365
+ div.appendChild(button);
366
+
367
+ const result = stringUtils.getAllText(div);
368
+ expect(result).toContain('Accessible Label');
369
+ expect(result).toContain('Visual Text');
370
+ });
371
+
372
+ it('should handle img without alt attribute', () => {
373
+ const div = document.createElement('div');
374
+ const img = document.createElement('img');
375
+ // No alt attribute
376
+ div.appendChild(img);
377
+
378
+ const result = stringUtils.getAllText(div);
379
+ expect(typeof result).toBe('string');
380
+ });
381
+ });
340
382
  });