accented 1.1.1 → 1.1.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 (61) hide show
  1. package/dist/accented.d.ts.map +1 -1
  2. package/dist/accented.js +2 -4
  3. package/dist/accented.js.map +1 -1
  4. package/dist/dom-updater.js +6 -6
  5. package/dist/dom-updater.js.map +1 -1
  6. package/dist/elements/accented-dialog.js +1 -1
  7. package/dist/elements/accented-trigger.js +2 -2
  8. package/dist/elements/accented-trigger.js.map +1 -1
  9. package/dist/intersection-observer.js +2 -2
  10. package/dist/intersection-observer.js.map +1 -1
  11. package/dist/logger.d.ts +1 -4
  12. package/dist/logger.d.ts.map +1 -1
  13. package/dist/logger.js +15 -15
  14. package/dist/logger.js.map +1 -1
  15. package/dist/scanner.d.ts.map +1 -1
  16. package/dist/scanner.js +4 -5
  17. package/dist/scanner.js.map +1 -1
  18. package/dist/state.js +4 -4
  19. package/dist/state.js.map +1 -1
  20. package/dist/utils/get-element-position.d.ts +1 -1
  21. package/dist/utils/get-element-position.d.ts.map +1 -1
  22. package/dist/utils/get-element-position.js +6 -6
  23. package/dist/utils/get-element-position.js.map +1 -1
  24. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  25. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  26. package/dist/utils/get-scrollable-ancestors.js +2 -2
  27. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  28. package/dist/utils/recalculate-positions.js +1 -1
  29. package/dist/utils/recalculate-positions.js.map +1 -1
  30. package/dist/utils/recalculate-scrollable-ancestors.js +1 -1
  31. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  32. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +2 -2
  33. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
  34. package/dist/utils/shadow-dom-aware-mutation-observer.js +29 -27
  35. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
  36. package/dist/utils/supports-anchor-positioning.d.ts +1 -5
  37. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  38. package/dist/utils/supports-anchor-positioning.js +4 -6
  39. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  40. package/dist/utils/update-elements-with-issues.d.ts +1 -4
  41. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  42. package/dist/utils/update-elements-with-issues.js +8 -10
  43. package/dist/utils/update-elements-with-issues.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/accented.ts +2 -4
  46. package/src/dom-updater.ts +6 -6
  47. package/src/elements/accented-dialog.ts +1 -1
  48. package/src/elements/accented-trigger.ts +2 -2
  49. package/src/intersection-observer.ts +2 -2
  50. package/src/logger.ts +15 -15
  51. package/src/scanner.ts +4 -5
  52. package/src/state.ts +5 -5
  53. package/src/utils/get-element-position.ts +6 -6
  54. package/src/utils/get-scrollable-ancestors.ts +2 -2
  55. package/src/utils/recalculate-positions.ts +1 -1
  56. package/src/utils/recalculate-scrollable-ancestors.ts +1 -1
  57. package/src/utils/shadow-dom-aware-mutation-observer.test.ts +413 -0
  58. package/src/utils/shadow-dom-aware-mutation-observer.ts +36 -28
  59. package/src/utils/supports-anchor-positioning.ts +4 -10
  60. package/src/utils/update-elements-with-issues.test.ts +29 -54
  61. package/src/utils/update-elements-with-issues.ts +7 -11
@@ -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
+ });
@@ -1,34 +1,47 @@
1
1
  import { getAccentedElementNames } from '../constants.js';
2
2
  import { isDocument, isDocumentFragment, isElement } from './dom-helpers.js';
3
3
 
