pour-engine 1.22.0 → 1.24.0

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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.22.0',
9
+ version: '1.24.0',
10
10
  },
11
11
  };
@@ -143,7 +143,18 @@ function textFromNodes(nodes, includeHidden) {
143
143
  continue;
144
144
  }
145
145
  if (tag === 'img' || tag === 'area') {
146
- text += ` ${node.getAttribute('alt') ?? ''} `;
146
+ // HTML-AAM order for an image's own name: aria-label → alt → title.
147
+ // A PRESENT-but-empty alt marks the image decorative and contributes
148
+ // nothing — title must not resurrect it. Only when alt is ABSENT do
149
+ // the fallbacks apply (a title-only <img> inside a link names the
150
+ // link; Chromium exposes exactly this, measured on a live site where
151
+ // the walk's old alt-or-nothing shortcut called a named link nameless).
152
+ const imgAria = node.getAttribute('aria-label')?.trim();
153
+ if (imgAria) { text += ` ${imgAria} `; continue; }
154
+ const alt = node.getAttribute('alt');
155
+ if (alt !== null) { text += ` ${alt} `; continue; }
156
+ const imgTitle = node.getAttribute('title')?.trim();
157
+ if (imgTitle) text += ` ${imgTitle} `;
147
158
  continue;
148
159
  }
149
160
  const ariaLabel = node.getAttribute('aria-label')?.trim();
@@ -16,6 +16,24 @@ export default {
16
16
  if (element.querySelector('track[kind="captions" i], track[kind="subtitles" i], track:not([kind])')) {
17
17
  return { status: 'pass' };
18
18
  }
19
+ // A video with no media resource at all presents nothing — there is no
20
+ // audio content to caption (lazy players before their source is set).
21
+ if (!element.currentSrc && !element.getAttribute('src') && !element.querySelector('source')) {
22
+ return { status: 'pass' };
23
+ }
24
+ // The decorative no-audio-path signature — the same boundary
25
+ // video-loop-motion draws for 2.2.2: muted, autoplaying, no controls.
26
+ // With no controls there is no user path to unmute, so whatever audio
27
+ // the file may carry is never presented to anyone; audio that cannot be
28
+ // presented is not audio content under 1.2.2, and asking a human to
29
+ // verify captions for it is noise (measured on the benchmark corpus:
30
+ // our own site's aria-hidden hero loops, ffprobe-confirmed to carry no
31
+ // audio stream, plus the same family on four news/university homes).
32
+ // Properties, not attributes: script-muted heroes count too. A muted
33
+ // video WITH controls keeps its review — one click unmutes it.
34
+ if (element.muted && element.autoplay && !element.controls) {
35
+ return { status: 'pass' };
36
+ }
19
37
  // Captions may be burned in or provided by the player — a human must check.
20
38
  return {
21
39
  status: 'incomplete',
@@ -38,7 +38,19 @@ export default {
38
38
  ?? IMPLICIT_CONTAINER[owner.tagName.toLowerCase()]);
39
39
  if (ownerRole && containers.includes(ownerRole)) return { status: 'pass' };
40
40
  }
41
- for (let parent = element.parentElement; parent; parent = parent.parentElement) {
41
+ // FLAT-TREE ancestry, not light-DOM ancestry: assistive technology sees
42
+ // the composed tree, so a slotted role="listitem" whose role="list"
43
+ // container lives inside the host component's shadow root IS inside a
44
+ // list (Adobe Spectrum's sidenav is exactly this — a light-DOM walk was
45
+ // 11 false asserts on one page). An element assigned to a slot continues
46
+ // through the slot's shadow-side ancestors; an element at a shadow root's
47
+ // top continues through the host.
48
+ const flatParent = (node) => node.assignedSlot
49
+ ?? node.parentElement
50
+ ?? (node.getRootNode() instanceof ShadowRoot ? node.getRootNode().host : null);
51
+ for (let parent = flatParent(element); parent; parent = flatParent(parent)) {
52
+ // A <slot> is rendering plumbing with no tree presence of its own.
53
+ if (parent.tagName === 'SLOT' && !parent.hasAttribute('role')) continue;
42
54
  const parentRole =
43
55
  parent.getAttribute('role')?.trim().split(/\s+/)[0]?.toLowerCase() ??
44
56
  IMPLICIT_CONTAINER[parent.tagName.toLowerCase()];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",