@afixt/test-utils 1.1.5 → 1.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -336,6 +336,50 @@ function getAccessibleName(element) {
336
336
  // So we don't return false here - let it fall through
337
337
  }
338
338
 
339
+ // STEP 11-4: meter element (native or role="meter")
340
+ // Per Axe aria-meter-name rule, meter elements need accessible name from
341
+ // aria-labelledby, aria-label (handled above), title, or associated label
342
+ // Text content inside meter is NOT a valid accessible name (it's fallback content)
343
+ // STEP 11-4.1: use title attribute
344
+ // STEP 11-4.2: for native meter, use associated label
345
+ // STEP 11-4.3: return false (don't use text content)
346
+ if (element.tagName.toLowerCase() === "meter" || element.getAttribute("role") === "meter") {
347
+ if (element.hasAttribute("title")) {
348
+ const titleValue = element.getAttribute("title");
349
+ if (strlen(titleValue) > 0) {
350
+ return titleValue;
351
+ }
352
+ }
353
+
354
+ // For native meter elements, check for associated label
355
+ if (element.tagName.toLowerCase() === "meter") {
356
+ // Check for label with for attribute
357
+ if (element.id) {
358
+ const label = document.querySelector('label[for="' + element.id + '"]');
359
+ if (label && strlen(getAccessibleText(label)) > 0) {
360
+ return getAccessibleText(label);
361
+ }
362
+ }
363
+
364
+ // Check for wrapping label
365
+ const parentLabel = element.closest("label");
366
+ if (parentLabel) {
367
+ // Get label text excluding the meter's own content
368
+ const clone = parentLabel.cloneNode(true);
369
+ const meterInClone = clone.querySelector("meter");
370
+ if (meterInClone) {
371
+ meterInClone.remove();
372
+ }
373
+ if (strlen(clone.textContent) > 0) {
374
+ return clone.textContent.trim();
375
+ }
376
+ }
377
+ }
378
+
379
+ // Meter text content is not an accessible name, return false
380
+ return false;
381
+ }
382
+
339
383
  // STEP 12: table element
340
384
  // STEP 12.1: caption element
341
385
  // STEP 12.2: use the title attribute
@@ -392,6 +436,27 @@ function getAccessibleName(element) {
392
436
  }
393
437
  }
394
438
 
439
+ // STEP 15: Landmark elements that require explicit accessible names
440
+ // Navigation landmarks (nav elements and role="navigation") should NOT get
441
+ // their accessible name from text content - they require explicit labeling
442
+ // via aria-labelledby, aria-label, or title attribute.
443
+ // Since steps 1 & 2 already checked aria-labelledby and aria-label,
444
+ // we only need to check title here, then return false.
445
+ const isNavigation = element.tagName.toLowerCase() === "nav" ||
446
+ element.getAttribute("role") === "navigation";
447
+
448
+ if (isNavigation) {
449
+ // Title attribute is valid for navigation landmarks
450
+ if (element.hasAttribute("title")) {
451
+ const titleValue = element.getAttribute("title");
452
+ if (strlen(titleValue) > 0) {
453
+ return titleValue;
454
+ }
455
+ }
456
+ // Navigation landmarks do not get accessible name from text content
457
+ return false;
458
+ }
459
+
395
460
  // Absolute last ditch for the whole plugin:
396
461
  // use the accessible text from the element itself.
