@afixt/test-utils 1.1.2 → 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 +6 -2
- package/.github/workflows/test.yml +26 -0
- package/BROWSER_TESTING.md +109 -0
- package/CLAUDE.md +22 -0
- package/package.json +6 -8
- package/playwright.config.js +27 -0
- package/src/domUtils.js +1 -1
- package/src/getAccessibleName.js +8 -4
- package/src/getCSSGeneratedContent.js +9 -5
- package/src/getFocusableElements.js +13 -4
- 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 +241 -0
- package/test/getAccessibleName.test.js +182 -0
- package/test/getAccessibleText.test.js +350 -79
- package/test/getCSSGeneratedContent.test.js +175 -1
- package/test/getFocusableElements.test.js +106 -35
- package/test/getImageText.test.js +95 -12
- package/test/getStyleObject.test.js +19 -1
- package/test/hasCSSGeneratedContent.test.js +7 -2
- package/test/hasParent.test.js +116 -0
- package/test/hasValidAriaRole.test.js +64 -2
- package/test/index.test.js +165 -0
- package/test/interactiveRoles.test.js +60 -0
- package/test/isAriaAttributesValid.test.js +36 -0
- package/test/isDataTable.test.js +492 -0
- package/test/isFocusable.test.js +94 -1
- package/test/isValidUrl.test.js +31 -19
- 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 +277 -1
- package/test/testContrast.test.js +614 -9
- package/test/testLang.test.js +152 -11
- package/test/testOrder.integration.test.js +369 -0
- package/test/testOrder.test.js +756 -21
- package/todo.md +11 -1
- package/vitest.config.js +8 -1
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -51
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -161
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/test-utils/docs/scripts/core.js.html +0 -2263
- package/coverage/test-utils/docs/scripts/core.min.js.html +0 -151
- package/coverage/test-utils/docs/scripts/index.html +0 -176
- package/coverage/test-utils/docs/scripts/resize.js.html +0 -355
- package/coverage/test-utils/docs/scripts/search.js.html +0 -880
- package/coverage/test-utils/docs/scripts/search.min.js.html +0 -100
- package/coverage/test-utils/docs/scripts/third-party/fuse.js.html +0 -109
- package/coverage/test-utils/docs/scripts/third-party/hljs-line-num-original.js.html +0 -1192
- package/coverage/test-utils/docs/scripts/third-party/hljs-line-num.js.html +0 -85
- package/coverage/test-utils/docs/scripts/third-party/hljs-original.js.html +0 -15598
- package/coverage/test-utils/docs/scripts/third-party/hljs.js.html +0 -85
- package/coverage/test-utils/docs/scripts/third-party/index.html +0 -236
- package/coverage/test-utils/docs/scripts/third-party/popper.js.html +0 -100
- package/coverage/test-utils/docs/scripts/third-party/tippy.js.html +0 -88
- package/coverage/test-utils/docs/scripts/third-party/tocbot.js.html +0 -2098
- package/coverage/test-utils/docs/scripts/third-party/tocbot.min.js.html +0 -85
- package/coverage/test-utils/index.html +0 -131
- package/coverage/test-utils/src/arrayUtils.js.html +0 -283
- package/coverage/test-utils/src/domUtils.js.html +0 -622
- package/coverage/test-utils/src/getAccessibleName.js.html +0 -1444
- package/coverage/test-utils/src/getAccessibleText.js.html +0 -271
- package/coverage/test-utils/src/getAriaAttributesByElement.js.html +0 -142
- package/coverage/test-utils/src/getCSSGeneratedContent.js.html +0 -265
- package/coverage/test-utils/src/getComputedRole.js.html +0 -592
- package/coverage/test-utils/src/getFocusableElements.js.html +0 -163
- package/coverage/test-utils/src/getGeneratedContent.js.html +0 -130
- package/coverage/test-utils/src/getImageText.js.html +0 -160
- package/coverage/test-utils/src/getStyleObject.js.html +0 -220
- package/coverage/test-utils/src/hasAccessibleName.js.html +0 -166
- package/coverage/test-utils/src/hasAttribute.js.html +0 -130
- package/coverage/test-utils/src/hasCSSGeneratedContent.js.html +0 -145
- package/coverage/test-utils/src/hasHiddenParent.js.html +0 -172
- package/coverage/test-utils/src/hasParent.js.html +0 -247
- package/coverage/test-utils/src/hasValidAriaAttributes.js.html +0 -175
- package/coverage/test-utils/src/hasValidAriaRole.js.html +0 -172
- package/coverage/test-utils/src/index.html +0 -611
- package/coverage/test-utils/src/index.js.html +0 -274
- package/coverage/test-utils/src/interactiveRoles.js.html +0 -145
- package/coverage/test-utils/src/isAriaAttributesValid.js.html +0 -304
- package/coverage/test-utils/src/isComplexTable.js.html +0 -412
- package/coverage/test-utils/src/isDataTable.js.html +0 -799
- package/coverage/test-utils/src/isFocusable.js.html +0 -187
- package/coverage/test-utils/src/isHidden.js.html +0 -136
- package/coverage/test-utils/src/isOffScreen.js.html +0 -133
- package/coverage/test-utils/src/isValidUrl.js.html +0 -124
- package/coverage/test-utils/src/isVisible.js.html +0 -271
- package/coverage/test-utils/src/listEventListeners.js.html +0 -370
- package/coverage/test-utils/src/queryCache.js.html +0 -1156
- package/coverage/test-utils/src/stringUtils.js.html +0 -535
- package/coverage/test-utils/src/testContrast.js.html +0 -784
- package/coverage/test-utils/src/testLang.js.html +0 -1810
- package/coverage/test-utils/src/testOrder.js.html +0 -355
- package/coverage/test-utils/vitest.config.browser.js.html +0 -133
- package/coverage/test-utils/vitest.config.js.html +0 -157
- package/test/browser-setup.js +0 -68
- package/vitest.config.browser.js +0 -17
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
|
@@ -94,13 +94,289 @@ describe('stringUtils', () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
describe('isAlphaNumeric', () => {
|
|
98
|
+
it('should return true for strings containing only alphanumeric characters', () => {
|
|
99
|
+
expect(stringUtils.isAlphaNumeric('abc123')).toBe(true);
|
|
100
|
+
expect(stringUtils.isAlphaNumeric('ABC')).toBe(true);
|
|
101
|
+
expect(stringUtils.isAlphaNumeric('123')).toBe(true);
|
|
102
|
+
expect(stringUtils.isAlphaNumeric('Test123')).toBe(true);
|
|
103
|
+
expect(stringUtils.isAlphaNumeric('a')).toBe(true);
|
|
104
|
+
expect(stringUtils.isAlphaNumeric('1')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should return false for strings containing non-alphanumeric characters', () => {
|
|
108
|
+
expect(stringUtils.isAlphaNumeric('abc 123')).toBe(false); // space
|
|
109
|
+
expect(stringUtils.isAlphaNumeric('abc-123')).toBe(false); // hyphen
|
|
110
|
+
expect(stringUtils.isAlphaNumeric('abc_123')).toBe(false); // underscore
|
|
111
|
+
expect(stringUtils.isAlphaNumeric('abc@123')).toBe(false); // special character
|
|
112
|
+
expect(stringUtils.isAlphaNumeric('abc.123')).toBe(false); // period
|
|
113
|
+
expect(stringUtils.isAlphaNumeric('abc!123')).toBe(false); // exclamation
|
|
114
|
+
expect(stringUtils.isAlphaNumeric('')).toBe(false); // empty string
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false for special edge cases', () => {
|
|
118
|
+
expect(stringUtils.isAlphaNumeric('\n')).toBe(false); // newline
|
|
119
|
+
expect(stringUtils.isAlphaNumeric('\t')).toBe(false); // tab
|
|
120
|
+
expect(stringUtils.isAlphaNumeric(' ')).toBe(false); // whitespace
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
97
124
|
describe('getPathFromUrl', () => {
|
|
98
125
|
it('should extract the pathname correctly', () => {
|
|
99
126
|
expect(stringUtils.getPathFromUrl('https://example.com/path/to/resource')).toBe('/path/to/resource');
|
|
100
127
|
expect(stringUtils.getPathFromUrl('https://example.com/')).toBe('/');
|
|
101
128
|
expect(stringUtils.getPathFromUrl('https://example.com/path?query=string')).toBe('/path');
|
|
102
129
|
});
|
|
130
|
+
|
|
131
|
+
it('should handle complex URLs', () => {
|
|
132
|
+
expect(stringUtils.getPathFromUrl('https://subdomain.example.com/path/to/resource')).toBe('/path/to/resource');
|
|
133
|
+
expect(stringUtils.getPathFromUrl('http://example.com/path/to/resource#hash')).toBe('/path/to/resource');
|
|
134
|
+
expect(stringUtils.getPathFromUrl('https://example.com:8080/path')).toBe('/path');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle URLs with encoded characters', () => {
|
|
138
|
+
expect(stringUtils.getPathFromUrl('https://example.com/path%20with%20spaces')).toBe('/path%20with%20spaces');
|
|
139
|
+
expect(stringUtils.getPathFromUrl('https://example.com/path/with/special%20chars')).toBe('/path/with/special%20chars');
|
|
140
|
+
});
|
|
103
141
|
});
|
|
104
142
|
|
|
105
|
-
|
|
143
|
+
describe('getAllText', () => {
|
|
144
|
+
it('should extract text from simple text nodes', () => {
|
|
145
|
+
const div = document.createElement('div');
|
|
146
|
+
div.textContent = 'Hello World';
|
|
147
|
+
|
|
148
|
+
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should extract text from nested elements', () => {
|
|
152
|
+
const div = document.createElement('div');
|
|
153
|
+
const span = document.createElement('span');
|
|
154
|
+
span.textContent = 'Hello';
|
|
155
|
+
const text = document.createTextNode(' World');
|
|
156
|
+
|
|
157
|
+
div.appendChild(span);
|
|
158
|
+
div.appendChild(text);
|
|
159
|
+
|
|
160
|
+
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should extract aria-label attributes', () => {
|
|
164
|
+
const div = document.createElement('div');
|
|
165
|
+
const button = document.createElement('button');
|
|
166
|
+
button.setAttribute('aria-label', 'Close dialog');
|
|
167
|
+
button.textContent = 'X';
|
|
168
|
+
|
|
169
|
+
div.appendChild(button);
|
|
170
|
+
|
|
171
|
+
const result = stringUtils.getAllText(div);
|
|
172
|
+
expect(result).toContain('Close dialog');
|
|
173
|
+
expect(result).toContain('X');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should extract alt attributes from images', () => {
|
|
177
|
+
const div = document.createElement('div');
|
|
178
|
+
const img = document.createElement('img');
|
|
179
|
+
img.setAttribute('alt', 'Profile picture');
|
|
180
|
+
img.setAttribute('src', 'profile.jpg');
|
|
181
|
+
|
|
182
|
+
div.appendChild(img);
|
|
183
|
+
|
|
184
|
+
expect(stringUtils.getAllText(div)).toBe('Profile picture');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle mixed content with text, aria-labels, and alt text', () => {
|
|
188
|
+
const div = document.createElement('div');
|
|
189
|
+
|
|
190
|
+
// Add some text
|
|
191
|
+
const textNode = document.createTextNode('Welcome ');
|
|
192
|
+
div.appendChild(textNode);
|
|
193
|
+
|
|
194
|
+
// Add element with aria-label
|
|
195
|
+
const button = document.createElement('button');
|
|
196
|
+
button.setAttribute('aria-label', 'Help');
|
|
197
|
+
button.textContent = '?';
|
|
198
|
+
div.appendChild(button);
|
|
199
|
+
|
|
200
|
+
// Add some more text
|
|
201
|
+
const moreText = document.createTextNode(' to our site ');
|
|
202
|
+
div.appendChild(moreText);
|
|
203
|
+
|
|
204
|
+
// Add image with alt text
|
|
205
|
+
const img = document.createElement('img');
|
|
206
|
+
img.setAttribute('alt', 'Company logo');
|
|
207
|
+
div.appendChild(img);
|
|
208
|
+
|
|
209
|
+
const result = stringUtils.getAllText(div);
|
|
210
|
+
expect(result).toContain('Welcome');
|
|
211
|
+
expect(result).toContain('Help');
|
|
212
|
+
expect(result).toContain('?');
|
|
213
|
+
expect(result).toContain('to our site');
|
|
214
|
+
expect(result).toContain('Company logo');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle empty elements', () => {
|
|
218
|
+
const div = document.createElement('div');
|
|
219
|
+
expect(stringUtils.getAllText(div)).toBe('');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should trim whitespace from text nodes', () => {
|
|
223
|
+
const div = document.createElement('div');
|
|
224
|
+
const textNode = document.createTextNode(' Hello World ');
|
|
225
|
+
div.appendChild(textNode);
|
|
226
|
+
|
|
227
|
+
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle elements with only whitespace', () => {
|
|
231
|
+
const div = document.createElement('div');
|
|
232
|
+
const textNode = document.createTextNode(' ');
|
|
233
|
+
div.appendChild(textNode);
|
|
234
|
+
|
|
235
|
+
// Should use textContent.trim() as fallback for empty text
|
|
236
|
+
expect(stringUtils.getAllText(div)).toBe('');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle deeply nested structures', () => {
|
|
240
|
+
const div = document.createElement('div');
|
|
241
|
+
const section = document.createElement('section');
|
|
242
|
+
const article = document.createElement('article');
|
|
243
|
+
const p = document.createElement('p');
|
|
244
|
+
|
|
245
|
+
p.textContent = 'Deep content';
|
|
246
|
+
article.appendChild(p);
|
|
247
|
+
section.appendChild(article);
|
|
248
|
+
div.appendChild(section);
|
|
249
|
+
|
|
250
|
+
expect(stringUtils.getAllText(div)).toBe('Deep content');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle images without alt attributes', () => {
|
|
254
|
+
const div = document.createElement('div');
|
|
255
|
+
const img = document.createElement('img');
|
|
256
|
+
img.setAttribute('src', 'image.jpg');
|
|
257
|
+
// No alt attribute
|
|
258
|
+
|
|
259
|
+
const text = document.createTextNode('Some text');
|
|
260
|
+
div.appendChild(text);
|
|
261
|
+
div.appendChild(img);
|
|
262
|
+
|
|
263
|
+
expect(stringUtils.getAllText(div)).toBe('Some text');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle elements without aria-label', () => {
|
|
267
|
+
const div = document.createElement('div');
|
|
268
|
+
const button = document.createElement('button');
|
|
269
|
+
button.textContent = 'Click me';
|
|
270
|
+
// No aria-label attribute
|
|
271
|
+
|
|
272
|
+
div.appendChild(button);
|
|
273
|
+
|
|
274
|
+
expect(stringUtils.getAllText(div)).toBe('Click me');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('hasText', () => {
|
|
279
|
+
it('should return true for elements with text content', () => {
|
|
280
|
+
const div = document.createElement('div');
|
|
281
|
+
div.textContent = 'Hello World';
|
|
282
|
+
|
|
283
|
+
expect(stringUtils.hasText(div)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should return false for empty elements', () => {
|
|
287
|
+
const div = document.createElement('div');
|
|
288
|
+
|
|
289
|
+
expect(stringUtils.hasText(div)).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should return false for elements with only whitespace', () => {
|
|
293
|
+
const div = document.createElement('div');
|
|
294
|
+
div.textContent = ' \n\t ';
|
|
295
|
+
|
|
296
|
+
expect(stringUtils.hasText(div)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should return true for elements with aria-label', () => {
|
|
300
|
+
const div = document.createElement('div');
|
|
301
|
+
const button = document.createElement('button');
|
|
302
|
+
button.setAttribute('aria-label', 'Close');
|
|
303
|
+
|
|
304
|
+
div.appendChild(button);
|
|
305
|
+
|
|
306
|
+
expect(stringUtils.hasText(div)).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should return true for elements with image alt text', () => {
|
|
310
|
+
const div = document.createElement('div');
|
|
311
|
+
const img = document.createElement('img');
|
|
312
|
+
img.setAttribute('alt', 'Profile picture');
|
|
313
|
+
|
|
314
|
+
div.appendChild(img);
|
|
315
|
+
|
|
316
|
+
expect(stringUtils.hasText(div)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return true for nested elements with text', () => {
|
|
320
|
+
const div = document.createElement('div');
|
|
321
|
+
const span = document.createElement('span');
|
|
322
|
+
span.textContent = 'Nested text';
|
|
323
|
+
|
|
324
|
+
div.appendChild(span);
|
|
325
|
+
|
|
326
|
+
expect(stringUtils.hasText(div)).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should return false for elements with empty nested structure', () => {
|
|
330
|
+
const div = document.createElement('div');
|
|
331
|
+
const span = document.createElement('span');
|
|
332
|
+
const p = document.createElement('p');
|
|
333
|
+
|
|
334
|
+
div.appendChild(span);
|
|
335
|
+
span.appendChild(p);
|
|
336
|
+
|
|
337
|
+
expect(stringUtils.hasText(div)).toBe(false);
|
|
338
|
+
});
|
|
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
|
+
});
|
|
106
382
|
});
|