@afixt/test-utils 1.1.2 → 1.1.3

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 (86) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/CLAUDE.md +12 -0
  3. package/package.json +1 -1
  4. package/src/domUtils.js +1 -1
  5. package/src/getAccessibleName.js +8 -4
  6. package/src/getFocusableElements.js +13 -4
  7. package/test/domUtils.test.js +117 -0
  8. package/test/getAccessibleName.test.js +182 -0
  9. package/test/getAccessibleText.test.js +350 -79
  10. package/test/getCSSGeneratedContent.test.js +175 -1
  11. package/test/getFocusableElements.test.js +106 -35
  12. package/test/getImageText.test.js +61 -12
  13. package/test/hasParent.test.js +116 -0
  14. package/test/index.test.js +165 -0
  15. package/test/interactiveRoles.test.js +60 -0
  16. package/test/isAriaAttributesValid.test.js +36 -0
  17. package/test/isDataTable.test.js +492 -0
  18. package/test/isValidUrl.test.js +31 -19
  19. package/test/stringUtils.test.js +235 -1
  20. package/test/testContrast.test.js +176 -8
  21. package/test/testOrder.integration.test.js +369 -0
  22. package/test/testOrder.test.js +756 -21
  23. package/todo.md +150 -1
  24. package/coverage/base.css +0 -224
  25. package/coverage/block-navigation.js +0 -87
  26. package/coverage/coverage-final.json +0 -51
  27. package/coverage/favicon.png +0 -0
  28. package/coverage/index.html +0 -161
  29. package/coverage/prettify.css +0 -1
  30. package/coverage/prettify.js +0 -2
  31. package/coverage/sort-arrow-sprite.png +0 -0
  32. package/coverage/sorter.js +0 -196
  33. package/coverage/test-utils/docs/scripts/core.js.html +0 -2263
  34. package/coverage/test-utils/docs/scripts/core.min.js.html +0 -151
  35. package/coverage/test-utils/docs/scripts/index.html +0 -176
  36. package/coverage/test-utils/docs/scripts/resize.js.html +0 -355
  37. package/coverage/test-utils/docs/scripts/search.js.html +0 -880
  38. package/coverage/test-utils/docs/scripts/search.min.js.html +0 -100
  39. package/coverage/test-utils/docs/scripts/third-party/fuse.js.html +0 -109
  40. package/coverage/test-utils/docs/scripts/third-party/hljs-line-num-original.js.html +0 -1192
  41. package/coverage/test-utils/docs/scripts/third-party/hljs-line-num.js.html +0 -85
  42. package/coverage/test-utils/docs/scripts/third-party/hljs-original.js.html +0 -15598
  43. package/coverage/test-utils/docs/scripts/third-party/hljs.js.html +0 -85
  44. package/coverage/test-utils/docs/scripts/third-party/index.html +0 -236
  45. package/coverage/test-utils/docs/scripts/third-party/popper.js.html +0 -100
  46. package/coverage/test-utils/docs/scripts/third-party/tippy.js.html +0 -88
  47. package/coverage/test-utils/docs/scripts/third-party/tocbot.js.html +0 -2098
  48. package/coverage/test-utils/docs/scripts/third-party/tocbot.min.js.html +0 -85
  49. package/coverage/test-utils/index.html +0 -131
  50. package/coverage/test-utils/src/arrayUtils.js.html +0 -283
  51. package/coverage/test-utils/src/domUtils.js.html +0 -622
  52. package/coverage/test-utils/src/getAccessibleName.js.html +0 -1444
  53. package/coverage/test-utils/src/getAccessibleText.js.html +0 -271
  54. package/coverage/test-utils/src/getAriaAttributesByElement.js.html +0 -142
  55. package/coverage/test-utils/src/getCSSGeneratedContent.js.html +0 -265
  56. package/coverage/test-utils/src/getComputedRole.js.html +0 -592
  57. package/coverage/test-utils/src/getFocusableElements.js.html +0 -163
  58. package/coverage/test-utils/src/getGeneratedContent.js.html +0 -130
  59. package/coverage/test-utils/src/getImageText.js.html +0 -160
  60. package/coverage/test-utils/src/getStyleObject.js.html +0 -220
  61. package/coverage/test-utils/src/hasAccessibleName.js.html +0 -166
  62. package/coverage/test-utils/src/hasAttribute.js.html +0 -130
  63. package/coverage/test-utils/src/hasCSSGeneratedContent.js.html +0 -145
  64. package/coverage/test-utils/src/hasHiddenParent.js.html +0 -172
  65. package/coverage/test-utils/src/hasParent.js.html +0 -247
  66. package/coverage/test-utils/src/hasValidAriaAttributes.js.html +0 -175
  67. package/coverage/test-utils/src/hasValidAriaRole.js.html +0 -172
  68. package/coverage/test-utils/src/index.html +0 -611
  69. package/coverage/test-utils/src/index.js.html +0 -274
  70. package/coverage/test-utils/src/interactiveRoles.js.html +0 -145
  71. package/coverage/test-utils/src/isAriaAttributesValid.js.html +0 -304
  72. package/coverage/test-utils/src/isComplexTable.js.html +0 -412
  73. package/coverage/test-utils/src/isDataTable.js.html +0 -799
  74. package/coverage/test-utils/src/isFocusable.js.html +0 -187
  75. package/coverage/test-utils/src/isHidden.js.html +0 -136
  76. package/coverage/test-utils/src/isOffScreen.js.html +0 -133
  77. package/coverage/test-utils/src/isValidUrl.js.html +0 -124
  78. package/coverage/test-utils/src/isVisible.js.html +0 -271
  79. package/coverage/test-utils/src/listEventListeners.js.html +0 -370
  80. package/coverage/test-utils/src/queryCache.js.html +0 -1156
  81. package/coverage/test-utils/src/stringUtils.js.html +0 -535
  82. package/coverage/test-utils/src/testContrast.js.html +0 -784
  83. package/coverage/test-utils/src/testLang.js.html +0 -1810
  84. package/coverage/test-utils/src/testOrder.js.html +0 -355
  85. package/coverage/test-utils/vitest.config.browser.js.html +0 -133
  86. package/coverage/test-utils/vitest.config.js.html +0 -157
