diff --git a/scripts/test-bidi-devtools.js b/scripts/test-bidi-devtools.js
index 18edccd..d447825 100755
--- a/scripts/test-bidi-devtools.js
+++ b/scripts/test-bidi-devtools.js
@@ -89,7 +89,13 @@ drop.addEventListener('dragover', (e)=>e.preventDefault());
+ `Upload Test
`
);
- await firefox.uploadFileBySelector('#file', filePath);
+ // The input has to remain visible: snapshots skip elements hidden via CSS.
+ const uploadSnapshot = await firefox.takeSnapshot();
+ const fileNode = uploadSnapshot.json.root.children?.find((n) => n.id === 'file');
+ if (!fileNode?.uid) {
+ throw new Error('file upload: input not found in snapshot');
+ }
+ await firefox.uploadFileByUid(fileNode.uid, filePath);
const ok = await firefox.evaluate("return document.body.getAttribute('data-ok') === '1'");
console.log(ok ? '✅ File upload worked\n' : '❌ File upload failed\n');
} catch (e) {
diff --git a/src/firefox/dom.ts b/src/firefox/dom.ts
index a03f696..8702941 100644
--- a/src/firefox/dom.ts
+++ b/src/firefox/dom.ts
@@ -2,7 +2,7 @@
* DOM interactions: evaluate, element lookup, input actions
*/
-import { By, Key, WebDriver, WebElement } from 'selenium-webdriver';
+import { Key, WebDriver, WebElement } from 'selenium-webdriver';
import type { Driver as FirefoxDriver } from 'selenium-webdriver/firefox.js';
import type { Actions } from 'selenium-webdriver/lib/input.js';
@@ -154,27 +154,6 @@ export class DomInteractions {
private resolveUid?: (uid: string) => Promise
) {}
- // ============================================================================
- // Element polling helpers
- // ============================================================================
-
- /**
- * Poll for an element matching a CSS selector until found or timeout.
- */
- private async waitForElement(selector: string, timeout = 5000): Promise {
- const deadline = Date.now() + timeout;
- let lastError: Error | undefined;
- while (Date.now() < deadline) {
- try {
- return await this.driver.findElement(By.css(selector));
- } catch (e) {
- lastError = e instanceof Error ? e : new Error(String(e));
- }
- await new Promise((r) => setTimeout(r, 100));
- }
- throw lastError ?? new Error(`Element not found: ${selector}`);
- }
-
/**
* Wait until an element reports isDisplayed(), ignoring failures.
*/
@@ -193,94 +172,6 @@ export class DomInteractions {
// Visibility wait is best-effort; don't throw
}
- // ============================================================================
- // Selector-based input methods
- // ============================================================================
-
- /**
- * Click element by CSS selector
- */
- async clickBySelector(selector: string): Promise {
- const el = await this.waitForElement(selector, 5000);
- await this.waitForVisible(el, 5000);
- await el.click();
- }
-
- /**
- * Hover over element by CSS selector
- */
- async hoverBySelector(selector: string): Promise {
- const el = await this.waitForElement(selector, 5000);
- await this.driver.actions({ async: true }).move({ origin: el }).perform();
- }
-
- /**
- * Fill input field by CSS selector
- */
- async fillBySelector(selector: string, text: string): Promise {
- const el = await this.waitForElement(selector, 5000);
- try {
- await el.clear();
- } catch {
- // Some inputs may not support clear(); fall back to select-all + delete
- await el.sendKeys(Key.chord(Key.CONTROL, 'a'), Key.DELETE);
- }
- await el.sendKeys(text);
- }
-
- /**
- * Drag & drop using JS events fallback (DataTransfer).
- * Works on simple pages; not guaranteed for all custom DnD libs.
- */
- async dragAndDropBySelectors(sourceSelector: string, targetSelector: string): Promise {
- await this.driver.executeScript(
- `
- var srcSel = arguments[0], tgtSel = arguments[1];
- var src = document.querySelector(srcSel);
- var tgt = document.querySelector(tgtSel);
- if (!src || !tgt) throw new Error('dragAndDrop: element not found');
- function dispatch(type, target, dt) {
- var evt = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt });
- return target.dispatchEvent(evt);
- }
- var dt = typeof DataTransfer !== 'undefined' ? new DataTransfer() : undefined;
- dispatch('dragstart', src, dt);
- dispatch('dragenter', tgt, dt);
- dispatch('dragover', tgt, dt);
- dispatch('drop', tgt, dt);
- dispatch('dragend', src, dt);
- `,
- sourceSelector,
- targetSelector
- );
- }
-
- /**
- * File upload: unhide if needed, then send local path to .
- */
- async uploadFileBySelector(selector: string, filePath: string): Promise {
- const el = await this.waitForElement(selector, 5000);
- // Ensure it's an ; if hidden, unhide via JS
- await this.driver.executeScript(
- `
- var sel = arguments[0];
- var e = document.querySelector(sel);
- if (!e) throw new Error('uploadFile: element not found');
- if (e.tagName !== 'INPUT' || e.type !== 'file')
- throw new Error('uploadFile: selector must target ');
- var style = window.getComputedStyle(e);
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
- var s = e.style;
- s.display = 'block'; s.visibility = 'visible'; s.opacity = '1';
- s.position = 'fixed'; s.left = '0px'; s.top = '0px';
- s.zIndex = '2147483647';
- }
- `,
- selector
- );
- await el.sendKeys(filePath);
- }
-
// ============================================================================
// UID-based input methods
// ============================================================================
diff --git a/src/firefox/index.ts b/src/firefox/index.ts
index 161e54d..1ca6227 100644
--- a/src/firefox/index.ts
+++ b/src/firefox/index.ts
@@ -139,41 +139,6 @@ export class FirefoxClient {
);
}
- async clickBySelector(selector: string): Promise {
- if (!this.dom) {
- throw new Error('Not connected');
- }
- return await this.dom.clickBySelector(selector);
- }
-
- async hoverBySelector(selector: string): Promise {
- if (!this.dom) {
- throw new Error('Not connected');
- }
- return await this.dom.hoverBySelector(selector);
- }
-
- async fillBySelector(selector: string, text: string): Promise {
- if (!this.dom) {
- throw new Error('Not connected');
- }
- return await this.dom.fillBySelector(selector, text);
- }
-
- async dragAndDropBySelectors(sourceSelector: string, targetSelector: string): Promise {
- if (!this.dom) {
- throw new Error('Not connected');
- }
- return await this.dom.dragAndDropBySelectors(sourceSelector, targetSelector);
- }
-
- async uploadFileBySelector(selector: string, filePath: string): Promise {
- if (!this.dom) {
- throw new Error('Not connected');
- }
- return await this.dom.uploadFileBySelector(selector, filePath);
- }
-
// UID-based input methods
async clickByUid(uid: string, dblClick = false): Promise {
`
);
- await firefox.dragAndDropBySelectors('#drag', '#drop');
+ const dndSnapshot = await firefox.takeSnapshot();
+ const dragNode = dndSnapshot.json.root.children?.find((n) => n.id === 'drag');
+ const dropNode = dndSnapshot.json.root.children?.find((n) => n.id === 'drop');
+ if (!dragNode?.uid || !dropNode?.uid) {
+ throw new Error('drag & drop: elements not found in snapshot');
+ }
+ await firefox.dragByUidToUid(dragNode.uid, dropNode.uid);
const ok = await firefox.evaluate("return !!document.querySelector('#ok')");
console.log(ok ? '✅ Drag & drop worked\n' : '❌ Drag & drop failed\n');
} catch (e) {
@@ -108,13 +114,19 @@ drop.addEventListener('dragover', (e)=>e.preventDefault());
await loadHTML(
firefox,
- `
Upload Test