feat(vom): support canvas reading

This commit is contained in:
Ljy-0827
2026-08-31 14:44:24 +08:00
parent f5dab955e2
commit e069abaf2e
9 changed files with 587 additions and 25 deletions
@@ -93,6 +93,29 @@ Run with a `900x700` Agent Window.
- `fixture-below-fold` is absent initially.
- After scrolling it into view and observing again, `fixture-below-fold` must be represented with a fresh ref.
### `input-frames.html`
This page exercises generic frame-aware input routing. It contains a top-level
input plus same-origin, cross-origin (OOPIF), and nested frame inputs. Each
frame includes an input, textarea, contenteditable editor, and a controlled
input that resets its value on the next animation frame.
- `fill` must route focus, text insertion, DOM events, and readback through the
ref's owning CDP target.
- `press --ref` must focus and dispatch every key event through that same target.
- Untargeted `press` must follow the actual focused target or refuse when focus
ownership cannot be determined uniquely.
- The rejecting input must return `input_not_applied`, never a successful
requested-value length.
### `canvas-input.html`
This is the PR3 end-to-end fixture. Its cross-origin frame contains a
visual-only three-row Canvas. A screenshot-bound click on row 3 exposes and
focuses a standard textbox; ordinary `fill` must then write and read back its
real value through the textbox's owning target. The fixture deliberately does
not provide a Canvas-specific fill action.
### `dynamic.html`
Use the normal DOM buttons and re-observe after each action:
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas to standard input fixture</title>
<style>
body { padding: 16px; font: 14px system-ui, sans-serif; }
iframe { width: 300px; height: 220px; border: 2px solid #5d6d7e; }
</style>
</head>
<body>
<h1>Canvas to standard input fixture</h1>
<p>Select row 3 in the visual-only Canvas, then fill the exposed standard textbox.</p>
<iframe
title="cross-origin Canvas editor"
src="http://localhost:4173/frame.html?label=canvas-input-grid&interactive=1&editor=1"
></iframe>
</body>
</html>
@@ -19,6 +19,18 @@
if (params.get("canvas") === "0") canvas.remove();
else {
canvas.setAttribute("aria-label", params.get("label") || "fixture-frame-default");
if (params.get("editor") === "1") {
const context = canvas.getContext("2d");
context.font = "14px system-ui, sans-serif";
context.textBaseline = "middle";
context.strokeStyle = "#1e8449";
context.fillStyle = "#145a32";
for (let row = 0; row < 3; row += 1) {
const top = row * 30;
context.strokeRect(0.5, top + 0.5, 219, 29);
context.fillText(`Row ${row + 1}`, 12, top + 15);
}
}
if (params.get("interactive") === "1") {
const status = document.createElement("p");
status.id = "pointer-status";
@@ -39,6 +51,22 @@
canvas.addEventListener("click", () => {
status.textContent += "; click received";
});
if (params.get("editor") === "1") {
canvas.addEventListener("click", (event) => {
const rect = canvas.getBoundingClientRect();
const canvasY = ((event.clientY - rect.top) / rect.height) * canvas.height;
const row = Math.min(2, Math.max(0, Math.floor(canvasY / 30)));
status.textContent += `; selected row=${row + 1}`;
document.querySelector("#canvas-cell-editor")?.remove();
if (row !== 2) return;
const editor = document.createElement("input");
editor.id = "canvas-cell-editor";
editor.setAttribute("aria-label", "selected canvas cell editor");
editor.placeholder = "Type into selected row 3";
canvas.insertAdjacentElement("afterend", editor);
editor.focus();
});
}
}
}
if (params.get("nested") === "1") {
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { margin: 0; padding: 12px; font: 14px system-ui, sans-serif; }
label, textarea, input, [contenteditable] { display: block; margin: 8px 0; }
input, textarea, [contenteditable] { width: 240px; padding: 6px; border: 1px solid #85929e; }
iframe { width: 280px; height: 190px; border: 2px solid #85929e; }
</style>
</head>
<body>
<label>Frame input <input aria-label="frame input" /></label>
<label>Frame textarea <textarea aria-label="frame textarea"></textarea></label>
<div contenteditable="true" aria-label="frame editor"></div>
<label>Rejecting input <input id="rejecting" aria-label="rejecting input" /></label>
<script>
const params = new URLSearchParams(location.search);
const rejecting = document.querySelector("#rejecting");
rejecting.addEventListener("input", () => requestAnimationFrame(() => { rejecting.value = ""; }));
if (params.get("nested") === "1") {
const iframe = document.createElement("iframe");
iframe.title = "nested input frame";
iframe.src = "input-frame.html";
document.body.append(iframe);
}
</script>
</body>
</html>
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Frame input routing matrix</title>
<style>
body { padding: 16px; font: 14px system-ui, sans-serif; }
.frames { display: grid; grid-template-columns: repeat(3, 320px); gap: 16px; }
iframe { width: 300px; height: 430px; border: 2px solid #5d6d7e; }
input { padding: 6px; }
</style>
</head>
<body>
<h1>Frame input routing matrix</h1>
<label>Top input <input aria-label="top input" /></label>
<div class="frames">
<iframe title="same-origin inputs" src="input-frame.html"></iframe>
<iframe title="cross-origin inputs" src="http://localhost:4173/input-frame.html"></iframe>
<iframe title="nested inputs" src="input-frame.html?nested=1"></iframe>
</div>
</body>
</html>