@@ -14,7 +14,10 @@
14
14
  "Bash(git stash:*)",
15
15
  "Bash(npm publish:*)",
16
16
  "Bash(git commit:*)",
17
- "Bash(git push:*)"
17
+ "Bash(git push:*)",
18
+ "Bash(node:*)",
19
+ "Bash(npx vitest run:*)",
20
+ "Bash(1)"
18
21
  ]
19
22
  },
20
23
  "enableAllProjectMcpServers": false
package/CLAUDE.md CHANGED
@@ -22,6 +22,18 @@
22
22
  - **No jQuery**: All code should be vanilla javascript. No jQuery should be used
23
23
  - **No duplication**: In any case where a function already exists elsewhere, that function should be imported and used, rather than being duplicated
24
24
 
25
+ ## Git Workflow
26
+
27
+ - **Branching Strategy**: Git Flow must be used for all development
28
+ - **main/master**: Production-ready code only
29
+ - **develop**: Integration branch for features
30
+ - **feature/***: New features (branch from develop, merge back to develop)
31
+ - **release/***: Release preparation (branch from develop, merge to main and develop)
32
+ - **hotfix/***: Emergency fixes (branch from main, merge to main and develop)
33
+ - **Branch Naming**: Use descriptive names (e.g., `feature/add-aria-validation`, `hotfix/fix-focus-trap`)
34
+ - **Commits**: Write clear, concise commit messages following conventional commits when possible
35
+ - **Pull Requests**: All features must go through PR review before merging to develop
36
+
25
37
  ## Testing
26
38
 
27
39
  - Test files should be placed in the `/test` directory with a `.test.js` extension
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/domUtils.js CHANGED
@@ -58,7 +58,7 @@ const domUtils = {
58
58
  */
59
59
  getAttributesAsString(element) {
60
60
  const attrs = domUtils.getAttributes(element);
61
- return attrs ? JSON.stringify(attrs) : false;
61
+ return Object.keys(attrs).length > 0 ? JSON.stringify(attrs) : false;
62
62
  },
