accented 1.1.1 → 1.2.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.
- package/README.md +2 -2
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +2 -4
- package/dist/accented.js.map +1 -1
- package/dist/dom-updater.js +6 -6
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts +2 -2
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +15 -14
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts +6 -6
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +2 -2
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/intersection-observer.js +2 -2
- package/dist/intersection-observer.js.map +1 -1
- package/dist/logger.d.ts +1 -4
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +15 -15
- package/dist/logger.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +4 -5
- package/dist/scanner.js.map +1 -1
- package/dist/state.js +4 -4
- package/dist/state.js.map +1 -1
- package/dist/utils/get-element-position.d.ts +1 -1
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +6 -6
- package/dist/utils/get-element-position.js.map +1 -1
- package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
- package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
- package/dist/utils/get-scrollable-ancestors.js +2 -2
- package/dist/utils/get-scrollable-ancestors.js.map +1 -1
- package/dist/utils/recalculate-positions.js +1 -1
- package/dist/utils/recalculate-positions.js.map +1 -1
- package/dist/utils/recalculate-scrollable-ancestors.js +1 -1
- package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +4 -4
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.js +29 -29
- package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
- package/dist/utils/supports-anchor-positioning.d.ts +1 -5
- package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
- package/dist/utils/supports-anchor-positioning.js +4 -6
- package/dist/utils/supports-anchor-positioning.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts +1 -4
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +8 -10
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/package.json +5 -5
- package/src/accented.ts +2 -4
- package/src/dom-updater.ts +6 -6
- package/src/elements/accented-dialog.ts +16 -17
- package/src/elements/accented-trigger.ts +2 -2
- package/src/intersection-observer.ts +2 -2
- package/src/logger.ts +15 -15
- package/src/scanner.ts +4 -5
- package/src/state.ts +5 -5
- package/src/utils/get-element-position.ts +6 -6
- package/src/utils/get-scrollable-ancestors.ts +2 -2
- package/src/utils/recalculate-positions.ts +1 -1
- package/src/utils/recalculate-scrollable-ancestors.ts +1 -1
- package/src/utils/shadow-dom-aware-mutation-observer.test.ts +413 -0
- package/src/utils/shadow-dom-aware-mutation-observer.ts +36 -30
- package/src/utils/supports-anchor-positioning.ts +4 -10
- package/src/utils/update-elements-with-issues.test.ts +29 -54
- package/src/utils/update-elements-with-issues.ts +7 -11
package/src/logger.ts
CHANGED
|
@@ -53,8 +53,8 @@ type IssueType = {
|
|
|
53
53
|
|
|
54
54
|
type GroupedByIssueType = Record<string, IssueType>;
|
|
55
55
|
|
|
56
|
-
const getIssueTypeGroups = (
|
|
57
|
-
const groupedByIssueType =
|
|
56
|
+
const getIssueTypeGroups = (elsWithIssues: Array<ElementWithIssues>) => {
|
|
57
|
+
const groupedByIssueType = elsWithIssues.reduce((acc, { element, issues }) => {
|
|
58
58
|
for (const issue of issues) {
|
|
59
59
|
if (!acc[issue.id]) {
|
|
60
60
|
acc[issue.id] = {
|
|
@@ -80,11 +80,11 @@ const getIssueTypeGroups = (elementsWithIssues: Array<ElementWithIssues>) => {
|
|
|
80
80
|
return sorted;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
function logIssuesByElement(
|
|
83
|
+
function logIssuesByElement(elsWithIssues: Array<ElementWithIssues>) {
|
|
84
84
|
// Elements with more severe issues (or with a higher number of issues of the same severity)
|
|
85
85
|
// will appear higher in the output.
|
|
86
86
|
// This way, issues with a higher severity will be prioritized.
|
|
87
|
-
const sortedElementsWithIssues =
|
|
87
|
+
const sortedElementsWithIssues = elsWithIssues.toSorted((a, b) => {
|
|
88
88
|
const impacts = orderedImpacts.toReversed();
|
|
89
89
|
const impactWithDifferentIssueCount = impacts.find((impact) => {
|
|
90
90
|
const aCount = a.issues.filter((issue) => issue.impact === impact).length;
|
|
@@ -172,12 +172,12 @@ function logIssuesByType(issueTypeGroups: Array<IssueType>) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function logNewIssues(
|
|
175
|
-
|
|
175
|
+
elsWithIssues: Array<ElementWithIssues>,
|
|
176
176
|
previousElementsWithIssues: Array<ElementWithIssues>,
|
|
177
177
|
) {
|
|
178
178
|
// The elements with accessibility issues that didn't have any associated issues
|
|
179
179
|
// or that weren't in the DOM at the time of last scan.
|
|
180
|
-
const addedElements =
|
|
180
|
+
const addedElements = elsWithIssues.filter((elementWithIssues) => {
|
|
181
181
|
return !previousElementsWithIssues.some((previousElementWithIssues) =>
|
|
182
182
|
areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues),
|
|
183
183
|
);
|
|
@@ -185,7 +185,7 @@ function logNewIssues(
|
|
|
185
185
|
|
|
186
186
|
// The elements that now have more issues than at the time of last scan,
|
|
187
187
|
// with just the new issues (previously existing issues are filtered out).
|
|
188
|
-
const existingElementsWithNewIssues =
|
|
188
|
+
const existingElementsWithNewIssues = elsWithIssues.reduce<Array<ElementWithIssues>>(
|
|
189
189
|
(acc, elementWithIssues) => {
|
|
190
190
|
let foundElementWithIssues: ElementWithIssues | null = null;
|
|
191
191
|
for (const previousElementWithIssues of previousElementsWithIssues) {
|
|
@@ -232,17 +232,17 @@ function logNewIssues(
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
function logIssues(
|
|
235
|
-
|
|
235
|
+
elsWithIssues: Array<ElementWithIssues>,
|
|
236
236
|
previousElementsWithIssues: Array<ElementWithIssues>,
|
|
237
237
|
) {
|
|
238
|
-
const elementCount =
|
|
238
|
+
const elementCount = elsWithIssues.length;
|
|
239
239
|
|
|
240
240
|
if (elementCount === 0) {
|
|
241
241
|
console.log(`No accessibility issues (Accented, ${accentedUrl}).`);
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
const issueCount =
|
|
245
|
+
const issueCount = elsWithIssues.reduce((acc, { issues }) => acc + issues.length, 0);
|
|
246
246
|
console.group(
|
|
247
247
|
`%c${issueCount} accessibility issue${issueCount === 1 ? '' : 's'} in ${elementCount} element${elementCount === 1 ? '' : 's'} (Accented, ${accentedUrl}):\n`,
|
|
248
248
|
defaultStyle,
|
|
@@ -251,16 +251,16 @@ function logIssues(
|
|
|
251
251
|
if (issueCount <= MAX_ISSUES_BEFORE_OUTPUT_COLLAPSE) {
|
|
252
252
|
// Don't collapse issues if there are not too many (this hopefully helps user avoid unnecessary clicks in the console).
|
|
253
253
|
// Output by element (no specific reason for this choice, just a preference).
|
|
254
|
-
logIssuesByElement(
|
|
254
|
+
logIssuesByElement(elsWithIssues);
|
|
255
255
|
} else {
|
|
256
256
|
// When there are many issues, outputting them all would probably make the console too noisy,
|
|
257
257
|
// so we collapse them.
|
|
258
258
|
// Moreover, we output all issues twice, by element and by issue type, to give users more choice.
|
|
259
|
-
console.groupCollapsed(`%cAll by element (${
|
|
260
|
-
logIssuesByElement(
|
|
259
|
+
console.groupCollapsed(`%cAll by element (${elsWithIssues.length})`, defaultStyle);
|
|
260
|
+
logIssuesByElement(elsWithIssues);
|
|
261
261
|
console.groupEnd();
|
|
262
262
|
|
|
263
|
-
const issueTypeGroups = getIssueTypeGroups(
|
|
263
|
+
const issueTypeGroups = getIssueTypeGroups(elsWithIssues);
|
|
264
264
|
console.groupCollapsed(`%cAll by issue type (${issueTypeGroups.length})`, defaultStyle);
|
|
265
265
|
logIssuesByType(issueTypeGroups);
|
|
266
266
|
console.groupEnd();
|
|
@@ -269,7 +269,7 @@ function logIssues(
|
|
|
269
269
|
if (previousElementsWithIssues.length > 0) {
|
|
270
270
|
// Log new issues separately, to make it easier for the user to know what issues
|
|
271
271
|
// were introduced recently.
|
|
272
|
-
logNewIssues(
|
|
272
|
+
logNewIssues(elsWithIssues, previousElementsWithIssues);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
console.groupEnd();
|
package/src/scanner.ts
CHANGED
|
@@ -96,7 +96,6 @@ export function createScanner(
|
|
|
96
96
|
extendedElementsWithIssues,
|
|
97
97
|
scanContext,
|
|
98
98
|
violations: result.violations,
|
|
99
|
-
win: window,
|
|
100
99
|
name,
|
|
101
100
|
});
|
|
102
101
|
|
|
@@ -143,7 +142,7 @@ export function createScanner(
|
|
|
143
142
|
return !(onlyAccentedElementsAddedOrRemoved || accentedElementChanged);
|
|
144
143
|
});
|
|
145
144
|
|
|
146
|
-
if (listWithoutAccentedElements.length !== 0 && !supportsAnchorPositioning(
|
|
145
|
+
if (listWithoutAccentedElements.length !== 0 && !supportsAnchorPositioning()) {
|
|
147
146
|
// Something has changed in the DOM, so we need to realign all triggers with respective elements.
|
|
148
147
|
recalculatePositions();
|
|
149
148
|
|
|
@@ -159,14 +158,14 @@ export function createScanner(
|
|
|
159
158
|
// we may miss other mutations on those same elements caused by Accented,
|
|
160
159
|
// leading to extra runs of the mutation observer.
|
|
161
160
|
const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce(
|
|
162
|
-
(
|
|
161
|
+
(acc, mutationRecord) => {
|
|
163
162
|
if (
|
|
164
163
|
mutationRecord.type === 'attributes' &&
|
|
165
164
|
mutationRecord.attributeName === `data-${name}`
|
|
166
165
|
) {
|
|
167
|
-
|
|
166
|
+
acc.add(mutationRecord.target);
|
|
168
167
|
}
|
|
169
|
-
return
|
|
168
|
+
return acc;
|
|
170
169
|
},
|
|
171
170
|
new Set<Node>(),
|
|
172
171
|
);
|
package/src/state.ts
CHANGED
|
@@ -14,10 +14,10 @@ export const elementsWithIssues = computed<Array<ElementWithIssues>>(() =>
|
|
|
14
14
|
})),
|
|
15
15
|
);
|
|
16
16
|
|
|
17
|
-
export const rootNodes = computed
|
|
17
|
+
export const rootNodes = computed(
|
|
18
18
|
() =>
|
|
19
19
|
new Set(
|
|
20
|
-
(enabled.value ? [document
|
|
20
|
+
Array.from<Node>(enabled.value ? [document] : []).concat(
|
|
21
21
|
...extendedElementsWithIssues.value.map(
|
|
22
22
|
(extendedElementWithIssues) => extendedElementWithIssues.rootNode,
|
|
23
23
|
),
|
|
@@ -26,10 +26,10 @@ export const rootNodes = computed<Set<Node>>(
|
|
|
26
26
|
);
|
|
27
27
|
|
|
28
28
|
export const scrollableAncestors = computed<Set<Element>>(() =>
|
|
29
|
-
extendedElementsWithIssues.value.reduce((
|
|
29
|
+
extendedElementsWithIssues.value.reduce((acc, extendedElementWithIssues) => {
|
|
30
30
|
for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
|
|
31
|
-
|
|
31
|
+
acc.add(scrollableAncestor);
|
|
32
32
|
}
|
|
33
|
-
return
|
|
33
|
+
return acc;
|
|
34
34
|
}, new Set<Element>()),
|
|
35
35
|
);
|
|
@@ -4,8 +4,8 @@ import { isHtmlElement } from './dom-helpers.js';
|
|
|
4
4
|
import { getParent } from './get-parent.js';
|
|
5
5
|
|
|
6
6
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block#identifying_the_containing_block
|
|
7
|
-
function isContainingBlock(element: Element
|
|
8
|
-
const style =
|
|
7
|
+
function isContainingBlock(element: Element): boolean {
|
|
8
|
+
const style = getComputedStyle(element);
|
|
9
9
|
const {
|
|
10
10
|
transform,
|
|
11
11
|
perspective,
|
|
@@ -33,11 +33,11 @@ function isContainingBlock(element: Element, win: Window): boolean {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function getNonInitialContainingBlock(element: Element
|
|
36
|
+
function getNonInitialContainingBlock(element: Element): Element | null {
|
|
37
37
|
let currentElement: Element | null = element;
|
|
38
38
|
while (currentElement) {
|
|
39
39
|
currentElement = getParent(currentElement);
|
|
40
|
-
if (currentElement && isContainingBlock(currentElement
|
|
40
|
+
if (currentElement && isContainingBlock(currentElement)) {
|
|
41
41
|
return currentElement;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -52,8 +52,8 @@ function getNonInitialContainingBlock(element: Element, win: Window): Element |
|
|
|
52
52
|
* * The element itself, or one of the element's ancestors has a scale or rotate transform.
|
|
53
53
|
* * The browser doesn't support anchor positioning.
|
|
54
54
|
*/
|
|
55
|
-
export function getElementPosition(element: Element
|
|
56
|
-
const nonInitialContainingBlock = getNonInitialContainingBlock(element
|
|
55
|
+
export function getElementPosition(element: Element): Position {
|
|
56
|
+
const nonInitialContainingBlock = getNonInitialContainingBlock(element);
|
|
57
57
|
// If an element has a containing block as an ancestor,
|
|
58
58
|
// and that containing block is not the <html> element (the initial containing block),
|
|
59
59
|
// fixed positioning works differently.
|
|
@@ -2,7 +2,7 @@ import { getParent } from './get-parent.js';
|
|
|
2
2
|
|
|
3
3
|
const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
|
|
4
4
|
|
|
5
|
-
export function getScrollableAncestors(element: Element
|
|
5
|
+
export function getScrollableAncestors(element: Element) {
|
|
6
6
|
let currentElement: Element | null = element;
|
|
7
7
|
const scrollableAncestors = new Set<Element>();
|
|
8
8
|
while (true) {
|
|
@@ -10,7 +10,7 @@ export function getScrollableAncestors(element: Element, win: Window) {
|
|
|
10
10
|
if (!currentElement) {
|
|
11
11
|
break;
|
|
12
12
|
}
|
|
13
|
-
const computedStyle =
|
|
13
|
+
const computedStyle = getComputedStyle(currentElement);
|
|
14
14
|
if (
|
|
15
15
|
scrollableOverflowValues.has(computedStyle.overflowX) ||
|
|
16
16
|
scrollableOverflowValues.has(computedStyle.overflowY)
|
|
@@ -16,7 +16,7 @@ export function recalculatePositions() {
|
|
|
16
16
|
batch(() => {
|
|
17
17
|
for (const { element, position, visible } of extendedElementsWithIssues.value) {
|
|
18
18
|
if (visible.value && element.isConnected) {
|
|
19
|
-
position.value = getElementPosition(element
|
|
19
|
+
position.value = getElementPosition(element);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
});
|
|
@@ -6,7 +6,7 @@ export function recalculateScrollableAncestors() {
|
|
|
6
6
|
batch(() => {
|
|
7
7
|
for (const { element, scrollableAncestors } of extendedElementsWithIssues.value) {
|
|
8
8
|
if (element.isConnected) {
|
|
9
|
-
scrollableAncestors.value = getScrollableAncestors(element
|
|
9
|
+
scrollableAncestors.value = getScrollableAncestors(element);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
});
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { beforeEach, suite, test } from 'node:test';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import { createShadowDOMAwareMutationObserver } from './shadow-dom-aware-mutation-observer';
|
|
5
|
+
|
|
6
|
+
type MutationAssertion = {
|
|
7
|
+
type: string;
|
|
8
|
+
target?: Node;
|
|
9
|
+
addedNodes?: { length: number; tagName?: string };
|
|
10
|
+
removedNodes?: { length: number; tagName?: string };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createTestDOM = (html = '<div id="container"></div>') => {
|
|
14
|
+
const dom = new JSDOM(html);
|
|
15
|
+
return { dom, document: dom.window.document };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const waitForMutation = (ms = 10) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
20
|
+
const createElementWithShadowRoot = (document: Document, tagName = 'div') => {
|
|
21
|
+
const element = document.createElement(tagName);
|
|
22
|
+
const shadowRoot = element.attachShadow({ mode: 'open' });
|
|
23
|
+
return { element, shadowRoot };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const assertMutationEquals = (mutation: MutationRecord, expected: MutationAssertion) => {
|
|
27
|
+
assert.equal(mutation.type, expected.type);
|
|
28
|
+
if (expected.target) {
|
|
29
|
+
assert.equal(mutation.target, expected.target);
|
|
30
|
+
}
|
|
31
|
+
if (expected.addedNodes) {
|
|
32
|
+
assert.equal(mutation.addedNodes.length, expected.addedNodes.length);
|
|
33
|
+
if (expected.addedNodes.tagName) {
|
|
34
|
+
assert.equal((mutation.addedNodes[0] as Element)?.tagName, expected.addedNodes.tagName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (expected.removedNodes) {
|
|
38
|
+
assert.equal(mutation.removedNodes.length, expected.removedNodes.length);
|
|
39
|
+
if (expected.removedNodes.tagName) {
|
|
40
|
+
assert.equal((mutation.removedNodes[0] as Element)?.tagName, expected.removedNodes.tagName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createAsyncMutationTest = (expectedMutations: number) => {
|
|
46
|
+
let mutationCount = 0;
|
|
47
|
+
let resolveTest: () => void;
|
|
48
|
+
const testComplete = new Promise<void>((resolve) => {
|
|
49
|
+
resolveTest = resolve;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handleMutation =
|
|
53
|
+
(callback: (mutations: MutationRecord[], count: number) => void) =>
|
|
54
|
+
(mutations: MutationRecord[]) => {
|
|
55
|
+
mutationCount++;
|
|
56
|
+
callback(mutations, mutationCount);
|
|
57
|
+
|
|
58
|
+
if (mutationCount === expectedMutations) {
|
|
59
|
+
resolveTest();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return { handleMutation, testComplete, getMutationCount: () => mutationCount };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
suite('ShadowDOMAwareMutationObserver', () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
const dom = new JSDOM();
|
|
69
|
+
global.Node = dom.window.Node;
|
|
70
|
+
global.MutationObserver = dom.window.MutationObserver;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
suite('inserting elements', () => {
|
|
74
|
+
test('fires when a node is inserted in an observed node', (_t, done) => {
|
|
75
|
+
const { document } = createTestDOM();
|
|
76
|
+
const container = document.querySelector('#container')!;
|
|
77
|
+
|
|
78
|
+
const observer = createShadowDOMAwareMutationObserver('test', (mutations) => {
|
|
79
|
+
assert.equal(mutations.length, 1);
|
|
80
|
+
assertMutationEquals(mutations[0]!, {
|
|
81
|
+
type: 'childList',
|
|
82
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
done();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
observer.observe(container, { childList: true });
|
|
89
|
+
|
|
90
|
+
const newDiv = document.createElement('div');
|
|
91
|
+
container.appendChild(newDiv);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('fires when a node is inserted in a shadow root of an observed element', (_t, done) => {
|
|
95
|
+
const { document } = createTestDOM('<div id="container"><div id="host"></div></div>');
|
|
96
|
+
const container = document.querySelector('#container')!;
|
|
97
|
+
const host = document.querySelector('#host')!;
|
|
98
|
+
|
|
99
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
100
|
+
|
|
101
|
+
const observer = createShadowDOMAwareMutationObserver('test', (mutations) => {
|
|
102
|
+
assert.equal(mutations.length, 1);
|
|
103
|
+
assertMutationEquals(mutations[0]!, {
|
|
104
|
+
type: 'childList',
|
|
105
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
observer.observe(container, { childList: true });
|
|
112
|
+
|
|
113
|
+
const newDiv = document.createElement('div');
|
|
114
|
+
shadowRoot.appendChild(newDiv);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// This is an unfortunate limitation: we cannot be notified when a shadow root is created
|
|
118
|
+
// in the observed DOM. When that happens, the whole shadow root subtree will stay unobserved.
|
|
119
|
+
test('does not fire when a node is inserted in a shadow root created after observer starts', (_t, done) => {
|
|
120
|
+
const { document } = createTestDOM('<div id="container"><div id="host"></div></div>');
|
|
121
|
+
const container = document.querySelector('#container')!;
|
|
122
|
+
const host = document.querySelector('#host')!;
|
|
123
|
+
|
|
124
|
+
let callbackFired = false;
|
|
125
|
+
const observer = createShadowDOMAwareMutationObserver('test', () => {
|
|
126
|
+
callbackFired = true;
|
|
127
|
+
done(new Error('Callback should not have fired'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
131
|
+
|
|
132
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
133
|
+
const newDiv = document.createElement('div');
|
|
134
|
+
shadowRoot.appendChild(newDiv);
|
|
135
|
+
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
assert.equal(
|
|
138
|
+
callbackFired,
|
|
139
|
+
false,
|
|
140
|
+
'Callback should not have fired for shadow root created after observation',
|
|
141
|
+
);
|
|
142
|
+
done();
|
|
143
|
+
}, 50);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('discovers shadow roots when descendants with shadow roots are added', async () => {
|
|
147
|
+
// This test verifies that when a wrapper element containing descendant shadow hosts
|
|
148
|
+
// is added to the DOM, the shadow roots are properly discovered and observed
|
|
149
|
+
const { document } = createTestDOM();
|
|
150
|
+
const container = document.querySelector('#container')!;
|
|
151
|
+
|
|
152
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
153
|
+
|
|
154
|
+
const { element: hostElement, shadowRoot } = createElementWithShadowRoot(document);
|
|
155
|
+
const wrapper = document.createElement('div');
|
|
156
|
+
wrapper.appendChild(hostElement);
|
|
157
|
+
|
|
158
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
159
|
+
'test',
|
|
160
|
+
handleMutation((mutations, count) => {
|
|
161
|
+
assert.equal(mutations.length, 1);
|
|
162
|
+
|
|
163
|
+
if (count === 1) {
|
|
164
|
+
// First mutation: wrapper with descendant that has shadow root
|
|
165
|
+
assertMutationEquals(mutations[0]!, {
|
|
166
|
+
type: 'childList',
|
|
167
|
+
target: container,
|
|
168
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
169
|
+
});
|
|
170
|
+
} else if (count === 2) {
|
|
171
|
+
// Second mutation: element added to the shadow root that was discovered
|
|
172
|
+
assertMutationEquals(mutations[0]!, {
|
|
173
|
+
type: 'childList',
|
|
174
|
+
target: shadowRoot,
|
|
175
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
182
|
+
|
|
183
|
+
container.appendChild(wrapper);
|
|
184
|
+
await waitForMutation();
|
|
185
|
+
|
|
186
|
+
const shadowDiv = document.createElement('span');
|
|
187
|
+
shadowRoot.appendChild(shadowDiv);
|
|
188
|
+
|
|
189
|
+
await testComplete;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('discovers shadow roots on directly added elements', async () => {
|
|
193
|
+
// This test verifies that shadow roots are discovered on elements that are
|
|
194
|
+
// added directly to the observed container (not just on descendants)
|
|
195
|
+
const { document } = createTestDOM();
|
|
196
|
+
const container = document.querySelector('#container')!;
|
|
197
|
+
|
|
198
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
199
|
+
const { element: hostElement, shadowRoot } = createElementWithShadowRoot(document);
|
|
200
|
+
|
|
201
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
202
|
+
'test',
|
|
203
|
+
handleMutation((mutations, count) => {
|
|
204
|
+
assert.equal(mutations.length, 1);
|
|
205
|
+
|
|
206
|
+
if (count === 1) {
|
|
207
|
+
// First mutation: element with shadow root added directly
|
|
208
|
+
assertMutationEquals(mutations[0]!, {
|
|
209
|
+
type: 'childList',
|
|
210
|
+
target: container,
|
|
211
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
212
|
+
});
|
|
213
|
+
} else if (count === 2) {
|
|
214
|
+
// Second mutation: element added to the shadow root on the directly added element
|
|
215
|
+
assertMutationEquals(mutations[0]!, {
|
|
216
|
+
type: 'childList',
|
|
217
|
+
target: shadowRoot,
|
|
218
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
225
|
+
|
|
226
|
+
// Add element with shadow root directly to container
|
|
227
|
+
// This should now work with the bug fix (previously didn't work)
|
|
228
|
+
container.appendChild(hostElement);
|
|
229
|
+
await waitForMutation();
|
|
230
|
+
|
|
231
|
+
const shadowDiv = document.createElement('span');
|
|
232
|
+
shadowRoot.appendChild(shadowDiv);
|
|
233
|
+
|
|
234
|
+
await testComplete;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('discovers and observes nested shadow roots (two levels deep)', async () => {
|
|
238
|
+
const { document } = createTestDOM();
|
|
239
|
+
const container = document.querySelector('#container')!;
|
|
240
|
+
|
|
241
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(3);
|
|
242
|
+
const { element: outerElement, shadowRoot: outerShadowRoot } =
|
|
243
|
+
createElementWithShadowRoot(document);
|
|
244
|
+
const { element: innerElement, shadowRoot: innerShadowRoot } = createElementWithShadowRoot(
|
|
245
|
+
document,
|
|
246
|
+
'span',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
250
|
+
'test',
|
|
251
|
+
handleMutation((mutations, count) => {
|
|
252
|
+
assert.equal(mutations.length, 1);
|
|
253
|
+
|
|
254
|
+
if (count === 1) {
|
|
255
|
+
// First mutation: outer element with shadow root added
|
|
256
|
+
assertMutationEquals(mutations[0]!, {
|
|
257
|
+
type: 'childList',
|
|
258
|
+
target: container,
|
|
259
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
260
|
+
});
|
|
261
|
+
} else if (count === 2) {
|
|
262
|
+
// Second mutation: inner element with nested shadow root added to outer shadow root
|
|
263
|
+
assertMutationEquals(mutations[0]!, {
|
|
264
|
+
type: 'childList',
|
|
265
|
+
target: outerShadowRoot,
|
|
266
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
267
|
+
});
|
|
268
|
+
} else if (count === 3) {
|
|
269
|
+
// Third mutation: element added to nested (inner) shadow root
|
|
270
|
+
assertMutationEquals(mutations[0]!, {
|
|
271
|
+
type: 'childList',
|
|
272
|
+
target: innerShadowRoot,
|
|
273
|
+
addedNodes: { length: 1, tagName: 'P' },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
280
|
+
|
|
281
|
+
container.appendChild(outerElement);
|
|
282
|
+
await waitForMutation();
|
|
283
|
+
|
|
284
|
+
outerShadowRoot.appendChild(innerElement);
|
|
285
|
+
await waitForMutation();
|
|
286
|
+
|
|
287
|
+
const deepElement = document.createElement('p');
|
|
288
|
+
innerShadowRoot.appendChild(deepElement);
|
|
289
|
+
|
|
290
|
+
await testComplete;
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
suite('removing elements', () => {
|
|
295
|
+
// Tests for proper cleanup when elements are removed from the DOM
|
|
296
|
+
test('does not fire when elements are added to removed elements', async () => {
|
|
297
|
+
const { document } = createTestDOM();
|
|
298
|
+
const container = document.querySelector('#container')!;
|
|
299
|
+
|
|
300
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
301
|
+
let completionScheduled = false;
|
|
302
|
+
|
|
303
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
304
|
+
'test',
|
|
305
|
+
handleMutation((mutations, count) => {
|
|
306
|
+
assert.equal(mutations.length, 1);
|
|
307
|
+
|
|
308
|
+
if (count === 1) {
|
|
309
|
+
// First mutation: element added to container
|
|
310
|
+
assertMutationEquals(mutations[0]!, {
|
|
311
|
+
type: 'childList',
|
|
312
|
+
target: container,
|
|
313
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
314
|
+
});
|
|
315
|
+
} else if (count === 2) {
|
|
316
|
+
// Second mutation: element removed from container
|
|
317
|
+
assertMutationEquals(mutations[0]!, {
|
|
318
|
+
type: 'childList',
|
|
319
|
+
target: container,
|
|
320
|
+
removedNodes: { length: 1, tagName: 'DIV' },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Schedule completion check to ensure no third mutation occurs
|
|
324
|
+
if (!completionScheduled) {
|
|
325
|
+
completionScheduled = true;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
throw new Error(`Unexpected mutation count: ${count}`);
|
|
329
|
+
}
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
334
|
+
|
|
335
|
+
const testElement = document.createElement('div');
|
|
336
|
+
container.appendChild(testElement);
|
|
337
|
+
await waitForMutation();
|
|
338
|
+
|
|
339
|
+
container.removeChild(testElement);
|
|
340
|
+
await waitForMutation();
|
|
341
|
+
|
|
342
|
+
// Add element to the removed element (should NOT trigger mutation)
|
|
343
|
+
const childElement = document.createElement('span');
|
|
344
|
+
testElement.appendChild(childElement);
|
|
345
|
+
|
|
346
|
+
await testComplete;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('does not fire when elements are added to shadow roots of removed hosts', async () => {
|
|
350
|
+
const { document } = createTestDOM();
|
|
351
|
+
const container = document.querySelector('#container')!;
|
|
352
|
+
|
|
353
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(3);
|
|
354
|
+
const { element: shadowHost, shadowRoot } = createElementWithShadowRoot(document);
|
|
355
|
+
let completionScheduled = false;
|
|
356
|
+
|
|
357
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
358
|
+
'test',
|
|
359
|
+
handleMutation((mutations, count) => {
|
|
360
|
+
assert.equal(mutations.length, 1);
|
|
361
|
+
|
|
362
|
+
if (count === 1) {
|
|
363
|
+
// First mutation: shadow host added to container
|
|
364
|
+
assertMutationEquals(mutations[0]!, {
|
|
365
|
+
type: 'childList',
|
|
366
|
+
target: container,
|
|
367
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
368
|
+
});
|
|
369
|
+
} else if (count === 2) {
|
|
370
|
+
// Second mutation: element added to shadow root
|
|
371
|
+
assertMutationEquals(mutations[0]!, {
|
|
372
|
+
type: 'childList',
|
|
373
|
+
target: shadowRoot,
|
|
374
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
375
|
+
});
|
|
376
|
+
} else if (count === 3) {
|
|
377
|
+
// Third mutation: shadow host removed from container
|
|
378
|
+
assertMutationEquals(mutations[0]!, {
|
|
379
|
+
type: 'childList',
|
|
380
|
+
target: container,
|
|
381
|
+
removedNodes: { length: 1, tagName: 'DIV' },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Schedule completion check to ensure no fourth mutation occurs
|
|
385
|
+
if (!completionScheduled) {
|
|
386
|
+
completionScheduled = true;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
throw new Error(`Unexpected mutation count: ${count}`);
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
395
|
+
|
|
396
|
+
container.appendChild(shadowHost);
|
|
397
|
+
await waitForMutation();
|
|
398
|
+
|
|
399
|
+
const shadowChild = document.createElement('span');
|
|
400
|
+
shadowRoot.appendChild(shadowChild);
|
|
401
|
+
await waitForMutation();
|
|
402
|
+
|
|
403
|
+
container.removeChild(shadowHost);
|
|
404
|
+
await waitForMutation();
|
|
405
|
+
|
|
406
|
+
// Add another element to the shadow root of removed host (should NOT trigger mutation)
|
|
407
|
+
const anotherShadowChild = document.createElement('p');
|
|
408
|
+
shadowRoot.appendChild(anotherShadowChild);
|
|
409
|
+
|
|
410
|
+
await testComplete;
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|