@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.
- package/.claude/settings.local.json +3 -2
- package/.github/workflows/test.yml +26 -0
- package/BROWSER_TESTING.md +109 -0
- package/CLAUDE.md +10 -0
- package/package.json +6 -8
- package/playwright.config.js +27 -0
- package/src/getCSSGeneratedContent.js +9 -5
- package/src/getImageText.js +4 -1
- package/src/testContrast.js +5 -1
- package/test/__screenshots__/getImageText.test.js/getImageText-should-be-an-async-function-1.png +0 -0
- package/test/__screenshots__/getImageText.test.js/getImageText-should-be-defined-and-exported-from-the-module-1.png +0 -0
- package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-empty-string-input-gracefully-1.png +0 -0
- package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-invalid-image-paths-gracefully-1.png +0 -0
- package/test/__screenshots__/getImageText.test.js/getImageText-should-handle-null-or-undefined-input-gracefully-1.png +0 -0
- package/test/__screenshots__/getImageText.test.js/getImageText-should-log-errors-in-non-test-environments-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-call-original-addEventListener-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-added-event-listeners-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-listeners-for-different-event-types-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-multiple-listeners-for-the-same-event-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-addEventListener-override-should-track-options-parameter-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getEventListeners-should-return-all-event-listeners-for-an-element-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getEventListeners-should-return-empty-object-for-elements-without-listeners-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-generate-XPath-for-elements-without-id-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-handle-multiple-siblings-correctly-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-getXPath-should-return-XPath-for-element-with-id-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-event-listeners-on-child-elements-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-event-listeners-on-root-element-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-listeners-from-multiple-elements-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-list-multiple-event-types-on-same-element-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-return-empty-array-when-no-event-listeners-exist-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-use-document-as-default-root-element-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-listEventListeners-should-work-with-custom-root-element-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-call-original-removeEventListener-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-handle-removing-non-existent-listeners-gracefully-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-only-remove-the-specified-listener-1.png +0 -0
- package/test/__screenshots__/listEventListeners.test.js/listEventListeners-removeEventListener-override-should-remove-tracked-event-listeners-1.png +0 -0
- package/test/arrayUtils.test.js +22 -0
- package/test/domUtils.test.js +124 -0
- package/test/getImageText.test.js +37 -3
- package/test/getStyleObject.test.js +19 -1
- package/test/hasCSSGeneratedContent.test.js +7 -2
- package/test/hasValidAriaRole.test.js +64 -2
- package/test/isFocusable.test.js +94 -1
- package/test/isVisible.test.js +121 -3
- package/test/playwright/css-pseudo-elements.spec.js +155 -0
- package/test/playwright/fixtures/css-pseudo-elements.html +77 -0
- package/test/setup.js +9 -1
- package/test/stringUtils.test.js +44 -2
- package/test/testContrast.test.js +439 -2
- package/test/testLang.test.js +152 -11
- package/todo.md +7 -146
- package/vitest.config.js +8 -1
- package/test/browser-setup.js +0 -68
- 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
|
});
|
package/test/isFocusable.test.js
CHANGED
|
@@ -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
|
});
|
package/test/isVisible.test.js
CHANGED
|
@@ -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
|
});
|
package/test/stringUtils.test.js
CHANGED
|
@@ -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
|
});
|