63
63
 
64
64
  /**
@@ -78,7 +78,9 @@ function getAccessibleName(element) {
78
78
  }
79
79
 
80
80
  // STEP 4: is the tag one of:
81
- // input without any type, input type="text", input type="email", input type="password", input type="search", input type="tel", input type="url" and textarea element
81
+ // input without any type, input type="text", input type="email",
82
+ // input type="password", input type="search", input type="tel",
83
+ // input type="url" and textarea element
82
84
  // STEP 4.1 use the associated label
83
85
  // STEP 4.3 Otherwise use the title attribute
84
86
  // STEP 4.4 - return false. If none of the above yield a usable text string there is no accessible name
@@ -91,7 +93,8 @@ function getAccessibleName(element) {
91
93
  if (element.id && document.querySelector('label[for="' + element.id + '"]')) {
92
94
  id = element.id;
93
95
  // Use only the *first* label that matches this ID.
94
- // Sometimes JS libraries screw this up by hiding one of the labels or misnaming one
96
+ // Sometimes JS libraries screw this up by hiding one of the
97
+ // labels or misnaming one
95
98
  label = document.querySelector('label[for="' + id + '"]');
96
99
  if (label) {
97
100
  return getAccessibleText(label);
@@ -195,7 +198,8 @@ function getAccessibleName(element) {
195
198
  // STEP 8: Other form elements
196
199
  // STEP 8.1: use label element
197
200
  // STEP 8.2: use title attribute
198
- // STEP 8.3: return false. If none of the above yield a usable text string there is no accessible name
201
+ // STEP 8.3: return false. If none of the above yield a usable text
202
+ // string there is no accessible name
199
203
  if (
200
204
  matchesSelector(element,
201
205
  'select, input[type="checkbox"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="email"], input[type="file"], input[type="month"], input[type="number"], input[type="radio"], input[type="range"], input[type="time"], input[type="week"]'
@@ -449,4 +453,4 @@ function strlen(str) {
449
453
  }
450
454
 
451
455
  // Export the function for CommonJS module usage
452
- module.exports = getAccessibleName;
456
+ module.exports = getAccessibleName;
@@ -4,6 +4,10 @@
4
4
  * @returns {Array} - Array of focusable elements
5
5
  */
6
6
  function getFocusableElements(el) {
7
+ if (!el) {
8
+ throw new Error('Container element is required');
9
+ }
10
+
7
11
  const focusableSelectors = [
8
12
  "a[href]",
9
13
  "area",
@@ -18,10 +22,15 @@ function getFocusableElements(el) {
18
22
  el.querySelectorAll(focusableSelectors.join(", "))
19
23
  ).filter((element) => {
20
24
  const tabindex = element.getAttribute("tabindex");
21
- return (
22
- (tabindex === null || parseInt(tabindex, 10) >= 0) &&
23
- element.offsetParent !== null // Checks visibility
24
- );
25
+ const hasValidTabindex = tabindex === null || parseInt(tabindex, 10) >= 0;
26
+
27
+ // Check visibility - handle JSDOM environment where offsetParent might not work correctly
28
+ const isVisible = element.offsetParent !== null ||
29
+ (typeof window !== 'undefined' &&
30
+ window.navigator &&
31
+ window.navigator.userAgent.includes('jsdom'));
32
+
33
+ return hasValidTabindex && isVisible;
25
34
  });
26
35
  }
27
36
 
@@ -144,4 +144,121 @@ describe('domUtils', () => {
144
144
  expect(domUtils.hasFocus(element2)).toBe(false);
145
145
  });
146
146
  });
