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/domains/form-errors-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
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { createFakeClient, type FakeClient } from "./fake-client";
import { fillTool, focusTool, selectOptionTool } from "../../src/domains/form";
import type { HandlerContext } from "../../src/util/tool";
import { ok } from "../../src/util/result";

const RESOLVED_NODE = ok({ object: { type: "object", objectId: "obj-1" } });

const THREW = ok({
  result: { type: "undefined" },
  exceptionDetails: { text: "Uncaught TypeError: el.dispatchEvent is not a function" },
});

const returned = (value: unknown) => ok({ result: { type: "object", value } });

const ctxFor = (fake: FakeClient): HandlerContext => ({
  client: fake.client,
  signal: undefined,
  onUpdate: () => {},
  extensionCtx: undefined as never,
});

const withRef = async (callResult: ReturnType<typeof ok>): Promise<FakeClient> =>
  createFakeClient({
    refs: [{ ref: "e1", backendId: 101 }],
    canned: { "DOM.resolveNode": RESOLVED_NODE, "Runtime.callFunctionOn": callResult },
  });

describe("a page-side throw is reported as a throw, not as a missing element", () => {
  test("browser_fill surfaces the page exception", async () => {
    const fake = await withRef(THREW);
    const r = await fillTool.handler({ ref: "e1", value: "x" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.equal(r.error.kind, "cdp_error");
    assert.doesNotMatch(r.error.message, /No element matched/);
    assert.match(r.error.message, /dispatchEvent is not a function/);
  });

  test("browser_focus surfaces the page exception", async () => {
    const fake = await withRef(THREW);
    const r = await focusTool.handler({ ref: "e1" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.equal(r.error.kind, "cdp_error");
    assert.doesNotMatch(r.error.message, /No element matched/);
  });

  test("browser_select_option surfaces the page exception", async () => {
    const fake = await withRef(THREW);
    const r = await selectOptionTool.handler({ ref: "e1", label: "India" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.equal(r.error.kind, "cdp_error");
    assert.doesNotMatch(r.error.message, /No element matched/);
  });
});

describe("a genuinely missing element still reports no match", () => {
  test("the selector path reports no match when querySelector finds nothing", async () => {
    const fake = await createFakeClient({
      evaluate: () => ok({ ok: false, reason: "not_found" }),
    });
    const r = await fillTool.handler({ selector: "#nope", value: "x" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.equal(r.error.kind, "invalid_state");
    assert.match(r.error.message, /No element matched: #nope/);
  });

  test("a stale ref still reports a stale ref", async () => {
    const fake = await createFakeClient({ refs: [{ ref: "e1", backendId: 101 }] });
    const r = await fillTool.handler({ ref: "e9", value: "x" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.match(r.error.message, /unknown or stale/);
  });
});

describe("a well-formed page result still succeeds", () => {
  test("browser_fill reports the value the page read back", async () => {
    const fake = await withRef(returned({ ok: true, kind: "text", value: "typed", tag: "INPUT" }));
    const r = await fillTool.handler({ ref: "e1", value: "typed" }, ctxFor(fake));
    assert.equal(r.success, true);
    if (!r.success) return;
    assert.match(r.data.text, /Filled e1 = "typed"/);
  });

  test("browser_fill refuses a disabled field with the page's reason", async () => {
    const fake = await withRef(returned({ ok: false, reason: "element is disabled", tag: "INPUT" }));
    const r = await fillTool.handler({ ref: "e1", value: "typed" }, ctxFor(fake));
    assert.equal(r.success, false);
    if (r.success) return;
    assert.match(r.error.message, /element is disabled/);
  });
});