accented 0.0.1-dev.4 → 0.0.2

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 (142) hide show
  1. package/README.md +214 -0
  2. package/dist/accented.d.ts +28 -7
  3. package/dist/accented.d.ts.map +1 -1
  4. package/dist/accented.js +100 -42
  5. package/dist/accented.js.map +1 -1
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/constants.js +3 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/dom-updater.d.ts +1 -6
  11. package/dist/dom-updater.d.ts.map +1 -1
  12. package/dist/dom-updater.js +94 -20
  13. package/dist/dom-updater.js.map +1 -1
  14. package/dist/elements/accented-dialog.d.ts +356 -0
  15. package/dist/elements/accented-dialog.d.ts.map +1 -0
  16. package/dist/elements/accented-dialog.js +361 -0
  17. package/dist/elements/accented-dialog.js.map +1 -0
  18. package/dist/elements/accented-trigger.d.ts +359 -0
  19. package/dist/elements/accented-trigger.d.ts.map +1 -0
  20. package/dist/elements/accented-trigger.js +159 -0
  21. package/dist/elements/accented-trigger.js.map +1 -0
  22. package/dist/intersection-observer.d.ts +5 -0
  23. package/dist/intersection-observer.d.ts.map +1 -0
  24. package/dist/intersection-observer.js +28 -0
  25. package/dist/intersection-observer.js.map +1 -0
  26. package/dist/log-and-rethrow.d.ts +2 -0
  27. package/dist/log-and-rethrow.d.ts.map +1 -0
  28. package/dist/log-and-rethrow.js +7 -0
  29. package/dist/log-and-rethrow.js.map +1 -0
  30. package/dist/logger.d.ts +2 -0
  31. package/dist/logger.d.ts.map +1 -0
  32. package/dist/logger.js +25 -0
  33. package/dist/logger.js.map +1 -0
  34. package/dist/register-elements.d.ts +2 -0
  35. package/dist/register-elements.d.ts.map +1 -0
  36. package/dist/register-elements.js +21 -0
  37. package/dist/register-elements.js.map +1 -0
  38. package/dist/resize-listener.d.ts +2 -0
  39. package/dist/resize-listener.d.ts.map +1 -0
  40. package/dist/resize-listener.js +18 -0
  41. package/dist/resize-listener.js.map +1 -0
  42. package/dist/scanner.d.ts +3 -0
  43. package/dist/scanner.d.ts.map +1 -0
  44. package/dist/scanner.js +120 -0
  45. package/dist/scanner.js.map +1 -0
  46. package/dist/scroll-listeners.d.ts +2 -0
  47. package/dist/scroll-listeners.d.ts.map +1 -0
  48. package/dist/scroll-listeners.js +38 -0
  49. package/dist/scroll-listeners.js.map +1 -0
  50. package/dist/state.d.ts +6 -0
  51. package/dist/state.d.ts.map +1 -0
  52. package/dist/state.js +14 -0
  53. package/dist/state.js.map +1 -0
  54. package/dist/task-queue.d.ts +3 -4
  55. package/dist/task-queue.d.ts.map +1 -1
  56. package/dist/task-queue.js +27 -23
  57. package/dist/task-queue.js.map +1 -1
  58. package/dist/types.d.ts +136 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/utils/are-issue-sets-equal.d.ts +3 -0
  63. package/dist/utils/are-issue-sets-equal.d.ts.map +1 -0
  64. package/dist/utils/are-issue-sets-equal.js +6 -0
  65. package/dist/utils/are-issue-sets-equal.js.map +1 -0
  66. package/dist/utils/deep-merge.d.ts +4 -0
  67. package/dist/utils/deep-merge.d.ts.map +1 -0
  68. package/dist/utils/deep-merge.js +18 -0
  69. package/dist/utils/deep-merge.js.map +1 -0
  70. package/dist/utils/get-element-html.d.ts +2 -0
  71. package/dist/utils/get-element-html.d.ts.map +1 -0
  72. package/dist/utils/get-element-html.js +14 -0
  73. package/dist/utils/get-element-html.js.map +1 -0
  74. package/dist/utils/get-element-position.d.ts +3 -0
  75. package/dist/utils/get-element-position.d.ts.map +1 -0
  76. package/dist/utils/get-element-position.js +22 -0
  77. package/dist/utils/get-element-position.js.map +1 -0
  78. package/dist/utils/get-scrollable-ancestors.d.ts +2 -0
  79. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -0
  80. package/dist/utils/get-scrollable-ancestors.js +15 -0
  81. package/dist/utils/get-scrollable-ancestors.js.map +1 -0
  82. package/dist/utils/recalculate-positions.d.ts +2 -0
  83. package/dist/utils/recalculate-positions.d.ts.map +1 -0
  84. package/dist/utils/recalculate-positions.js +27 -0
  85. package/dist/utils/recalculate-positions.js.map +1 -0
  86. package/dist/utils/recalculate-scrollable-ancestors.d.ts +2 -0
  87. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -0
  88. package/dist/utils/recalculate-scrollable-ancestors.js +13 -0
  89. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -0
  90. package/dist/utils/supports-anchor-positioning.d.ts +6 -0
  91. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -0
  92. package/dist/utils/supports-anchor-positioning.js +4 -0
  93. package/dist/utils/supports-anchor-positioning.js.map +1 -0
  94. package/dist/utils/transform-violations.d.ts +4 -0
  95. package/dist/utils/transform-violations.d.ts.map +1 -0
  96. package/dist/utils/transform-violations.js +48 -0
  97. package/dist/utils/transform-violations.js.map +1 -0
  98. package/dist/utils/update-elements-with-issues.d.ts +7 -0
  99. package/dist/utils/update-elements-with-issues.d.ts.map +1 -0
  100. package/dist/utils/update-elements-with-issues.js +64 -0
  101. package/dist/utils/update-elements-with-issues.js.map +1 -0
  102. package/dist/validate-options.d.ts +3 -0
  103. package/dist/validate-options.d.ts.map +1 -0
  104. package/dist/validate-options.js +42 -0
  105. package/dist/validate-options.js.map +1 -0
  106. package/package.json +9 -4
  107. package/src/accented.test.ts +24 -0
  108. package/src/accented.ts +119 -0
  109. package/src/constants.ts +2 -0
  110. package/src/dom-updater.ts +112 -0
  111. package/src/elements/accented-dialog.ts +384 -0
  112. package/src/elements/accented-trigger.ts +179 -0
  113. package/src/intersection-observer.ts +28 -0
  114. package/src/log-and-rethrow.ts +9 -0
  115. package/src/logger.ts +26 -0
  116. package/src/register-elements.ts +21 -0
  117. package/src/resize-listener.ts +17 -0
  118. package/src/scanner.ts +139 -0
  119. package/src/scroll-listeners.ts +37 -0
  120. package/src/state.ts +24 -0
  121. package/src/task-queue.test.ts +135 -0
  122. package/src/task-queue.ts +59 -0
  123. package/src/types.ts +155 -0
  124. package/src/utils/are-issue-sets-equal.test.ts +49 -0
  125. package/src/utils/are-issue-sets-equal.ts +10 -0
  126. package/src/utils/deep-merge.test.ts +34 -0
  127. package/src/utils/deep-merge.ts +18 -0
  128. package/src/utils/get-element-html.ts +13 -0
  129. package/src/utils/get-element-position.ts +21 -0
  130. package/src/utils/get-scrollable-ancestors.ts +14 -0
  131. package/src/utils/recalculate-positions.ts +27 -0
  132. package/src/utils/recalculate-scrollable-ancestors.ts +13 -0
  133. package/src/utils/supports-anchor-positioning.ts +7 -0
  134. package/src/utils/transform-violations.test.ts +124 -0
  135. package/src/utils/transform-violations.ts +56 -0
  136. package/src/utils/update-elements-with-issues.test.ts +283 -0
  137. package/src/utils/update-elements-with-issues.ts +75 -0
  138. package/src/validate-options.ts +44 -0
  139. package/dist/utils/issuesToElements.d.ts +0 -3
  140. package/dist/utils/issuesToElements.d.ts.map +0 -1
  141. package/dist/utils/issuesToElements.js +0 -16
  142. package/dist/utils/issuesToElements.js.map +0 -1
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import { suite, test } from 'node:test';
3
+
4
+ import deepMerge from './deep-merge';
5
+
6
+ suite('deepMerge', () => {
7
+ test('merges two objects with overlapping keys', () => {
8
+ const target = { a: 1, b: 2 };
9
+ const source = { b: 3, c: 4 };
10
+ const result = deepMerge(target, source);
11
+ assert.deepEqual(result, { a: 1, b: 3, c: 4 });
12
+ });
13
+
14
+ test('deeply merges nested objects', () => {
15
+ const target = { a: 1, b: { x: 1, y: 2 } };
16
+ const source = { b: { y: 3, z: 4 }, c: 5 };
17
+ const result = deepMerge(target, source);
18
+ assert.deepEqual(result, { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 });
19
+ });
20
+
21
+ test('handles null values in a logical way', () => {
22
+ const target = { a: 1 };
23
+ const source = { a: null };
24
+ const result = deepMerge(target, source);
25
+ assert.deepEqual(result, { a: null });
26
+ });
27
+
28
+ test('doesn’t turn arrays into objects', () => {
29
+ const target = { a: [1, 2, 3] };
30
+ const source = { a: [4, 5] };
31
+ const result = deepMerge(target, source);
32
+ assert.deepEqual(result, { a: [4, 5] });
33
+ });
34
+ });
@@ -0,0 +1,18 @@
1
+ type AnyObject = Record<string, any>;
2
+
3
+ export default function deepMerge(target: AnyObject, source: AnyObject): AnyObject {
4
+ const output = {...target};
5
+ for (const key of Object.keys(source)) {
6
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
7
+ if (!(key in target)) {
8
+ output[key] = source[key];
9
+ } else {
10
+ output[key] = deepMerge(target[key], source[key]);
11
+ }
12
+ }
13
+ else {
14
+ output[key] = source[key];
15
+ }
16
+ }
17
+ return output;
18
+ }
@@ -0,0 +1,13 @@
1
+ export default function getElementHtml(element: Element) {
2
+ const outerHtml = element.outerHTML;
3
+ const innerHtml = element.innerHTML;
4
+ if (!innerHtml) {
5
+ return outerHtml;
6
+ }
7
+ const index = outerHtml.indexOf(innerHtml);
8
+ if (index === -1) {
9
+ // This shouldn't be happening, but if it does, we can just return the outer HTML.
10
+ return outerHtml;
11
+ }
12
+ return outerHtml.slice(0, index) + '…' + outerHtml.slice(index + innerHtml.length);
13
+ }
@@ -0,0 +1,21 @@
1
+ import type { Position } from '../types';
2
+
3
+ export default function getElementPosition(element: Element, win: Window): Position {
4
+ const rect = element.getBoundingClientRect();
5
+ const direction = win.getComputedStyle(element).direction;
6
+ if (direction === 'ltr') {
7
+ return {
8
+ inlineEndLeft: rect.right,
9
+ blockStartTop: rect.top,
10
+ direction
11
+ };
12
+ } else if (direction === 'rtl') {
13
+ return {
14
+ inlineEndLeft: rect.left,
15
+ blockStartTop: rect.top,
16
+ direction
17
+ };
18
+ } else {
19
+ throw new Error(`The element ${element} has a direction "${direction}", which is not supported.`);
20
+ }
21
+ }
@@ -0,0 +1,14 @@
1
+ const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
2
+
3
+ export default function getScrollableAncestors (element: HTMLElement, win: Window) {
4
+ let currentElement = element;
5
+ let scrollableAncestors = new Set<HTMLElement>();
6
+ while (currentElement.parentElement) {
7
+ currentElement = currentElement.parentElement;
8
+ const computedStyle = win.getComputedStyle(currentElement);
9
+ if (scrollableOverflowValues.has(computedStyle.overflowX) || scrollableOverflowValues.has(computedStyle.overflowY)) {
10
+ scrollableAncestors.add(currentElement);
11
+ }
12
+ }
13
+ return scrollableAncestors;
14
+ };
@@ -0,0 +1,27 @@
1
+ import { batch } from '@preact/signals-core';
2
+ import { extendedElementsWithIssues } from '../state.js';
3
+ import getElementPosition from './get-element-position.js';
4
+ import logAndRethrow from '../log-and-rethrow.js';
5
+
6
+ let frameRequested = false;
7
+
8
+ export default function recalculatePositions() {
9
+ if (frameRequested) {
10
+ return;
11
+ }
12
+ frameRequested = true;
13
+ window.requestAnimationFrame(() => {
14
+ try {
15
+ frameRequested = false;
16
+ batch(() => {
17
+ extendedElementsWithIssues.value.forEach(({ element, position, visible }) => {
18
+ if (visible.value && element.isConnected) {
19
+ position.value = getElementPosition(element, window);
20
+ }
21
+ });
22
+ });
23
+ } catch (error) {
24
+ logAndRethrow(error);
25
+ }
26
+ });
27
+ }
@@ -0,0 +1,13 @@
1
+ import { batch } from '@preact/signals-core';
2
+ import { extendedElementsWithIssues } from '../state.js';
3
+ import getScrollableAncestors from './get-scrollable-ancestors.js';
4
+
5
+ export default function recalculateScrollableAncestors() {
6
+ batch(() => {
7
+ extendedElementsWithIssues.value.forEach(({ element, scrollableAncestors }) => {
8
+ if (element.isConnected) {
9
+ scrollableAncestors.value = getScrollableAncestors(element, window);
10
+ }
11
+ });
12
+ });
13
+ }
@@ -0,0 +1,7 @@
1
+ type WindowWithCSS = Window & {
2
+ CSS: typeof CSS
3
+ }
4
+
5
+ export default function supportsAnchorPositioning(win: WindowWithCSS) {
6
+ return win.CSS.supports('anchor-name: --foo') && win.CSS.supports('position-anchor: --foo');
7
+ }
@@ -0,0 +1,124 @@
1
+ import assert from 'node:assert/strict';
2
+ import {suite, test} from 'node:test';
3
+ import transformViolations from './transform-violations';
4
+
5
+ import type { AxeResults } from 'axe-core';
6
+ type Violation = AxeResults['violations'][number];
7
+ type Node = Violation['nodes'][number];
8
+
9
+ const commonViolationProps1: Omit<Violation, 'nodes'> = {
10
+ id: 'id1',
11
+ help: 'help1',
12
+ helpUrl: 'http://example.com',
13
+ description: 'description1',
14
+ tags: [],
15
+ impact: 'serious'
16
+ };
17
+
18
+ const commonViolationProps2: Omit<Violation, 'nodes'> = {
19
+ id: 'id2',
20
+ help: 'help2',
21
+ helpUrl: 'http://example.com',
22
+ description: 'description2',
23
+ tags: [],
24
+ impact: 'serious'
25
+ };
26
+
27
+ // @ts-expect-error element is not HTMLElement
28
+ const element1: HTMLElement = {};
29
+ // @ts-expect-error element is not HTMLElement
30
+ const element2: HTMLElement = {};
31
+ // @ts-expect-error element is not HTMLElement
32
+ const element3: HTMLElement = {};
33
+
34
+ const commonNodeProps = {
35
+ html: '<div></div>',
36
+ any: [],
37
+ all: [],
38
+ none: []
39
+ };
40
+
41
+ const node1: Node = {
42
+ ...commonNodeProps,
43
+ element: element1,
44
+ target: ['div'],
45
+ failureSummary: 'summary1'
46
+ };
47
+
48
+ const node2: Node = {
49
+ ...commonNodeProps,
50
+ element: element2,
51
+ target: ['div'],
52
+ failureSummary: 'summary2'
53
+ };
54
+
55
+ const node3: Node = {
56
+ ...commonNodeProps,
57
+ element: element3,
58
+ target: ['div'],
59
+ failureSummary: 'summary3'
60
+ };
61
+
62
+ suite('transformViolations', () => {
63
+ test('one violation, one element', () => {
64
+ const violation: Violation = {
65
+ ...commonViolationProps1,
66
+ nodes: [node1]
67
+ };
68
+ const elementsWithIssues = transformViolations([violation]);
69
+ assert.equal(elementsWithIssues.length, 1);
70
+ assert.equal(elementsWithIssues[0]?.element, element1);
71
+ assert.equal(elementsWithIssues[0].issues.length, 1);
72
+ assert.equal(elementsWithIssues[0].issues[0]?.id, 'id1');
73
+ assert.equal(elementsWithIssues[0].issues[0]?.description, 'summary1');
74
+ });
75
+
76
+ test('two violations, two elements each', () => {
77
+ const violation1: Violation = {
78
+ ...commonViolationProps1,
79
+ nodes: [node1, node2]
80
+ };
81
+ const violation2: Violation = {
82
+ ...commonViolationProps2,
83
+ nodes: [node1, node3]
84
+ };
85
+ const elementsWithIssues = transformViolations([violation1, violation2]);
86
+ assert.equal(elementsWithIssues.length, 3);
87
+ const elementWithTwoIssues = elementsWithIssues.find(elementWithIssues => elementWithIssues.element === element1);
88
+ assert.equal(elementWithTwoIssues?.issues.length, 2);
89
+ });
90
+
91
+ test('a violation in an iframe', () => {
92
+ const node: Node = {
93
+ ...commonNodeProps,
94
+ element: element1,
95
+ // A target array whose length is > 1 signifies an element in an iframe
96
+ target: ['iframe', 'div'],
97
+ failureSummary: 'summary1'
98
+ };
99
+ const violation: Violation = {
100
+ ...commonViolationProps1,
101
+ nodes: [node]
102
+ };
103
+
104
+ const elementsWithIssues = transformViolations([violation]);
105
+ assert.equal(elementsWithIssues.length, 0);
106
+ });
107
+
108
+ test('a violation in shadow DOM', () => {
109
+ const node: Node = {
110
+ ...commonNodeProps,
111
+ element: element1,
112
+ // A target that contains an array within the outer array signifies an element in shadow DOM
113
+ target: [['div', 'div']],
114
+ failureSummary: 'summary1'
115
+ };
116
+ const violation: Violation = {
117
+ ...commonViolationProps1,
118
+ nodes: [node]
119
+ };
120
+
121
+ const elementsWithIssues = transformViolations([violation]);
122
+ assert.equal(elementsWithIssues.length, 0);
123
+ });
124
+ });
@@ -0,0 +1,56 @@
1
+ import type { AxeResults, ImpactValue } from 'axe-core';
2
+ import type { Issue, ElementWithIssues } from '../types';
3
+
4
+ function impactCompare(a: ImpactValue, b: ImpactValue) {
5
+ const impactOrder = [null, 'minor', 'moderate', 'serious', 'critical'];
6
+ return impactOrder.indexOf(a) - impactOrder.indexOf(b);
7
+ }
8
+
9
+ export default function transformViolations(violations: typeof AxeResults.violations) {
10
+ const elementsWithIssues: Array<ElementWithIssues> = [];
11
+
12
+ for (const violation of violations) {
13
+ for (const node of violation.nodes) {
14
+ const { element, target } = node;
15
+
16
+ // Although axe-core can perform iframe scanning, I haven't succeeded in it,
17
+ // and the docs suggest that the axe-core script should be explicitly included
18
+ // in each of the iframed documents anyway.
19
+ // It seems preferable to disallow iframe scanning and not report issues in elements within iframes
20
+ // in the case that such issues are for some reason reported by axe-core.
21
+ // A consumer of Accented can instead scan the iframed document by calling Accented initialization from that document.
22
+ const isInIframe = target.length > 1;
23
+
24
+ // Highlighting elements in shadow DOM is not yet supported, see https://github.com/pomerantsev/accented/issues/25
25
+ // Until then, we don’t want such elements to be added to the set.
26
+ const isInShadowDOM = Array.isArray(target[0]);
27
+
28
+ if (element && !isInIframe && !isInShadowDOM) {
29
+ const issue: Issue = {
30
+ id: violation.id,
31
+ title: violation.help,
32
+ description: node.failureSummary ?? violation.description,
33
+ url: violation.helpUrl,
34
+ impact: violation.impact ?? null
35
+ };
36
+ const existingElementIndex = elementsWithIssues.findIndex(elementWithIssues => elementWithIssues.element === element);
37
+ if (existingElementIndex === -1) {
38
+ elementsWithIssues.push({
39
+ element,
40
+ issues: [issue]
41
+ });
42
+ } else {
43
+ elementsWithIssues[existingElementIndex]!.issues.push(issue);
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ for (const elementWithIssues of elementsWithIssues) {
50
+ elementWithIssues.issues.sort((a, b) => {
51
+ return -impactCompare(a.impact, b.impact) || a.id.localeCompare(b.id);
52
+ });
53
+ }
54
+
55
+ return elementsWithIssues;
56
+ }
@@ -0,0 +1,283 @@
1
+ import {suite, test} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import type { Signal } from '@preact/signals-core';
4
+ import { signal } from '@preact/signals-core';
5
+ import type { ExtendedElementWithIssues, Issue } from '../types';
6
+ import updateElementsWithIssues from './update-elements-with-issues';
7
+
8
+ import type { AxeResults, ImpactValue } from 'axe-core';
9
+ import type { AccentedTrigger } from '../elements/accented-trigger';
10
+ type Violation = AxeResults['violations'][number];
11
+ type Node = Violation['nodes'][number];
12
+
13
+ const win: Window & { CSS: typeof CSS } = {
14
+ document: {
15
+ // @ts-expect-error the return value is of incorrect type.
16
+ createElement: () => ({
17
+ style: {
18
+ setProperty: () => {}
19
+ },
20
+ dataset: {}
21
+ })
22
+ },
23
+ // @ts-expect-error we're missing a lot of properties
24
+ getComputedStyle: () => ({
25
+ zIndex: '',
26
+ direction: 'ltr'
27
+ }),
28
+ // @ts-expect-error we're missing a lot of properties
29
+ CSS: {
30
+ supports: () => true
31
+ }
32
+ }
33
+
34
+ const getBoundingClientRect = () => ({});
35
+
36
+ // @ts-expect-error element is not HTMLElement
37
+ const element1: HTMLElement = {getBoundingClientRect, isConnected: true};
38
+ // @ts-expect-error element is not HTMLElement
39
+ const element2: HTMLElement = {getBoundingClientRect, isConnected: true};
40
+ // @ts-expect-error element is not HTMLElement
41
+ const element3: HTMLElement = {getBoundingClientRect, isConnected: false};
42
+
43
+ const trigger = win.document.createElement('accented-trigger') as AccentedTrigger;
44
+
45
+ const position = signal({
46
+ inlineEndLeft: 0,
47
+ blockStartTop: 0,
48
+ direction: 'ltr' as const
49
+ });
50
+
51
+ const visible = signal(true);
52
+
53
+ const scrollableAncestors = signal(new Set<HTMLElement>());
54
+
55
+ const commonNodeProps = {
56
+ html: '<div></div>',
57
+ any: [],
58
+ all: [],
59
+ none: [],
60
+ target: ['div']
61
+ };
62
+
63
+ const node1: Node = {
64
+ ...commonNodeProps,
65
+ element: element1,
66
+ };
67
+
68
+ const node2: Node = {
69
+ ...commonNodeProps,
70
+ element: element2,
71
+ };
72
+
73
+ const node3: Node = {
74
+ ...commonNodeProps,
75
+ element: element3,
76
+ };
77
+
78
+ const commonViolationProps = {
79
+ help: 'help',
80
+ helpUrl: 'http://example.com',
81
+ description: 'description',
82
+ tags: [],
83
+ impact: 'serious' as ImpactValue
84
+ };
85
+
86
+ const violation1: Violation = {
87
+ ...commonViolationProps,
88
+ id: 'id1',
89
+ nodes: [node1]
90
+ };
91
+
92
+ const violation2: Violation = {
93
+ ...commonViolationProps,
94
+ id: 'id2',
95
+ nodes: [node2]
96
+ };
97
+
98
+ const violation3: Violation = {
99
+ ...commonViolationProps,
100
+ id: 'id3',
101
+ nodes: [node2]
102
+ };
103
+
104
+ const violation4: Violation = {
105
+ ...commonViolationProps,
106
+ id: 'id4',
107
+ nodes: [node3]
108
+ };
109
+
110
+ const commonIssueProps = {
111
+ title: 'help',
112
+ description: 'description',
113
+ url: 'http://example.com',
114
+ impact: 'serious'
115
+ } as const;
116
+
117
+ const issue1: Issue = {
118
+ id: 'id1',
119
+ ...commonIssueProps
120
+ };
121
+
122
+ const issue2: Issue = {
123
+ id: 'id2',
124
+ ...commonIssueProps
125
+ };
126
+
127
+ const issue3: Issue = {
128
+ id: 'id3',
129
+ ...commonIssueProps
130
+ };
131
+
132
+ suite('updateElementsWithIssues', () => {
133
+ test('no changes', () => {
134
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
135
+ {
136
+ id: 1,
137
+ element: element1,
138
+ position,
139
+ visible,
140
+ trigger,
141
+ scrollableAncestors,
142
+ issues: signal([issue1])
143
+ },
144
+ {
145
+ id: 2,
146
+ element: element2,
147
+ position,
148
+ visible,
149
+ trigger,
150
+ scrollableAncestors,
151
+ issues: signal([issue2])
152
+ }
153
+ ]);
154
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
155
+ assert.equal(extendedElementsWithIssues.value.length, 2);
156
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
157
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
158
+ assert.equal(extendedElementsWithIssues.value[1]?.element, element2);
159
+ assert.equal(extendedElementsWithIssues.value[1]?.issues.value.length, 1);
160
+ });
161
+
162
+ test('one issue added', () => {
163
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
164
+ {
165
+ id: 1,
166
+ element: element1,
167
+ position,
168
+ visible,
169
+ trigger,
170
+ scrollableAncestors,
171
+ issues: signal([issue1])
172
+ },
173
+ {
174
+ id: 2,
175
+ element: element2,
176
+ position,
177
+ visible,
178
+ trigger,
179
+ scrollableAncestors,
180
+ issues: signal([issue2])
181
+ }
182
+ ]);
183
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2, violation3], win, 'accented');
184
+ assert.equal(extendedElementsWithIssues.value.length, 2);
185
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
186
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
187
+ assert.equal(extendedElementsWithIssues.value[1]?.element, element2);
188
+ assert.equal(extendedElementsWithIssues.value[1]?.issues.value.length, 2);
189
+ });
190
+
191
+ test('one issue removed', () => {
192
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
193
+ {
194
+ id: 1,
195
+ element: element1,
196
+ position,
197
+ visible,
198
+ trigger,
199
+ scrollableAncestors,
200
+ issues: signal([issue1])
201
+ },
202
+ {
203
+ id: 2,
204
+ element: element2,
205
+ position,
206
+ visible,
207
+ trigger,
208
+ scrollableAncestors,
209
+ issues: signal([issue2, issue3])
210
+ }
211
+ ]);
212
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
213
+ assert.equal(extendedElementsWithIssues.value.length, 2);
214
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
215
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
216
+ assert.equal(extendedElementsWithIssues.value[1]?.element, element2);
217
+ assert.equal(extendedElementsWithIssues.value[1]?.issues.value.length, 1);
218
+ });
219
+
220
+ test('one element added', () => {
221
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
222
+ {
223
+ id: 1,
224
+ element: element1,
225
+ position,
226
+ visible,
227
+ trigger,
228
+ scrollableAncestors,
229
+ issues: signal([issue1])
230
+ }
231
+ ]);
232
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
233
+ assert.equal(extendedElementsWithIssues.value.length, 2);
234
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
235
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
236
+ assert.equal(extendedElementsWithIssues.value[1]?.element, element2);
237
+ assert.equal(extendedElementsWithIssues.value[1]?.issues.value.length, 1);
238
+ });
239
+
240
+ test('one disconnected element added', () => {
241
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
242
+ {
243
+ id: 1,
244
+ element: element1,
245
+ position,
246
+ visible,
247
+ trigger,
248
+ scrollableAncestors,
249
+ issues: signal([issue1])
250
+ }
251
+ ]);
252
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation4], win, 'accented');
253
+ assert.equal(extendedElementsWithIssues.value.length, 1);
254
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
255
+ });
256
+
257
+ test('one element removed', () => {
258
+ const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
259
+ {
260
+ id: 1,
261
+ element: element1,
262
+ position,
263
+ visible,
264
+ trigger,
265
+ scrollableAncestors,
266
+ issues: signal([issue1])
267
+ },
268
+ {
269
+ id: 2,
270
+ element: element2,
271
+ position,
272
+ visible,
273
+ trigger,
274
+ scrollableAncestors,
275
+ issues: signal([issue2])
276
+ }
277
+ ]);
278
+ updateElementsWithIssues(extendedElementsWithIssues, [violation1], win, 'accented');
279
+ assert.equal(extendedElementsWithIssues.value.length, 1);
280
+ assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
281
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
282
+ });
283
+ });