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});