1
'); const stable = await observe(page); try { await page.locator("#ticker").evaluate((e) => (e.textContent = "2")); await stable.execute( "CLICK", stable.data.targets[0], undefined, new AbortController().signal, ); assert.ok(page.url().endsWith("#one")); } finally { await stable.dispose(); } const changed = await observe(page); try { await page.locator("a").evaluate((e) => e.setAttribute("href", "#other")); await assert.rejects( changed.execute( "CLICK", changed.data.targets[0], undefined, new AbortController().signal, ), /changed/, ); } finally { await changed.dispose(); } } finally { await browser.close(); } }); test("observation exposes hidden radio options, covers, and offscreen choices", async () => { const browser = await launchTestBrowser(); try { const page = await browser.newPage(); await page.setContent( '', ); const snapshot = await observe(page); try { const target = snapshot.data.targets.find((t) => t.label === "Small $10"); assert.ok(target); assert.equal(target.role, "radio"); assert.equal(target.checked, "false"); assert.ok(!snapshot.data.targets.some((t) => t.label === "Unavailable")); await snapshot.execute( "CLICK", target, undefined, new AbortController().signal, ); assert.equal(await page.locator("#size").isChecked(), true); } finally { await snapshot.dispose(); } const updated = await observe(page); try { assert.equal( updated.data.targets.find((t) => t.label === "Small $10")?.checked, "true", ); } finally { await updated.dispose(); } await page.setContent( '', ); const covered = await observe(page); try { const target = covered.data.targets.find( (t) => t.label === "No extras" && t.role === "radio", ); assert.ok(target); await covered.execute( "CLICK", target, undefined, new AbortController().signal, ); assert.ok(await page.locator("#option").isChecked()); } finally { await covered.dispose(); } await page.setContent( '', ); const scrolledPast = await observe(page); try { assert.ok( scrolledPast.data.offscreenControls?.below.includes("Connect later"), ); assert.ok( !scrolledPast.data.targets.some((t) => t.label === "Connect later"), ); assert.ok( !scrolledPast.data.offscreenControls?.below.includes("Unavailable"), ); assert.deepEqual(scrolledPast.data.selectedOptions, [ { group: "size", label: "Small", value: "small" }, ]); } finally { await scrolledPast.dispose(); } await page.locator("#later").scrollIntoViewIfNeeded(); const scrolled = await observe(page); try { assert.equal(scrolled.data.selectedOptions?.[0].value, "small"); assert.ok(scrolled.data.targets.some((t) => t.label === "Connect later")); } finally { await scrolled.dispose(); } await page.setContent(""); const overlayed = await observe(page); try { await page.evaluate(() => { const cover = document.createElement("div"); cover.style.cssText = "position:fixed;inset:0;z-index:999"; document.body.append(cover); }); await assert.rejects( overlayed.execute( "CLICK", overlayed.data.targets.find((e) => e.label === "Search"), undefined, new AbortController().signal, ), /covered/, ); } finally { await overlayed.dispose(); } await page.setContent(""); const replaced = await observe(page); try { const target = replaced.data.targets.find((e) => e.label === "Search"); assert.ok(target); await page .locator("button") .evaluate((e) => e.replaceWith(e.cloneNode(true))); await assert.rejects( replaced.execute( "CLICK", target, undefined, new AbortController().signal, ), /disappeared/, ); } finally { await replaced.dispose(); } } finally { await browser.close(); } }); test("a target covered after the observation names what intercepted the pointer", async () => { const browser = await launchTestBrowser(); try { const page = await browser.newPage(); // Targets that cannot receive the pointer are already excluded when the page is // observed, so this covers the remaining window: the overlay appears after the // observation, which is how a cookie banner loading late behaves. Detecting it // needs no pixels, and naming it turns an unactionable stale into a next step. await page.setContent(''); const snapshot = await observe(page); try { const buy = snapshot.data.targets.find((target) => target.label === "Buy"); assert.ok(buy, "the button is observed while nothing covers it"); await page.evaluate(() => { const dialog = document.createElement("div"); dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-label", "We use cookies"); dialog.style.cssText = "position:fixed;inset:0;z-index:10"; const accept = document.createElement("button"); accept.id = "accept"; accept.textContent = "Accept cookies"; // Kept away from the observed button so the hit test lands on the overlay. accept.style.cssText = "position:absolute;bottom:8px;right:8px"; dialog.appendChild(accept); document.body.appendChild(dialog); }); await assert.rejects( snapshot.execute("CLICK", buy, undefined, new AbortController().signal), (error: unknown) => { assert.ok(error instanceof StaleObservationError); // Still classified as target_unavailable by the loop, but now actionable. assert.match(error.message, /Observed target is covered by/); assert.match(error.message, /dialog/); assert.match(error.message, /We use cookies/); return true; }, ); } finally { await snapshot.dispose(); } } finally { await browser.close(); } }); test("controls and text inside open shadow roots are observed and acted on", async () => { const browser = await launchTestBrowser(); try { const page = await browser.newPage(); await page.setContent( 'Light text
', ); await page.evaluate(() => { const host = document.querySelector("#host")!.attachShadow({ mode: "open" }); host.innerHTML = 'Shadow text
' + 'Shadow field'; // A shadow root inside a shadow root, as component libraries produce. const outer = document.querySelector("#nested")!.attachShadow({ mode: "open" }); outer.innerHTML = "Closed text
"; }); const snapshot = await observe(page); try { const labels = snapshot.data.targets.map((target) => `${target.operation}:${target.label}`); assert.ok(labels.includes("CLICK:Light button"), labels.join()); assert.ok(labels.includes("CLICK:Shadow button"), labels.join()); assert.ok(labels.includes("CLICK:Deep button"), labels.join()); // aria-labelledby resolves inside the shadow tree the input lives in. assert.ok(labels.includes("TYPE_TEXT:Shadow field"), labels.join()); assert.equal(labels.some((label) => label.includes("Closed button")), false, labels.join()); assert.match(snapshot.data.text, /Light text/); assert.match(snapshot.data.text, /Shadow text/); assert.doesNotMatch(snapshot.data.text, /Closed text/); const signal = new AbortController().signal; const shadowButton = snapshot.data.targets.find((target) => target.label === "Shadow button")!; await snapshot.execute("CLICK", shadowButton, undefined, signal); assert.equal( await page.evaluate(() => document.querySelector("#host")!.shadowRoot!.querySelector("button")!.textContent), "Shadow clicked", ); } finally { await snapshot.dispose(); } // A fresh observation is needed after the click changed the label; type and click deep. const again = await observe(page); try { const signal = new AbortController().signal; const field = again.data.targets.find((target) => target.operation === "TYPE_TEXT" && target.label === "Shadow field")!; await again.execute("TYPE_TEXT", field, "typed into shadow", signal); assert.equal( await page.evaluate(() => document.querySelector("#host")!.shadowRoot!.querySelector("input")!.value), "typed into shadow", ); } finally { await again.dispose(); } const third = await observe(page); try { const deep = third.data.targets.find((target) => target.label === "Deep button")!; // The typed value is now part of the observed state, as it is for light DOM. assert.equal(third.data.targets.find((target) => target.label === "Shadow field")?.value, "typed into shadow"); await third.execute("CLICK", deep, undefined, new AbortController().signal); assert.equal(await page.evaluate(() => (window as unknown as { __deep?: boolean }).__deep), true); } finally { await third.dispose(); } } finally { await browser.close(); } }); test("a shadow-DOM overlay covering a light-DOM target is named as the cover", async () => { const browser = await launchTestBrowser(); try { const page = await browser.newPage(); await page.setContent(''); const snapshot = await observe(page); try { const buy = snapshot.data.targets.find((target) => target.label === "Buy")!; // The banner appears after the observation, inside a shadow root. await page.evaluate(() => { const root = document.querySelector("#banner")!.attachShadow({ mode: "open" }); root.innerHTML = ''; }); await assert.rejects( snapshot.execute("CLICK", buy, undefined, new AbortController().signal), (error: unknown) => error instanceof StaleObservationError && /covered by dialog labelled "We use cookies"/.test(error.message), ); } finally { await snapshot.dispose(); } } finally { await browser.close(); } });