397
462
  if (strlen(getAccessibleText(element)) > 0) {
@@ -414,9 +479,11 @@ function isNotVisible(element) {
414
479
  if (!element) return true;
415
480
 
416
481
  // These elements are inherently not visible
482
+ // Note: 'area' is NOT included here because area elements DO have accessible names
483
+ // via the alt attribute and should participate in accessible name calculation
417
484
  const nonVisibleSelectors = [
418
485
  'base', 'head', 'meta', 'title', 'link', 'style', 'script', 'br', 'nobr', 'col', 'embed',
419
- 'input[type="hidden"]', 'keygen', 'source', 'track', 'wbr', 'datalist', 'area', 'param', 'noframes', 'ruby > rp'
486
+ 'input[type="hidden"]', 'keygen', 'source', 'track', 'wbr', 'datalist', 'param', 'noframes', 'ruby > rp'
420
487
  ];
421
488
 
422
489
  if (nonVisibleSelectors.some(selector => matchesSelector(element, selector))) {
@@ -1,67 +1,42 @@
1
1
  /**
2
2
  * Gets the CSS generated content for an element's ::before or ::after pseudo-elements.
3
- * Unlike getGeneratedContent, this function focuses only on CSS-generated content
4
- * and does not include the element's own text content.
5
- *
3
+ * This function only checks for content added via the CSS `content` property,
4
+ * not the element's own text content.
5
+ *
6
6
  * @param {Element} el - The DOM element to check
7
7
  * @param {string} [pseudoElement='both'] - Which pseudo-element to check ('before', 'after', or 'both')
8
8
  * @returns {string|boolean} The generated content as a string or false if none exists
9
9
  */
10
10
  function getCSSGeneratedContent(el, pseudoElement = 'both') {
11
11
  if (!el) return false;
12
-
13
- // jsdom doesn't fully support getComputedStyle for pseudo-elements
14
- // This is test code to make the tests pass in the JSDOM environment
15
- if (typeof window !== 'undefined' && window.document && el.classList) {
16
- if (pseudoElement === 'before' || pseudoElement === 'both') {
17
- if (el.classList.contains('with-before')) return 'Before Content';
18
- if (el.classList.contains('with-both')) return pseudoElement === 'both' ? 'Before Text After Text' : 'Before Text';
19
- if (el.classList.contains('with-quotes')) return 'Quoted Text';
20
- if (el.classList.contains('url-content')) return 'url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")';
21
- }
22
-
23
- if (pseudoElement === 'after' || pseudoElement === 'both') {
24
- if (el.classList.contains('with-after')) return 'After Content';
25
- if (el.classList.contains('with-both') && pseudoElement === 'after') return 'After Text';
26
- }
27
-
28
- // If element has classList but no special test classes, we're in JSDOM without mocked getComputedStyle
29
- // Return false early to avoid JSDOM errors being logged
30
- return false;
31
- }
32
12
 
33
- try {
34
- // This would be the actual implementation for browsers
35
- let content = '';
13
+ let content = '';
36
14
 
37
- if (pseudoElement === 'before' || pseudoElement === 'both') {
38
- const style = window.getComputedStyle(el, '::before');
39
- const before = style.getPropertyValue('content');
40
- if (before && before !== 'none' && before !== 'normal') {
41
- // Remove quotes if present
42
- const cleanBefore = before.replace(/^["'](.*)["']$/, '$1');
43
- if (cleanBefore) {
44
- content += cleanBefore;
45
- }
15
+ if (pseudoElement === 'before' || pseudoElement === 'both') {
16
+ const style = window.getComputedStyle(el, '::before');
17
+ const before = style.getPropertyValue('content');
18
+ if (before && before !== 'none' && before !== 'normal' && before !== '""' && before !== "''") {
19
+ // Remove surrounding quotes if present
20
+ const cleanBefore = before.replace(/^["'](.*)["']$/, '$1');
21
+ if (cleanBefore) {
22
+ content += cleanBefore;
46
23
  }
47
24
  }
25
+ }
48
26
 
49
- if (pseudoElement === 'after' || pseudoElement === 'both') {
50
- const style = window.getComputedStyle(el, '::after');
51
- const after = style.getPropertyValue('content');
52
- if (after && after !== 'none' && after !== 'normal') {
53
- // Remove quotes if present
54
- const cleanAfter = after.replace(/^["'](.*)["']$/, '$1');
55
- if (cleanAfter) {
56
- content += (content ? ' ' : '') + cleanAfter;
57
- }
27
+ if (pseudoElement === 'after' || pseudoElement === 'both') {
28
+ const style = window.getComputedStyle(el, '::after');
29
+ const after = style.getPropertyValue('content');
30
+ if (after && after !== 'none' && after !== 'normal' && after !== '""' && after !== "''") {
31
+ // Remove surrounding quotes if present
32
+ const cleanAfter = after.replace(/^["'](.*)["']$/, '$1');
33
+ if (cleanAfter) {
34
+ content += (content ? ' ' : '') + cleanAfter;
58
35
  }
59
36
  }
60
-
61
- return content ? content.trim() : false;
62
- } catch (error) {
63
- return false;
64
37
  }
38
+
39
+ return content ? content.trim() : false;
65
40
  }
66
41
 
67
42
  module.exports = {
@@ -1,21 +1,21 @@
1
1
 
2
- const { getGeneratedContent } = require('./getGeneratedContent.js');
2
+ const { getCSSGeneratedContent } = require('./getCSSGeneratedContent.js');
3
3
 
4
4
  /**
5
- * Checks if an element has CSS generated content in its ::before or ::after pseudo-elements
6
- * or text content.
5
+ * Checks if an element has CSS generated content in its ::before or ::after pseudo-elements.
6
+ * This only checks for content added via the CSS `content` property, not regular HTML text content.
7
7
  *
8
- * @param {HTMLElement} el - The element to check for generated content.
9
- * @returns {boolean} True if the element has any generated content, otherwise false.
8
+ * @param {HTMLElement} el - The element to check for CSS-generated content.
9
+ * @returns {boolean} True if the element has CSS-generated content, otherwise false.
10
10
  */
11
11
  function hasCSSGeneratedContent(el) {
12
12
  if (!el) return false;
13
-
14
- // Use getGeneratedContent and convert its result to a boolean
15
- const content = getGeneratedContent(el);
16
-
17
- // getGeneratedContent returns either a string or false
18
- // We want to return true if content exists, false otherwise
13
+
14
+ // Use getCSSGeneratedContent which only checks ::before and ::after pseudo-elements
15
+ const content = getCSSGeneratedContent(el);
16
+
17
+ // getCSSGeneratedContent returns either a string or false
18
+ // We want to return true if CSS-generated content exists, false otherwise
19
19
  return content !== false;
20
20
  }
21
21
 
@@ -322,4 +322,46 @@ describe('getAccessibleName', () => {
322
322
  const obj = document.querySelector('object');
323
323
  expect(getAccessibleName(obj)).toBe(false);
324
324
  });
325
+
326
+ it('should handle ARIA meter with aria-label', () => {
327
+ document.body.innerHTML = `<div role="meter" aria-label="Disk usage" aria-valuenow="75"></div>`;
328
+ const meter = document.querySelector('[role="meter"]');
329
+ expect(getAccessibleName(meter)).toBe('Disk usage');
330
+ });
331
+
332
+ it('should handle ARIA meter with title attribute', () => {
333
+ document.body.innerHTML = `<div role="meter" title="Battery level" aria-valuenow="50"></div>`;
334
+ const meter = document.querySelector('[role="meter"]');
335
+ expect(getAccessibleName(meter)).toBe('Battery level');
336
+ });
337
+
338
+ it('should handle native meter with associated label', () => {
339
+ document.body.innerHTML = `<label for="cpu">CPU usage</label><meter id="cpu" value="0.6">60%</meter>`;
340
+ const meter = document.querySelector('meter');
341
+ expect(getAccessibleName(meter)).toBe('CPU usage');
342
+ });
343
+
344
+ it('should handle native meter wrapped in label', () => {
345
+ document.body.innerHTML = `<label>Memory: <meter value="0.8">80%</meter></label>`;
346
+ const meter = document.querySelector('meter');
347
+ expect(getAccessibleName(meter)).toBe('Memory:');
348
+ });
349
+
350
+ it('should return false for meter without accessible name', () => {
351
+ document.body.innerHTML = `<div role="meter" aria-valuenow="50">50%</div>`;
352
+ const meter = document.querySelector('[role="meter"]');
353
+ expect(getAccessibleName(meter)).toBe(false);
354
+ });
355
+
356
+ it('should return false for native meter without label', () => {
357
+ document.body.innerHTML = `<meter value="0.5">50%</meter>`;
358
+ const meter = document.querySelector('meter');
359
+ expect(getAccessibleName(meter)).toBe(false);
360
+ });
361
+
362
+ it('should handle meter with aria-labelledby', () => {
363
+ document.body.innerHTML = `<span id="signal-label">Signal strength</span><div role="meter" aria-labelledby="signal-label" aria-valuenow="4"></div>`;
364
+ const meter = document.querySelector('[role="meter"]');
365
+ expect(getAccessibleName(meter)).toBe('Signal strength');
366
+ });
325
367
  });