mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
Compare commits
3 Commits
fix/apr_29
...
fix/mar17-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c3094ad | ||
|
|
543af16b38 | ||
|
|
1eb96eb081 |
@@ -763,8 +763,19 @@ export class Browser {
|
||||
): Promise<void> {
|
||||
const session = await this.resolveSession(page)
|
||||
await mouse.dispatchClick(session, x, y, 'left', 1, 0)
|
||||
if (clear) await keyboard.clearField(session)
|
||||
await keyboard.typeText(session, text)
|
||||
if (clear) {
|
||||
await keyboard.clearField(session)
|
||||
let value = await elements.getFocusedEditableValue(session)
|
||||
if (value) {
|
||||
await mouse.dispatchClick(session, x, y, 'left', 3, 0)
|
||||
await keyboard.pressCombo(session, 'Backspace')
|
||||
value = await elements.getFocusedEditableValue(session)
|
||||
}
|
||||
if (value) {
|
||||
await elements.clearFocusedEditableElement(session)
|
||||
}
|
||||
}
|
||||
if (text) await keyboard.typeText(session, text)
|
||||
}
|
||||
|
||||
async dragAt(
|
||||
@@ -815,27 +826,22 @@ export class Browser {
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
// Primary: keyboard select-all + backspace
|
||||
await keyboard.clearField(session)
|
||||
|
||||
// Fallback: if field still has content, triple-click to select all
|
||||
// then typeText will overwrite the selection
|
||||
if (coords) {
|
||||
const value = await elements.getInputValue(session, element)
|
||||
const existingValue = await elements.getInputValue(session, element)
|
||||
if (existingValue) {
|
||||
await keyboard.clearField(session)
|
||||
let value = await elements.getInputValue(session, element)
|
||||
if (value && coords) {
|
||||
await mouse.dispatchClick(session, coords.x, coords.y, 'left', 3, 0)
|
||||
await keyboard.pressCombo(session, 'Backspace')
|
||||
value = await elements.getInputValue(session, element)
|
||||
}
|
||||
if (value) {
|
||||
await mouse.dispatchClick(
|
||||
session,
|
||||
coords.x,
|
||||
coords.y,
|
||||
'left',
|
||||
3,
|
||||
0,
|
||||
)
|
||||
await elements.clearEditableElement(session, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await keyboard.typeText(session, text)
|
||||
if (text) await keyboard.typeText(session, text)
|
||||
return coords
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
import type { ProtocolApi } from '@browseros/cdp-protocol/protocol-api'
|
||||
|
||||
const CLEAR_EDITABLE_TARGET_BODY = `
|
||||
if (!(target instanceof Element)) return false;
|
||||
if (typeof target.focus === 'function') target.focus();
|
||||
const inputType =
|
||||
target instanceof HTMLInputElement ? target.type.toLowerCase() : '';
|
||||
const canClearInput =
|
||||
target instanceof HTMLInputElement &&
|
||||
![
|
||||
'button',
|
||||
'checkbox',
|
||||
'color',
|
||||
'file',
|
||||
'hidden',
|
||||
'image',
|
||||
'radio',
|
||||
'range',
|
||||
'reset',
|
||||
'submit',
|
||||
].includes(inputType);
|
||||
if (target instanceof HTMLTextAreaElement || canClearInput) {
|
||||
if (target.disabled || target.readOnly) return false;
|
||||
const prototype =
|
||||
target instanceof HTMLTextAreaElement
|
||||
? HTMLTextAreaElement.prototype
|
||||
: HTMLInputElement.prototype;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
||||
if (descriptor?.set) {
|
||||
descriptor.set.call(target, '');
|
||||
} else {
|
||||
target.value = '';
|
||||
}
|
||||
if (typeof target.setSelectionRange === 'function') {
|
||||
try {
|
||||
target.setSelectionRange(0, 0);
|
||||
} catch {}
|
||||
}
|
||||
} else if (target instanceof HTMLElement && target.isContentEditable) {
|
||||
target.replaceChildren();
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
target.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
return true;
|
||||
`
|
||||
|
||||
function quadCenter(q: number[]): { x: number; y: number } {
|
||||
const x = ((q[0] ?? 0) + (q[2] ?? 0) + (q[4] ?? 0) + (q[6] ?? 0)) / 4
|
||||
const y = ((q[1] ?? 0) + (q[3] ?? 0) + (q[5] ?? 0) + (q[7] ?? 0)) / 4
|
||||
@@ -111,6 +164,69 @@ export async function getInputValue(
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearEditableElement(
|
||||
session: ProtocolApi,
|
||||
backendNodeId: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const cleared = await callOnElement(
|
||||
session,
|
||||
backendNodeId,
|
||||
`function(){const target=this;${CLEAR_EDITABLE_TARGET_BODY}}`,
|
||||
)
|
||||
return Boolean(cleared)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFocusedEditableElement(
|
||||
session: ProtocolApi,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await session.Runtime.evaluate({
|
||||
expression: `(() => {
|
||||
let target = document.activeElement;
|
||||
while (
|
||||
target instanceof HTMLElement &&
|
||||
target.shadowRoot?.activeElement instanceof Element
|
||||
) {
|
||||
target = target.shadowRoot.activeElement;
|
||||
}
|
||||
${CLEAR_EDITABLE_TARGET_BODY}
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
})
|
||||
return Boolean(result.result?.value)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFocusedEditableValue(
|
||||
session: ProtocolApi,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const result = await session.Runtime.evaluate({
|
||||
expression: `(() => {
|
||||
let target = document.activeElement;
|
||||
while (
|
||||
target instanceof HTMLElement &&
|
||||
target.shadowRoot?.activeElement instanceof Element
|
||||
) {
|
||||
target = target.shadowRoot.activeElement;
|
||||
}
|
||||
if (!(target instanceof Element)) return '';
|
||||
return target.value ?? target.textContent ?? '';
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
})
|
||||
return (result.result?.value as string) ?? ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function callOnElement(
|
||||
session: ProtocolApi,
|
||||
backendNodeId: number,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { platform } from 'node:os'
|
||||
import type { ProtocolApi } from '@browseros/cdp-protocol/protocol-api'
|
||||
|
||||
// Meta (Cmd) on macOS, Control on everything else
|
||||
const PLATFORM_MODIFIER = platform() === 'darwin' ? 4 : 2
|
||||
|
||||
type KeyInfo = { code: string; keyCode: number | undefined }
|
||||
@@ -184,7 +183,6 @@ export async function typeText(
|
||||
}
|
||||
|
||||
export async function clearField(session: ProtocolApi): Promise<void> {
|
||||
// Select all: Cmd+A on macOS, Ctrl+A on others
|
||||
await session.Input.dispatchKeyEvent({
|
||||
type: 'keyDown',
|
||||
key: 'a',
|
||||
@@ -199,7 +197,6 @@ export async function clearField(session: ProtocolApi): Promise<void> {
|
||||
modifiers: PLATFORM_MODIFIER,
|
||||
windowsVirtualKeyCode: 65,
|
||||
})
|
||||
// Backspace to delete selection (more reliable cross-platform than Delete)
|
||||
await session.Input.dispatchKeyEvent({
|
||||
type: 'keyDown',
|
||||
key: 'Backspace',
|
||||
|
||||
@@ -2,12 +2,14 @@ import { describe, it } from 'bun:test'
|
||||
import assert from 'node:assert'
|
||||
import {
|
||||
check,
|
||||
clear,
|
||||
click,
|
||||
fill,
|
||||
hover,
|
||||
press_key,
|
||||
scroll,
|
||||
select_option,
|
||||
type_at,
|
||||
uncheck,
|
||||
} from '../../src/tools/input'
|
||||
import { close_page, new_page } from '../../src/tools/navigation'
|
||||
@@ -117,6 +119,100 @@ describe('input tools', () => {
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
it('fill replaces existing text by default', async () => {
|
||||
await withBrowser(async ({ execute }) => {
|
||||
const newResult = await execute(new_page, { url: FORM_PAGE })
|
||||
const pageId = pageIdOf(newResult)
|
||||
|
||||
const snap = await execute(take_snapshot, { page: pageId })
|
||||
const inputId = findElementId(textOf(snap), 'Enter name')
|
||||
|
||||
await execute(fill, { page: pageId, element: inputId, text: 'Alpha' })
|
||||
const fillResult = await execute(fill, {
|
||||
page: pageId,
|
||||
element: inputId,
|
||||
text: 'Beta',
|
||||
})
|
||||
assert.ok(!fillResult.isError, textOf(fillResult))
|
||||
|
||||
const val = await execute(evaluate_script, {
|
||||
page: pageId,
|
||||
expression: 'document.getElementById("name").value',
|
||||
})
|
||||
assert.strictEqual(textOf(val), 'Beta')
|
||||
|
||||
await execute(close_page, { page: pageId })
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
it('clear removes all text from an input', async () => {
|
||||
await withBrowser(async ({ execute }) => {
|
||||
const newResult = await execute(new_page, { url: FORM_PAGE })
|
||||
const pageId = pageIdOf(newResult)
|
||||
|
||||
const snap = await execute(take_snapshot, { page: pageId })
|
||||
const inputId = findElementId(textOf(snap), 'Enter name')
|
||||
|
||||
await execute(fill, { page: pageId, element: inputId, text: 'Alpha' })
|
||||
const clearResult = await execute(clear, {
|
||||
page: pageId,
|
||||
element: inputId,
|
||||
})
|
||||
assert.ok(!clearResult.isError, textOf(clearResult))
|
||||
|
||||
const val = await execute(evaluate_script, {
|
||||
page: pageId,
|
||||
expression: 'document.getElementById("name").value',
|
||||
})
|
||||
assert.strictEqual(textOf(val), '')
|
||||
|
||||
await execute(close_page, { page: pageId })
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
it('type_at clears the focused input when requested', async () => {
|
||||
await withBrowser(async ({ execute }) => {
|
||||
const newResult = await execute(new_page, { url: FORM_PAGE })
|
||||
const pageId = pageIdOf(newResult)
|
||||
|
||||
const snap = await execute(take_snapshot, { page: pageId })
|
||||
const inputId = findElementId(textOf(snap), 'Enter name')
|
||||
await execute(fill, { page: pageId, element: inputId, text: 'Alpha' })
|
||||
|
||||
const coordsResult = await execute(evaluate_script, {
|
||||
page: pageId,
|
||||
expression: `(() => {
|
||||
const rect = document.getElementById("name").getBoundingClientRect();
|
||||
return JSON.stringify({
|
||||
x: Math.round(rect.left + rect.width / 2),
|
||||
y: Math.round(rect.top + rect.height / 2),
|
||||
});
|
||||
})()`,
|
||||
})
|
||||
const coords = JSON.parse(textOf(coordsResult)) as {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const typeResult = await execute(type_at, {
|
||||
page: pageId,
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
text: 'Beta',
|
||||
clear: true,
|
||||
})
|
||||
assert.ok(!typeResult.isError, textOf(typeResult))
|
||||
|
||||
const val = await execute(evaluate_script, {
|
||||
page: pageId,
|
||||
expression: 'document.getElementById("name").value',
|
||||
})
|
||||
assert.strictEqual(textOf(val), 'Beta')
|
||||
|
||||
await execute(close_page, { page: pageId })
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
it('click triggers a button', async () => {
|
||||
await withBrowser(async ({ execute }) => {
|
||||
const newResult = await execute(new_page, { url: FORM_PAGE })
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { platform } from 'node:os'
|
||||
import {
|
||||
clearField,
|
||||
getKeyInfo,
|
||||
modifierBitmask,
|
||||
normalizeKey,
|
||||
@@ -158,3 +160,48 @@ describe('pressCombo validation', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearField', () => {
|
||||
it('uses lowercase select-all without standalone modifier events', async () => {
|
||||
const events: Array<Record<string, unknown>> = []
|
||||
const fakeSession = {
|
||||
Input: {
|
||||
dispatchKeyEvent: async (event: Record<string, unknown>) => {
|
||||
events.push(event)
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof clearField>[0]
|
||||
|
||||
await clearField(fakeSession)
|
||||
|
||||
const modifier = platform() === 'darwin' ? 4 : 2
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: 'keyDown',
|
||||
key: 'a',
|
||||
code: 'KeyA',
|
||||
modifiers: modifier,
|
||||
windowsVirtualKeyCode: 65,
|
||||
},
|
||||
{
|
||||
type: 'keyUp',
|
||||
key: 'a',
|
||||
code: 'KeyA',
|
||||
modifiers: modifier,
|
||||
windowsVirtualKeyCode: 65,
|
||||
},
|
||||
{
|
||||
type: 'keyDown',
|
||||
key: 'Backspace',
|
||||
code: 'Backspace',
|
||||
windowsVirtualKeyCode: 8,
|
||||
},
|
||||
{
|
||||
type: 'keyUp',
|
||||
key: 'Backspace',
|
||||
code: 'Backspace',
|
||||
windowsVirtualKeyCode: 8,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user