test(browser): drive a canvas-under-shadow-root fixture end to end

This commit is contained in:
pj committed 2026-08-13 00:42:02 +05:30
1 parent 61be8c0788
commit 596f0e0766
3 files changed
+72

No files matched your search

+12
View File
@@ -53,6 +53,18 @@ func TestBrowserCounterInvariantHolds(t *testing.T) {
} }
} }
// TestBrowserShadowDOMIsReachable drives a page whose entire UI (canvas, the
// button over it, and the counter) lives inside a shadow root, the shape
// Compose for Web produces. Both the enumeration and the selector lookup have
// to cross the boundary for the counter to move at all, so the property firing
// is the end-to-end evidence.
func TestBrowserShadowDOMIsReachable(t *testing.T) {
violations := runFixture(t, "shadow")
if !slices.Contains(violations, "counterNeverMoves") {
t.Fatalf("nothing inside the shadow root was ever tapped; violations=%v", violations)
}
}
// runFixture serves the named testdata case over an in-process file server, // runFixture serves the named testdata case over an in-process file server,
// drives it through headless Chrome with a fixed seed and a bounded step count, // drives it through headless Chrome with a fixed seed and a bounded step count,
// and returns every property name that was ever reported violated. // and returns every property name that was ever reported violated.
+43
View File
@@ -0,0 +1,43 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>shadow</title>
</head>
<body>
<!-- Shaped like Compose for Web: the app mounts a shadow root holding a
canvas plus an accessibility overlay. The overlay is pointer-events:
none, so a tap aimed at the button lands on the canvas, which routes it
by coordinates the way a canvas app does. -->
<div id="app"></div>
<script>
const root = document.getElementById("app").attachShadow({ mode: "open" });
root.innerHTML = `
<style>
#surface { position: absolute; left: 0; top: 0; }
#a11y { position: absolute; left: 0; top: 0; pointer-events: none; }
#bump { margin: 20px; }
</style>
<canvas id="surface" width="360" height="240"></canvas>
<div id="a11y">
<button id="bump">bump</button>
<div id="count">0</div>
</div>`;
const surface = root.getElementById("surface");
const bump = root.getElementById("bump");
const count = root.getElementById("count");
let value = 0;
surface.addEventListener("click", function (event) {
const rect = bump.getBoundingClientRect();
const hit =
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
if (!hit) return;
value += 1;
count.textContent = String(value);
});
</script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
import { always, extract, taps } from "@sanderling/spec";
const bumps = extract((s) => {
const el = s.ax.find({ id: "count" });
return el ? parseInt(el.text, 10) || 0 : 0;
}).named("bumps");
// The button, the counter that records the taps, and the canvas that services
// them all live inside a shadow root. A host that stops at the shadow boundary
// enumerates no target to tap and resolves no selector to read, so the counter
// can never leave zero: this property firing IS the evidence that both the
// enumeration and the selector lookup crossed the boundary.
const counterNeverMoves = always(() => bumps.current === 0);
export const properties = { counterNeverMoves };
export const actionsRoot = taps;