Skip to content

Commit 09b76f5

Browse files
test: add coverage for shadow-aware aria-owns, aria-flowto, and nested ID lookup
- aria-owns referencing element inside shadow DOM - aria-owns referencing element nested 2 shadow levels deep - aria-flowto referencing element inside shadow DOM - Fixes coverage threshold failures (getNodeByIdRef now 100%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff0ba7e commit 09b76f5

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

test/int/shadowDom.int.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,116 @@ describe("Shadow DOM", () => {
158158
expect(log).toContain("end of document");
159159
});
160160

161+
it("should resolve aria-owns referencing an element inside shadow DOM", async () => {
162+
// Container with aria-owns pointing to an ID inside a shadow root
163+
const owner = document.createElement("div");
164+
owner.setAttribute("role", "listbox");
165+
owner.setAttribute("aria-label", "Owner");
166+
owner.setAttribute("aria-owns", "shadow-option");
167+
document.body.appendChild(owner);
168+
169+
const host = document.createElement("div");
170+
document.body.appendChild(host);
171+
172+
const shadow = host.attachShadow({ mode: "open" });
173+
const option = document.createElement("div");
174+
option.setAttribute("role", "option");
175+
option.setAttribute("id", "shadow-option");
176+
option.textContent = "Shadow Option";
177+
shadow.appendChild(option);
178+
179+
await virtual.start({ container: document.body });
180+
181+
while ((await virtual.lastSpokenPhrase()) !== "end of document") {
182+
await virtual.next();
183+
}
184+
185+
const log = await virtual.spokenPhraseLog();
186+
187+
// The owned element inside shadow DOM should be found and reparented.
188+
// Listbox and option roles include extra ARIA attribute announcements.
189+
const hasListbox = log.some((p) => p.includes("listbox, Owner"));
190+
const hasOption = log.some(
191+
(p) => p.includes("option, Shadow Option") && p.includes("position 1")
192+
);
193+
expect(hasListbox).toBe(true);
194+
expect(hasOption).toBe(true);
195+
});
196+
197+
it("should resolve aria-flowto referencing an element inside shadow DOM", async () => {
198+
const host = document.createElement("div");
199+
document.body.appendChild(host);
200+
201+
const shadow = host.attachShadow({ mode: "open" });
202+
const target = document.createElement("div");
203+
target.setAttribute("id", "flow-target");
204+
target.setAttribute("role", "region");
205+
target.setAttribute("aria-label", "Target Region");
206+
target.textContent = "Flow target content";
207+
shadow.appendChild(target);
208+
209+
// Source element with aria-flowto pointing into shadow DOM
210+
const source = document.createElement("button");
211+
source.setAttribute("aria-flowto", "flow-target");
212+
source.textContent = "Source";
213+
document.body.appendChild(source);
214+
215+
await virtual.start({ container: document.body });
216+
217+
while ((await virtual.lastSpokenPhrase()) !== "end of document") {
218+
await virtual.next();
219+
}
220+
221+
const log = await virtual.spokenPhraseLog();
222+
223+
// Both the source and the shadow DOM target should be in the tree.
224+
// aria-flowto adds "alternate reading order" annotations to spoken output.
225+
const hasSource = log.some((p) => p.includes("button, Source"));
226+
const hasTarget = log.some((p) => p.includes("region, Target Region"));
227+
expect(hasSource).toBe(true);
228+
expect(hasTarget).toBe(true);
229+
});
230+
231+
it("should resolve aria-owns referencing an element nested two shadow levels deep", async () => {
232+
const owner = document.createElement("div");
233+
owner.setAttribute("role", "listbox");
234+
owner.setAttribute("aria-label", "Deep Owner");
235+
owner.setAttribute("aria-owns", "deep-option");
236+
document.body.appendChild(owner);
237+
238+
// Level 1: outer shadow host
239+
const outerHost = document.createElement("div");
240+
document.body.appendChild(outerHost);
241+
const outerShadow = outerHost.attachShadow({ mode: "open" });
242+
243+
// Level 2: inner shadow host inside outer shadow
244+
const innerHost = document.createElement("div");
245+
outerShadow.appendChild(innerHost);
246+
const innerShadow = innerHost.attachShadow({ mode: "open" });
247+
248+
// The target element is inside the innermost shadow root
249+
const option = document.createElement("div");
250+
option.setAttribute("role", "option");
251+
option.setAttribute("id", "deep-option");
252+
option.textContent = "Deep Option";
253+
innerShadow.appendChild(option);
254+
255+
await virtual.start({ container: document.body });
256+
257+
while ((await virtual.lastSpokenPhrase()) !== "end of document") {
258+
await virtual.next();
259+
}
260+
261+
const log = await virtual.spokenPhraseLog();
262+
263+
const hasListbox = log.some((p) => p.includes("listbox, Deep Owner"));
264+
const hasOption = log.some(
265+
(p) => p.includes("option, Deep Option") && p.includes("position 1")
266+
);
267+
expect(hasListbox).toBe(true);
268+
expect(hasOption).toBe(true);
269+
});
270+
161271
it("should not traverse closed shadow roots", async () => {
162272
const host = document.createElement("div");
163273
document.body.appendChild(host);

0 commit comments

Comments
 (0)