4
+ function getShadowRoots(elements: Array<Element | Document | DocumentFragment>) {
5
+ return elements
6
+ .flatMap((element) => [element, ...Array.from(element.querySelectorAll('*'))])
7
+ .reduce<Array<ShadowRoot>>(
8
+ (acc, element) =>
9
+ isElement(element) && element.shadowRoot ? acc.concat(element.shadowRoot) : acc,
10
+ [],
11
+ );
12
+ }
13
+
4
14
  export function createShadowDOMAwareMutationObserver(name: string, callback: MutationCallback) {
15
+ type ObserverMap = Map<ShadowRoot, ShadowDOMAwareMutationObserver>;
16
+
17
+ const accentedElementNames = getAccentedElementNames(name);
18
+
19
+ function getMutationNodes(mutations: Array<MutationRecord>, type: 'addedNodes' | 'removedNodes') {
20
+ return mutations
21
+ .filter((mutation) => mutation.type === 'childList')
22
+ .flatMap((mutation) => Array.from(mutation[type]))
23
+ .filter((node) => isElement(node))
24
+ .filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
25
+ }
26
+
5
27
  class ShadowDOMAwareMutationObserver extends MutationObserver {
6
- #shadowRoots = new Set();
28
+ #shadowRoots: ObserverMap = new Map();
7
29
 
8
30
  #options: MutationObserverInit | undefined;
9
31
 
10
- constructor(callback: MutationCallback) {
32
+ constructor(mutationCallback: MutationCallback) {
11
33
  super((mutations, observer) => {
12
- const accentedElementNames = getAccentedElementNames(name);
13
- const childListMutations = mutations.filter((mutation) => mutation.type === 'childList');
14
-
15
- const newElements = childListMutations
16
- .flatMap((mutation) => [...mutation.addedNodes])
17
- .filter((node) => isElement(node))
18
- .filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
34
+ const newElements = getMutationNodes(mutations, 'addedNodes');
19
35
 
20
36
  this.#observeShadowRoots(newElements);
21
37
 
22
- const removedElements = childListMutations
23
- .flatMap((mutation) => [...mutation.removedNodes])
24
- .filter((node) => isElement(node))
25
- .filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
38
+ const removedElements = getMutationNodes(mutations, 'removedNodes');
26
39
 
27
40
  // Mutation observer has no "unobserve" method, so we're simply deleting
28
41
  // the elements from the set of shadow roots.
29
- this.#deleteShadowRoots(removedElements);
42
+ this.#unobserveShadowRoots(removedElements);
30
43
 
31
- callback(mutations, observer);
44
+ mutationCallback(mutations, observer);
32
45
  });
33
46
  }
34
47
 
@@ -41,31 +54,26 @@ export function createShadowDOMAwareMutationObserver(name: string, callback: Mut
41
54
  }
42
55
 
43
56
  override disconnect(): void {
57
+ this.#unobserveShadowRoots(Array.from(this.#shadowRoots.keys()));
44
58
  this.#shadowRoots.clear();
45
59
  super.disconnect();
46
60
  }
47
61
 
48
62
  #observeShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
49
- const shadowRoots = elements
50
- .flatMap((element) => [...element.querySelectorAll('*')])
51
- .filter((element) => element.shadowRoot)
52
- .map((element) => element.shadowRoot);
63
+ const shadowRoots = getShadowRoots(elements);
53
64
 
54
65
  for (const shadowRoot of shadowRoots) {
55
- if (shadowRoot) {
56
- this.#shadowRoots.add(shadowRoot);
57
- this.observe(shadowRoot, this.#options);
58
- }
66
+ const observer = new ShadowDOMAwareMutationObserver(callback);
67
+ observer.observe(shadowRoot, this.#options);
68
+ this.#shadowRoots.set(shadowRoot, observer);
59
69
  }
60
70
  };
61
71
 
62
- #deleteShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
63
- const shadowRoots = elements
64
- .flatMap((element) => [...element.querySelectorAll('*')])
65
- .filter((element) => element.shadowRoot)
66
- .map((element) => element.shadowRoot);
72
+ #unobserveShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
73
+ const shadowRoots = getShadowRoots(elements);
67
74
 
68
75
  for (const shadowRoot of shadowRoots) {
76
+ this.#shadowRoots.get(shadowRoot)?.disconnect();
69
77
  this.#shadowRoots.delete(shadowRoot);
70
78
  }
71
79
  };
@@ -1,23 +1,17 @@
1
- type WindowWithCSS = Window & {
2
- CSS: typeof CSS;
3
- };
4
-
5
1
  /**
6
2
  * We have to do browser sniffing now and explicitly turn off Anchor positioning in Safari
7
3
  * since anchor positioning is not working correctly in Safari 26 Technology Preview.
8
4
  */
9
- function isWebKit(win: Window) {
10
- const ua = win.navigator.userAgent;
5
+ function isWebKit() {
6
+ const ua = navigator.userAgent;
11
7
  return (/AppleWebKit/.test(ua) && !/Chrome/.test(ua)) || /\b(iPad|iPhone|iPod)\b/.test(ua);
12
8
  }
13
9
 
14
10
  // ATTENTION: sync with the implementation in end-to-end tests.
15
11
  // I didn't find a way to sync this with automatically with the implementation of supportsAnchorPositioning
16
12
  // in end-to-end tests, so it has to be synced manually.
17
- export function supportsAnchorPositioning(win: WindowWithCSS) {
13
+ export function supportsAnchorPositioning() {
18
14
  return (
19
- win.CSS.supports('anchor-name: --foo') &&
20
- win.CSS.supports('position-anchor: --foo') &&
21
- !isWebKit(win)
15
+ CSS.supports('anchor-name: --foo') && CSS.supports('position-anchor: --foo') && !isWebKit()
22
16
  );
23
17
  }
@@ -3,64 +3,45 @@ import { suite, test } from 'node:test';
3
3
  import type { Signal } from '@preact/signals-core';
4
4
  import { signal } from '@preact/signals-core';
5
5
  import type { AxeResults, ImpactValue } from 'axe-core';
6
+ import { JSDOM } from 'jsdom';
6
7
  import type { AccentedTrigger } from '../elements/accented-trigger';
7
8
  import type { ExtendedElementWithIssues, Issue } from '../types';
8
9
  import { updateElementsWithIssues } from './update-elements-with-issues';
9
10
 
11
+ const dom = new JSDOM();
12
+ global.document = dom.window.document;
13
+ global.getComputedStyle = dom.window.getComputedStyle;
14
+ // JSDOM doesn't seem to have CSS, so we mock it
15
+ global.CSS = {
16
+ supports: () => true,
17
+ } as any;
18
+ // Node already has a global `navigator` object,
19
+ // so we're mocking it differently than other globals.
20
+ Object.defineProperty(global, 'navigator', {
21
+ value: { userAgent: dom.window.navigator.userAgent },
22
+ writable: true,
23
+ configurable: true,
24
+ });
25
+
10
26
  type Violation = AxeResults['violations'][number];
11
27
  type AxeNode = Violation['nodes'][number];
12
28
 
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
- contains: () => true,
23
- },
24
- // @ts-expect-error we're missing a lot of properties
25
- getComputedStyle: () => ({
26
- zIndex: '',
27
- direction: 'ltr',
28
- getPropertyValue: () => 'none',
29
- }),
30
- // @ts-expect-error we're missing a lot of properties
31
- CSS: {
32
- supports: () => true,
33
- },
34
- // @ts-expect-error we're missing a lot of properties
35
- navigator: {
36
- userAgent: '',
37
- },
38
- };
29
+ // Create real DOM elements using JSDOM
30
+ const element1 = document.createElement('div');
31
+ element1.setAttribute('id', 'element1');
32
+ document.body.appendChild(element1);
39
33
 
40
- const getBoundingClientRect = () => ({});
41
-
42
- const getRootNode = (): Node => ({}) as Node;
43
-
44
- const baseElement = {
45
- getBoundingClientRect,
46
- getRootNode,
47
- style: {
48
- getPropertyValue: () => '',
49
- },
50
- closest: () => null,
51
- };
34
+ const element2 = document.createElement('div');
35
+ element2.setAttribute('id', 'element2');
36
+ document.body.appendChild(element2);
52
37
 
53
- // @ts-expect-error element is not HTMLElement
54
- const element1: HTMLElement = { ...baseElement, isConnected: true };
55
- // @ts-expect-error element is not HTMLElement
56
- const element2: HTMLElement = { ...baseElement, isConnected: true };
57
- // @ts-expect-error element is not HTMLElement
58
- const element3: HTMLElement = { ...baseElement, isConnected: false };
38
+ const element3 = document.createElement('div');
39
+ element3.setAttribute('id', 'element3');
40
+ // element3 is not connected (not added to document)
59
41
 
60
- // @ts-expect-error rootNode is not Node
61
- const rootNode: Node = {};
42
+ const rootNode = document;
62
43
 
63
- const trigger = win.document.createElement('accented-trigger') as AccentedTrigger;
44
+ const trigger = document.createElement('accented-trigger') as AccentedTrigger;
64
45
 
65
46
  const position = signal({
66
47
  left: 0,
@@ -151,7 +132,7 @@ const issue3: Issue = {
151
132
  };
152
133
 
153
134
  const scanContext = {
154
- include: [win.document],
135
+ include: [document],
155
136
  exclude: [],
156
137
  };
157
138
 
@@ -187,7 +168,6 @@ suite('updateElementsWithIssues', () => {
187
168
  extendedElementsWithIssues,
188
169
  scanContext,
189
170
  violations: [violation1, violation2],
190
- win,
191
171
  name: 'accented',
192
172
  });
193
173
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -228,7 +208,6 @@ suite('updateElementsWithIssues', () => {
228
208
  extendedElementsWithIssues,
229
209
  scanContext,
230
210
  violations: [violation1, violation2, violation3],
231
- win,
232
211
  name: 'accented',
233
212
  });
234
213
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -269,7 +248,6 @@ suite('updateElementsWithIssues', () => {
269
248
  extendedElementsWithIssues,
270
249
  scanContext,
271
250
  violations: [violation1, violation2],
272
- win,
273
251
  name: 'accented',
274
252
  });
275
253
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -298,7 +276,6 @@ suite('updateElementsWithIssues', () => {
298
276
  extendedElementsWithIssues,
299
277
  scanContext,
300
278
  violations: [violation1, violation2],
301
- win,
302
279
  name: 'accented',
303
280
  });
304
281
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -327,7 +304,6 @@ suite('updateElementsWithIssues', () => {
327
304
  extendedElementsWithIssues,
328
305
  scanContext,
329
306
  violations: [violation1, violation4],
330
- win,
331
307
  name: 'accented',
332
308
  });
333
309
  assert.equal(extendedElementsWithIssues.value.length, 1);
@@ -365,7 +341,6 @@ suite('updateElementsWithIssues', () => {
365
341
  extendedElementsWithIssues,
366
342
  scanContext,
367
343
  violations: [violation1],
368
- win,
369
344
  name: 'accented',
370
345
  });
371
346
  assert.equal(extendedElementsWithIssues.value.length, 1);