baa-conductor


baa-conductor / plugins / baa-firefox
codex@macbookpro  ·  2026-03-31

delivery-adapters.test.cjs

  1const assert = require("node:assert/strict");
  2const test = require("node:test");
  3
  4const {
  5  createDeliveryRuntime,
  6  getPlatformAdapter,
  7  listPlatformAdapters
  8} = require("./delivery-adapters.js");
  9
 10function createMockElement(options = {}) {
 11  const attributes = new Map(
 12    Object.entries(options.attributes || {}).map(([key, value]) => [String(key).toLowerCase(), String(value)])
 13  );
 14
 15  return {
 16    disabled: options.disabled === true,
 17    dispatchedEvents: [],
 18    focusCalls: 0,
 19    isContentEditable: options.isContentEditable === true,
 20    tagName: String(options.tagName || "DIV").toUpperCase(),
 21    textContent: options.textContent || "",
 22    value: options.value || "",
 23    click() {
 24      options.onClick?.(this);
 25    },
 26    dispatchEvent(event) {
 27      this.dispatchedEvents.push(event);
 28      return true;
 29    },
 30    focus() {
 31      this.focusCalls += 1;
 32    },
 33    getAttribute(name) {
 34      return attributes.get(String(name || "").toLowerCase()) ?? null;
 35    },
 36    getBoundingClientRect() {
 37      if (options.visible === false) {
 38        return {
 39          height: 0,
 40          width: 0
 41        };
 42      }
 43
 44      return {
 45        height: 32,
 46        width: 120
 47      };
 48    }
 49  };
 50}
 51
 52function createHarness(options = {}) {
 53  const state = {
 54    now: 0,
 55    sending: false,
 56    url: options.url || "https://gemini.google.com/app/conv-gemini-smoke"
 57  };
 58  const selectorMap = new Map();
 59  const document = {
 60    body: options.pageReady === false ? null : {},
 61    querySelectorAll(selector) {
 62      const entry = selectorMap.get(selector);
 63
 64      if (typeof entry === "function") {
 65        return entry();
 66      }
 67
 68      return entry || [];
 69    },
 70    readyState: options.pageReady === false ? "loading" : "complete"
 71  };
 72  const runtime = createDeliveryRuntime({
 73    env: {
 74      createChangeEvent() {
 75        return {
 76          type: "change"
 77        };
 78      },
 79      createInputEvent(data) {
 80        return {
 81          data,
 82          type: "input"
 83        };
 84      },
 85      document,
 86      getComputedStyle() {
 87        return {
 88          display: "block",
 89          opacity: "1",
 90          visibility: "visible"
 91        };
 92      },
 93      getLocationHref() {
 94        return state.url;
 95      },
 96      now() {
 97        return state.now;
 98      },
 99      async sleep(ms) {
100        state.now += Number(ms) || 0;
101      }
102    }
103  });
104
105  return {
106    document,
107    registerSelector(selector, elements) {
108      selectorMap.set(selector, elements);
109    },
110    runtime,
111    state
112  };
113}
114
115test("delivery adapters register Gemini with current selector fallbacks", () => {
116  const adapter = getPlatformAdapter("gemini");
117
118  assert.ok(adapter);
119  assert.equal(adapter.platform, "gemini");
120  assert.deepEqual(adapter.pageHosts, ["gemini.google.com"]);
121  assert.equal(adapter.readinessSelectors.includes("rich-textarea"), true);
122  assert.equal(adapter.composerSelectors.includes("rich-textarea .ql-editor[contenteditable='true']"), true);
123  assert.equal(adapter.sendButtonSelectors.includes("button[mattooltip*='send' i]"), true);
124  assert.equal(
125    listPlatformAdapters().some((entry) => entry?.platform === "gemini"),
126    true
127  );
128});
129
130test("Gemini inject_message works with rich-textarea Quill editor selectors", async () => {
131  const harness = createHarness();
132  const readyMarker = createMockElement({
133    tagName: "rich-textarea"
134  });
135  const composer = createMockElement({
136    isContentEditable: true,
137    tagName: "div"
138  });
139
140  harness.registerSelector("rich-textarea", [readyMarker]);
141  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
142
143  const result = await harness.runtime.handleCommand({
144    command: "inject_message",
145    platform: "gemini",
146    retryAttempts: 1,
147    text: "hello from Gemini adapter test",
148    timeoutMs: 120
149  });
150
151  assert.equal(result.ok, true);
152  assert.equal(result.details.confirmed_by, "composer_text_match");
153  assert.equal(composer.textContent, "hello from Gemini adapter test");
154  assert.deepEqual(
155    composer.dispatchedEvents.map((event) => event.type),
156    ["input", "change"]
157  );
158});
159
160test("Gemini send_message confirms via mattooltip send button and stop icon", async () => {
161  const harness = createHarness();
162  const readyMarker = createMockElement({
163    tagName: "div"
164  });
165  const stopIndicator = createMockElement({
166    tagName: "mat-icon"
167  });
168  const composer = createMockElement({
169    isContentEditable: true,
170    tagName: "div",
171    textContent: "queued gemini message"
172  });
173  const sendButton = createMockElement({
174    attributes: {
175      mattooltip: "Send message"
176    },
177    onClick() {
178      composer.textContent = "";
179      harness.state.sending = true;
180    },
181    tagName: "button"
182  });
183
184  harness.registerSelector(".conversation-container", [readyMarker]);
185  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
186  harness.registerSelector("button[mattooltip*='send' i]", [sendButton]);
187  harness.registerSelector(
188    "mat-icon[data-mat-icon-name='stop_circle']",
189    () => (harness.state.sending ? [stopIndicator] : [])
190  );
191
192  const result = await harness.runtime.handleCommand({
193    command: "send_message",
194    platform: "gemini",
195    retryAttempts: 1,
196    timeoutMs: 120
197  });
198
199  assert.equal(result.ok, true);
200  assert.equal(result.details.confirmed_by, "sending_indicator");
201  assert.equal(harness.state.sending, true);
202  assert.equal(composer.textContent, "");
203});
204
205test("Gemini adapter fails closed on page host mismatch", async () => {
206  const harness = createHarness({
207    url: "https://chatgpt.com/c/conv-host-mismatch"
208  });
209  const readyMarker = createMockElement({
210    tagName: "rich-textarea"
211  });
212  const composer = createMockElement({
213    isContentEditable: true,
214    tagName: "div"
215  });
216
217  harness.registerSelector("rich-textarea", [readyMarker]);
218  harness.registerSelector("rich-textarea .ql-editor[contenteditable='true']", [composer]);
219
220  const result = await harness.runtime.handleCommand({
221    command: "inject_message",
222    platform: "gemini",
223    retryAttempts: 1,
224    text: "should fail",
225    timeoutMs: 120
226  });
227
228  assert.equal(result.ok, false);
229  assert.equal(result.code, "page_context_mismatch");
230  assert.match(result.reason, /delivery\.page_context_mismatch/u);
231});