all repos — sz-chrome-remote-cdp @ master

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

test/manual/forms-test.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
import { createDaemonTransport } from "../../src/daemon/transport";
import { createBrowserClient } from "../../src/client";
import { fillTool, selectOptionTool, setCheckedTool } from "../../src/domains/form";
import { fillFormTool } from "../../src/domains/form-batch";

let passed = 0;
let failed = 0;
const check = (cond: boolean, label: string): void => {
  if (cond) { passed++; console.log(`  ✓ ${label}`); }
  else { failed++; console.error(`  ✗ ${label}`); }
};

const mkCtx = (client: any): any => ({
  client,
  signal: undefined,
  onUpdate: () => {},
  extensionCtx: { cwd: process.cwd() },
});

const FIXTURE = `
<!doctype html><html><body>
  <h1>Form fixture</h1>
  <input id="plain" aria-label="plain" />
  <textarea id="ta" aria-label="ta"></textarea>
  <input id="controlled" aria-label="controlled" />
  <span id="model"></span>
  <select id="sel" aria-label="country">
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
    <option value="ca">Canada</option>
  </select>
  <input type="checkbox" id="chk" aria-label="agree" />
  <div id="editable" role="textbox" aria-label="bio" contenteditable="true"></div>
  <script>
    // Controlled input: its "model" updates ONLY when an input event fires.
    // A naive value-set that skips the input event leaves the model stale.
    var model = "";
    var c = document.getElementById("controlled");
    c.addEventListener("input", function () { model = c.value; document.getElementById("model").textContent = model; });
    window.__model = function () { return model; };
  </script>
</body></html>`;

const publishRefs = async (client: any, names: ReadonlyArray<string>): Promise<Record<string, string | undefined>> => {
  const ax = await client.session().call("Accessibility.getFullAXTree", {});
  if (!ax.success) return {};
  const byName = new Map<string, number>();
  for (const n of (ax.data as any).nodes as any[]) {
    if (typeof n?.name?.value === "string" && typeof n.backendDOMNodeId === "number" && !byName.has(n.name.value)) {
      byName.set(n.name.value, n.backendDOMNodeId);
    }
  }
  const refMap = new Map<string, number>();
  const refSig = new Map<string, string>();
  const out: Record<string, string | undefined> = {};
  names.forEach((name, i) => {
    const backendId = byName.get(name);
    if (backendId === undefined) return;
    const ref = `e${i + 1}`;
    refMap.set(ref, backendId);
    refSig.set(ref, `|${name}||`);
    out[name] = ref;
  });
  client.session().setRefMap(refMap, refSig);
  return out;
};

async function main(): Promise<void> {
  const transport = createDaemonTransport("pi-forms-test");
  const client = createBrowserClient({ namespace: "pi-forms-test", transport });
  const ctx = mkCtx(client);

  const started = await client.start();
  if (!started.success) { console.error("Could not start client:", started.error.message); process.exit(1); }

  const url = "data:text/html," + encodeURIComponent(FIXTURE);
  const tab = await client.newTab(url);
  check(tab.success, `Opened fixture tab: ${tab.success ? "ok" : tab.error.message}`);
  await new Promise((r) => setTimeout(r, 300));

  const byName = await publishRefs(client, ["plain", "ta", "controlled", "country", "agree", "bio"]);
  const refs = {
    plain: byName["plain"],
    ta: byName["ta"],
    controlled: byName["controlled"],
    sel: byName["country"],
    chk: byName["agree"],
    editable: byName["bio"],
  };
  for (const [k, v] of Object.entries(refs)) check(typeof v === "string", `ref present for ${k} (${v})`);

  const fill = await fillFormTool.handler({
    fields: [
      { ref: refs.plain!, value: "hello plain" },
      { ref: refs.ta!, value: "multi\nline" },
      { ref: refs.controlled!, value: "ctrl-value" },
      { ref: refs.editable!, value: "my bio" },
    ],
  }, ctx);
  check(fill.success, "browser_fill_form succeeded");
  if (fill.success) console.log("    " + fill.data.text);

  const sel = await selectOptionTool.handler({ ref: refs.sel!, label: "Canada" }, ctx);
  check(sel.success, `browser_select_option by label: ${sel.success ? sel.data.text : (sel as any).error.message}`);
  const chk = await setCheckedTool.handler({ ref: refs.chk!, checked: true }, ctx);
  check(chk.success, `browser_set_checked: ${chk.success ? chk.data.text : (chk as any).error.message}`);

  const read = async (expr: string): Promise<unknown> => {
    const r = await client.evaluateJs(expr);
    return r.success ? r.data : `ERR:${r.error.message}`;
  };
  check((await read("document.getElementById('plain').value")) === "hello plain", "plain input value committed");
  check((await read("document.getElementById('ta').value")) === "multi\nline", "textarea value committed");
  check((await read("window.__model()")) === "ctrl-value", "controlled input model committed (input event fired)");
  check((await read("document.getElementById('editable').textContent")) === "my bio", "contenteditable text committed");
  check((await read("document.getElementById('sel').value")) === "ca", "select value committed");
  check((await read("document.getElementById('chk').checked")) === true, "checkbox checked committed");

  const badSel = await selectOptionTool.handler({ ref: refs.sel!, label: "Atlantis" }, ctx);
  check(!badSel.success && (badSel as any).error.kind === "invalid_state", "select with no match → invalid_state error");

  const staleRef = refs.plain!;
  const nav = await client.session().call("Page.navigate", { url: "data:text/html," + encodeURIComponent("<body>gone</body>") });
  check(nav.success, "navigated away to invalidate refs");
  await new Promise((r) => setTimeout(r, 300));
  const stale = await fillTool.handler({ ref: staleRef, value: "x" }, ctx);
  check(!stale.success && (stale as any).error.kind === "invalid_state", "fill on stale ref → invalid_state error");

  await client.closeTab(tab.success ? (tab.data as string) : "");
  await client.stop();

  console.log(`\n${passed} passed, ${failed} failed`);
  process.exit(failed === 0 ? 0 : 1);
}

main().catch((e) => { console.error(e); process.exit(1); });