147
+
148
+ describe('getAttributesAsString', () => {
149
+ it('should return attributes as JSON string when element has attributes', () => {
150
+ document.body.innerHTML = `<div id="test" class="container" data-value="123"></div>`;
151
+ const element = document.getElementById('test');
152
+
153
+ const attrsString = domUtils.getAttributesAsString(element);
154
+ const parsed = JSON.parse(attrsString);
155
+
156
+ expect(parsed.id).toBe('test');
157
+ expect(parsed.class).toBe('container');
158
+ expect(parsed['data-value']).toBe('123');
159
+ });
160
+
161
+ it('should return false for elements with no attributes', () => {
162
+ const element = document.createElement('div');
163
+ expect(domUtils.getAttributesAsString(element)).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('getDocumentSize', () => {
168
+ it('should return the size of the document HTML', () => {
169
+ const size = domUtils.getDocumentSize();
170
+ expect(typeof size).toBe('number');
171
+ expect(size).toBeGreaterThan(0);
172
+ });
173
+ });
174
+
175
+ describe('getElementsWithDuplicateIds', () => {
176
+ it('should find elements with duplicate IDs', () => {
177
+ document.body.innerHTML = `
178
+ <div id="duplicate">First</div>
179
+ <span id="duplicate">Second</span>
180
+ <p id="unique">Unique</p>
181
+ `;
182
+
183
+ const duplicates = domUtils.getElementsWithDuplicateIds();
184
+ expect(duplicates.length).toBeGreaterThan(0);
185
+ expect(duplicates[0].id).toBe('duplicate');
186
+ });
187
+
188
+ it('should return empty array when no duplicate IDs exist', () => {
189
+ document.body.innerHTML = `
190
+ <div id="first">First</div>
191
+ <span id="second">Second</span>
192
+ <p id="third">Third</p>
193
+ `;
194
+
195
+ const duplicates = domUtils.getElementsWithDuplicateIds();
196
+ expect(duplicates.length).toBe(0);
197
+ });
198
+ });
199
+
200
+ describe('getOuterHTML', () => {
201
+ it('should return the outer HTML of an element', () => {
202
+ document.body.innerHTML = `<div id="test" class="container">Content</div>`;
203
+ const element = document.getElementById('test');
204
+
205
+ const outerHTML = domUtils.getOuterHTML(element);
206
+ expect(outerHTML).toBe('<div id="test" class="container">Content</div>');
207
+ });
208
+ });
209
+
210
+ describe('isFullyVisible', () => {
211
+ it('should check if element is fully visible in viewport', () => {
212
+ document.body.innerHTML = `<div id="test" style="width: 100px; height: 100px;">Test</div>`;
213
+ const element = document.getElementById('test');
214
+
215
+ // Mock getBoundingClientRect to simulate visible element
216
+ element.getBoundingClientRect = () => ({
217
+ top: 10,
218
+ left: 10,
219
+ bottom: 110,
220
+ right: 110
221
+ });
222
+
223
+ // Mock window dimensions
224
+ Object.defineProperty(window, 'innerHeight', {
225
+ writable: true,
226
+ configurable: true,
227
+ value: 800
228
+ });
229
+ Object.defineProperty(window, 'innerWidth', {
230
+ writable: true,
231
+ configurable: true,
232
+ value: 1200
233
+ });
234
+
235
+ expect(domUtils.isFullyVisible(element)).toBe(true);
236
+ });
237
+
238
+ it('should return false if element is not fully visible', () => {
239
+ document.body.innerHTML = `<div id="test" style="width: 100px; height: 100px;">Test</div>`;
240
+ const element = document.getElementById('test');
241
+
242
+ // Mock getBoundingClientRect to simulate partially visible element
243
+ element.getBoundingClientRect = () => ({
244
+ top: -50,
245
+ left: 10,
246
+ bottom: 50,
247
+ right: 110
248
+ });
249
+
250
+ Object.defineProperty(window, 'innerHeight', {
251
+ writable: true,
252
+ configurable: true,
253
+ value: 800
254
+ });
255
+ Object.defineProperty(window, 'innerWidth', {
256
+ writable: true,
257
+ configurable: true,
258
+ value: 1200
259
+ });
260
+
261
+ expect(domUtils.isFullyVisible(element)).toBe(false);
262
+ });
263
+ });
147
264
  });
@@ -110,4 +110,186 @@ describe('getAccessibleName', () => {
110
110
  const dialog = document.querySelector('[role="dialog"]');
111
111
  expect(getAccessibleName(dialog)).toBe('Confirmation Are you sure you want to continue?');
112
112
  });
113
+
114
+ it('should handle text-level elements with content', () => {
115
+ // Test various text-level elements
116
+ const textLevelElements = ['em', 'strong', 'small', 'cite', 'code', 'var', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark'];
117
+
118
+ textLevelElements.forEach(tagName => {
119
+ document.body.innerHTML = `<${tagName}>Important text</${tagName}>`;
120
+ const element = document.querySelector(tagName);
121
+ expect(getAccessibleName(element)).toBe('Important text');
122
+ });
123
+ });
124
+
125
+ it('should handle text-level elements with title fallback', () => {
126
+ document.body.innerHTML = `<em title="Emphasis title"></em>`;
127
+ const em = document.querySelector('em');
128
+ expect(getAccessibleName(em)).toBe('Emphasis title');
129
+ });
130
+
131
+ it('should return false for text-level elements without content or title', () => {
132
+ document.body.innerHTML = `<strong></strong>`;
133
+ const strong = document.querySelector('strong');
134
+ expect(getAccessibleName(strong)).toBe(false);
135
+ });
136
+
137
+ it('should handle elements with role-based text content', () => {
138
+ const textRoles = ['button', 'checkbox', 'link', 'menuitem', 'option', 'tab'];
139
+
140
+ textRoles.forEach(role => {
141
+ document.body.innerHTML = `<div role="${role}">Role content</div>`;
142
+ const element = document.querySelector('div');
143
+ expect(getAccessibleName(element)).toBe('Role content');
144
+ });
145
+ });
146
+
147
+ it('should return false for elements with roles but no text content', () => {
148
+ document.body.innerHTML = `<div role="button"></div>`;
149
+ const element = document.querySelector('div');
150
+ expect(getAccessibleName(element)).toBe(false);
151
+ });
152
+
153
+ it('should handle input type="button" with value', () => {
154
+ document.body.innerHTML = `<input type="button" value="Cancel">`;
155
+ const button = document.querySelector('input');
156
+ expect(getAccessibleName(button)).toBe('Cancel');
157
+ });
158
+
159
+ it('should handle input type="button" with title fallback', () => {
160
+ document.body.innerHTML = `<input type="button" title="Close Dialog">`;
161
+ const button = document.querySelector('input');
162
+ expect(getAccessibleName(button)).toBe('Close Dialog');
163
+ });
164
+
165
+ it('should return false for input type="button" without value or title', () => {
166
+ document.body.innerHTML = `<input type="button">`;
167
+ const button = document.querySelector('input');
168
+ expect(getAccessibleName(button)).toBe(false);
169
+ });
170
+
171
+ it('should handle input type="reset" with title', () => {
172
+ document.body.innerHTML = `<input type="reset" title="Clear Form">`;
173
+ const reset = document.querySelector('input');
174
+ expect(getAccessibleName(reset)).toBe('Clear Form');
175
+ });
176
+
177
+ it('should return default "Reset" for input type="reset" without title', () => {
178
+ document.body.innerHTML = `<input type="reset">`;
179
+ const reset = document.querySelector('input');
180
+ expect(getAccessibleName(reset)).toBe('Reset');
181
+ });
182
+
183
+ it('should handle input type="image" with alt attribute', () => {
184
+ document.body.innerHTML = `<input type="image" alt="Submit Button">`;
185
+ const image = document.querySelector('input');
186
+ expect(getAccessibleName(image)).toBe('Submit Button');
187
+ });
188
+
189
+ it('should handle input type="image" with value fallback', () => {
190
+ document.body.innerHTML = `<input type="image" value="Send Message">`;
191
+ const image = document.querySelector('input');
192
+ expect(getAccessibleName(image)).toBe('Send Message');
193
+ });
194
+
195
+ it('should handle input type="image" with title fallback', () => {
196
+ document.body.innerHTML = `<input type="image" title="Upload File">`;
197
+ const image = document.querySelector('input');
198
+ expect(getAccessibleName(image)).toBe('Upload File');
199
+ });
200
+
201
+ it('should return false for input type="image" without alt, value, or title', () => {
202
+ document.body.innerHTML = `<input type="image">`;
203
+ const image = document.querySelector('input');
204
+ expect(getAccessibleName(image)).toBe(false);
205
+ });
206
+
207
+ it('should handle button element with title fallback', () => {
208
+ document.body.innerHTML = `<button title="Submit Form"></button>`;
209
+ const button = document.querySelector('button');
210
+ expect(getAccessibleName(button)).toBe('Submit Form');
211
+ });
212
+
213
+ it('should return false for button element without content or title', () => {
214
+ document.body.innerHTML = `<button></button>`;
215
+ const button = document.querySelector('button');
216
+ expect(getAccessibleName(button)).toBe(false);
217
+ });
218
+
219
+ it('should handle empty aria-label gracefully', () => {
220
+ document.body.innerHTML = `<button aria-label="">Click Me</button>`;
221
+ const button = document.querySelector('button');
222
+ expect(getAccessibleName(button)).toBe('Click Me');
223
+ });
224
+
225
+ it('should return false for aria-labelledby with non-existent reference', () => {
226
+ document.body.innerHTML = `<button aria-labelledby="non-existent">Click Me</button>`;
227
+ const button = document.querySelector('button');
228
+ expect(getAccessibleName(button)).toBe(false);
229
+ });
230
+
231
+ it('should handle disconnected elements', () => {
232
+ const div = document.createElement('div');
233
+ div.textContent = 'Not connected';
234
+ expect(getAccessibleName(div)).toBe(false);
235
+ });
236
+
237
+ it('should handle invisible elements', () => {
238
+ document.body.innerHTML = `<div style="display: none;">Hidden content</div>`;
239
+ const div = document.querySelector('div');
240
+ expect(getAccessibleName(div)).toBe(false);
241
+ });
242
+
243
+ it('should handle elements with aria-hidden="true"', () => {
244
+ document.body.innerHTML = `<div aria-hidden="true">Hidden from AT</div>`;
245
+ const div = document.querySelector('div');
246
+ expect(getAccessibleName(div)).toBe(false);
247
+ });
248
+
249
+ it('should handle unlabellable elements', () => {
250
+ document.body.innerHTML = `<hr>`;
251
+ const hr = document.querySelector('hr');
252
+ expect(getAccessibleName(hr)).toBe(false);
253
+ });
254
+
255
+ it('should use fallback accessible text for general elements', () => {
256
+ document.body.innerHTML = `<span>Fallback text content</span>`;
257
+ const span = document.querySelector('span');
258
+ expect(getAccessibleName(span)).toBe('Fallback text content');
259
+ });
260
+
261
+ it('should return false when no accessible name can be determined', () => {
262
+ document.body.innerHTML = `<div></div>`;
263
+ const div = document.querySelector('div');
264
+ expect(getAccessibleName(div)).toBe(false);
265
+ });
266
+
267
+ it('should handle input element with empty title', () => {
268
+ document.body.innerHTML = `<input type="text" title="">`;
269
+ const input = document.querySelector('input');
270
+ expect(getAccessibleName(input)).toBe(false);
271
+ });
272
+
273
+ it('should handle form elements with label', () => {
274
+ document.body.innerHTML = `
275
+ <label for="select-id">Choose option</label>
276
+ <select id="select-id">
277
+ <option>Option 1</option>
278
+ </select>
279
+ `;
280
+ const select = document.querySelector('select');
281
+ expect(getAccessibleName(select)).toBe('Choose option');
282
+ });
283
+
284
+ it('should handle form elements without label but with title', () => {
285
+ document.body.innerHTML = `<select title="Dropdown menu"><option>Option 1</option></select>`;
286
+ const select = document.querySelector('select');
287
+ expect(getAccessibleName(select)).toBe('Dropdown menu');
288
+ });
289
+
290
+ it('should return false for form elements without label or title', () => {
291
+ document.body.innerHTML = `<select><option>Option 1</option></select>`;
292
+ const select = document.querySelector('select');
293
+ expect(getAccessibleName(select)).toBe(false);
294
+ });
113
295
  });