|
9 | 9 | planBrowserAdaptiveStateActions, |
10 | 10 | } from "../packages/runtime-core/src/browser-adaptive-exploration.js" |
11 | 11 | import { getCommandDefinition } from "../packages/runtime-core/src/command-registry.js" |
| 12 | +import { discoverBrowserActionCorpusDescriptors } from "../packages/runtime-playground/src/browser-action-discovery.js" |
12 | 13 | import { exploreAdaptiveBrowserStateMachine } from "../packages/runtime-playground/src/browser-adaptive-explorer.js" |
| 14 | +import { executeBrowserInteractionStep } from "../packages/runtime-playground/src/browser-interactions.js" |
13 | 15 | import { browserPreviewTopology, routeBrowserPreviewContextNetwork } from "../packages/runtime-playground/src/browser-preview-routing.js" |
14 | 16 | import { closeHttpServer, listenLocalHttpServer } from "../packages/runtime-playground/src/preview-server.js" |
15 | 17 |
|
@@ -111,6 +113,84 @@ test("adaptive contract normalizes every safety and exploration bound", () => { |
111 | 113 | assert.deepEqual(new Set(planned.map((action) => action.family)), new Set(["click", "fill", "select", "submit", "keyboard", "back", "reload", "repeat", "double-submit"])) |
112 | 114 | }) |
113 | 115 |
|
| 116 | +test("adaptive planner selects actions by HTML input type capability", () => { |
| 117 | + const fillableTypes = ["text", "search", "tel", "url", "email", "password", "number"] |
| 118 | + const toggleTypes = ["checkbox", "radio"] |
| 119 | + const buttonTypes = ["button", "reset"] |
| 120 | + const submitTypes = ["submit", "image"] |
| 121 | + const unsupportedTypes = ["color", "date", "datetime-local", "file", "hidden", "month", "range", "time", "week"] |
| 122 | + const descriptors = [ |
| 123 | + { id: "default", kind: "input" as const, selector: "#default", formId: "form", frameId: "document" }, |
| 124 | + ...[...fillableTypes, ...toggleTypes, ...buttonTypes, ...submitTypes, ...unsupportedTypes].map((type) => ({ id: type, kind: "input" as const, selector: `#${type}`, type, formId: "form", frameId: "document" })), |
| 125 | + { id: "textarea", kind: "textarea" as const, selector: "#textarea", formId: "form", frameId: "document" }, |
| 126 | + { id: "disabled", kind: "input" as const, selector: "#disabled", type: "text", disabled: true, frameId: "document" }, |
| 127 | + { id: "readonly", kind: "input" as const, selector: "#readonly", type: "text", readonly: true, frameId: "document" }, |
| 128 | + ] |
| 129 | + const contract = browserAdaptiveExplorationContract({ seed: "input-capabilities", startUrl: "/fixture" }) |
| 130 | + const planned = planBrowserAdaptiveStateActions({ |
| 131 | + digest: "state", |
| 132 | + url: "/fixture", |
| 133 | + historyLength: 1, |
| 134 | + historyStateDigest: "history", |
| 135 | + descriptorDigest: "descriptors", |
| 136 | + descriptors, |
| 137 | + frames: [{ id: "document", url: "/fixture", scope: "document" }], |
| 138 | + visits: 1, |
| 139 | + depth: 0, |
| 140 | + loadingIndicators: 0, |
| 141 | + }, contract) |
| 142 | + const familiesFor = (id: string) => planned.filter((action) => action.descriptorId === id).map((action) => action.family) |
| 143 | + |
| 144 | + for (const type of ["default", ...fillableTypes, "textarea"]) assert.deepEqual(familiesFor(type), ["fill", "keyboard", "submit", "repeat"], type) |
| 145 | + for (const type of toggleTypes) assert.deepEqual(familiesFor(type), ["click", "repeat"], type) |
| 146 | + for (const type of buttonTypes) assert.deepEqual(familiesFor(type), ["click", "repeat"], type) |
| 147 | + for (const type of submitTypes) assert.deepEqual(familiesFor(type), ["click", "repeat", "submit", "double-submit"], type) |
| 148 | + for (const type of [...unsupportedTypes, "disabled", "readonly"]) assert.deepEqual(familiesFor(type), [], type) |
| 149 | + assert(planned.filter((action) => action.steps.some((step) => step.kind === "fill")).every((action) => ["default", ...fillableTypes, "textarea"].includes(action.descriptorId ?? ""))) |
| 150 | +}) |
| 151 | + |
| 152 | +test("planned input actions execute safely in a real browser", async () => { |
| 153 | + const browser = await chromium.launch({ headless: true }) |
| 154 | + const page = await browser.newPage() |
| 155 | + const fixture = `<!doctype html> |
| 156 | + <style>input,textarea { display:block; width:180px; height:30px; margin:4px; }</style> |
| 157 | + <form id="form"> |
| 158 | + <input id="checkbox" type="checkbox"><input id="radio" type="radio" name="choice"> |
| 159 | + <input id="text" type="text"><input id="password" type="password"><input id="email" type="email"><input id="search" type="search"> |
| 160 | + <textarea id="textarea"></textarea><input id="submit" type="submit" value="Submit"><input id="range" type="range"><input id="file" type="file"> |
| 161 | + </form> |
| 162 | + <script>document.querySelector('#form').addEventListener('submit', (event) => event.preventDefault())</script>` |
| 163 | + try { |
| 164 | + await page.addInitScript("globalThis.__name = value => value") |
| 165 | + await page.setContent(fixture) |
| 166 | + await page.evaluate("globalThis.__name = value => value") |
| 167 | + const discovery = await discoverBrowserActionCorpusDescriptors(page) |
| 168 | + const contract = browserAdaptiveExplorationContract({ seed: "browser-input-capabilities", startUrl: page.url() }) |
| 169 | + const actions = planBrowserAdaptiveStateActions({ |
| 170 | + digest: "state", |
| 171 | + url: page.url(), |
| 172 | + historyLength: 1, |
| 173 | + historyStateDigest: "history", |
| 174 | + descriptorDigest: "descriptors", |
| 175 | + descriptors: discovery.descriptors, |
| 176 | + frames: [{ id: "document", url: page.url(), scope: "document" }], |
| 177 | + visits: 1, |
| 178 | + depth: 0, |
| 179 | + loadingIndicators: 0, |
| 180 | + }, contract).filter((action) => action.descriptorId) |
| 181 | + |
| 182 | + assert(actions.some((action) => action.descriptorId?.includes("\"type\":\"checkbox\"") && action.steps.every((step) => step.kind === "click"))) |
| 183 | + assert(actions.some((action) => action.descriptorId?.includes("\"type\":\"radio\"") && action.steps.every((step) => step.kind === "click"))) |
| 184 | + assert(!actions.some((action) => action.descriptorId?.includes("\"type\":\"range\"") || action.descriptorId?.includes("\"type\":\"file\""))) |
| 185 | + for (const action of actions) { |
| 186 | + await page.setContent(fixture) |
| 187 | + for (const step of action.steps) await executeBrowserInteractionStep(page, step, page.url(), 2_000, async () => ({ path: "unused", isDefault: false })) |
| 188 | + } |
| 189 | + } finally { |
| 190 | + await browser.close() |
| 191 | + } |
| 192 | +}) |
| 193 | + |
114 | 194 | test("rediscovery finds, minimizes, and replays a defect revealed by a dynamic modal", async () => { |
115 | 195 | const run = await runFixture(modalFixture, { |
116 | 196 | seed: "modal-seed", |
|
0 commit comments