mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(interaction): add explicit focus and blur primitives
This commit is contained in:
@@ -2,8 +2,10 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { SessionManager } from "@/session-manager/manager";
|
import { SessionManager } from "@/session-manager/manager";
|
||||||
import type { CdpRunner } from "@/tools/shared";
|
import type { CdpRunner } from "@/tools/shared";
|
||||||
import {
|
import {
|
||||||
|
handleBlur,
|
||||||
handleClick,
|
handleClick,
|
||||||
handleFill,
|
handleFill,
|
||||||
|
handleFocus,
|
||||||
handleHover,
|
handleHover,
|
||||||
handlePress,
|
handlePress,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
@@ -540,6 +542,156 @@ describe("handleHover", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handleFocus and handleBlur", () => {
|
||||||
|
it("focuses a ref and verifies the deep active element", async () => {
|
||||||
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
const ctx = await sm.start("aa11");
|
||||||
|
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
||||||
|
const fake = makeFakeCdp({
|
||||||
|
"DOM.scrollIntoViewIfNeeded": () => ({}),
|
||||||
|
"DOM.focus": () => ({}),
|
||||||
|
"DOM.resolveNode": () => ({ object: { objectId: "focus-target" } }),
|
||||||
|
"Runtime.callFunctionOn": () => ({ result: { value: { focused: true } } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await handleFocus(
|
||||||
|
sm,
|
||||||
|
{ session_id: "aa11", ref: "@e3" },
|
||||||
|
{ cdp: fake.cdp, tabsApi: fake.tabsApi },
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
|
||||||
|
expect(res).toMatchObject({ tab_id: 4, used_ref: "e3", focused: true });
|
||||||
|
expect(fake.sent.map((call) => call.method)).toEqual([
|
||||||
|
"DOM.scrollIntoViewIfNeeded",
|
||||||
|
"DOM.focus",
|
||||||
|
"DOM.resolveNode",
|
||||||
|
"Runtime.callFunctionOn",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("focuses an OOPIF ref in its CDP session", async () => {
|
||||||
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
const ctx = await sm.start("aa11");
|
||||||
|
ctx.refStore.set("e3", 1234, {
|
||||||
|
tabId: 4,
|
||||||
|
frameId: "child-frame",
|
||||||
|
cdpSessionId: "child-session",
|
||||||
|
});
|
||||||
|
const fake = makeFakeCdp({
|
||||||
|
"DOM.scrollIntoViewIfNeeded": () => ({}),
|
||||||
|
});
|
||||||
|
fake.cdp.getFrameGraph = vi.fn(async () => ({
|
||||||
|
rootFrameId: "main",
|
||||||
|
frames: [
|
||||||
|
{ frameId: "main", target: { tabId: 4 } },
|
||||||
|
{
|
||||||
|
frameId: "child-frame",
|
||||||
|
parentFrameId: "main",
|
||||||
|
ownerBackendNodeId: 99,
|
||||||
|
target: { tabId: 4, sessionId: "child-session" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
const targetCalls: Array<{ sessionId?: string; method: string }> = [];
|
||||||
|
fake.cdp.sendToTarget = vi.fn(async (target, method) => {
|
||||||
|
targetCalls.push({ sessionId: target.sessionId, method });
|
||||||
|
if (method === "DOM.scrollIntoViewIfNeeded") return {};
|
||||||
|
if (method === "DOM.focus") return {};
|
||||||
|
if (method === "DOM.resolveNode") return { object: { objectId: "focus-target" } };
|
||||||
|
if (method === "Runtime.callFunctionOn") return { result: { value: { focused: true } } };
|
||||||
|
throw new Error(`unexpected child CDP call ${method}`);
|
||||||
|
}) as CdpRunner["sendToTarget"];
|
||||||
|
|
||||||
|
const res = await handleFocus(
|
||||||
|
sm,
|
||||||
|
{ session_id: "aa11", ref: "@e3" },
|
||||||
|
{ cdp: fake.cdp, tabsApi: fake.tabsApi },
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
|
||||||
|
expect(res.focused).toBe(true);
|
||||||
|
expect(targetCalls).toEqual([
|
||||||
|
{ sessionId: "child-session", method: "DOM.scrollIntoViewIfNeeded" },
|
||||||
|
{ sessionId: "child-session", method: "DOM.focus" },
|
||||||
|
{ sessionId: "child-session", method: "DOM.resolveNode" },
|
||||||
|
{ sessionId: "child-session", method: "Runtime.callFunctionOn" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not focus after cancellation during scrolling", async () => {
|
||||||
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
const ctx = await sm.start("aa11");
|
||||||
|
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
||||||
|
const abort = new AbortController();
|
||||||
|
const fake = makeFakeCdp({
|
||||||
|
"DOM.scrollIntoViewIfNeeded": () => {
|
||||||
|
abort.abort();
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await handleFocus(
|
||||||
|
sm,
|
||||||
|
{ session_id: "aa11", ref: "@e3" },
|
||||||
|
{ cdp: fake.cdp, tabsApi: fake.tabsApi, signal: abort.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res).toMatchObject({ code: "cancelled" });
|
||||||
|
expect(fake.sent.some((call) => call.method === "DOM.focus")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blurs a ref and returns its previous focus state", async () => {
|
||||||
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
const ctx = await sm.start("aa11");
|
||||||
|
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
||||||
|
const fake = makeFakeCdp({
|
||||||
|
"DOM.resolveNode": () => ({ object: { objectId: "focus-target" } }),
|
||||||
|
"Runtime.callFunctionOn": () => ({
|
||||||
|
result: { value: { ok: true, was_focused: true, focused: false } },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await handleBlur(
|
||||||
|
sm,
|
||||||
|
{ session_id: "aa11", ref: "@e3" },
|
||||||
|
{ cdp: fake.cdp, tabsApi: fake.tabsApi },
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
|
||||||
|
expect(res).toMatchObject({
|
||||||
|
tab_id: 4,
|
||||||
|
used_ref: "e3",
|
||||||
|
was_focused: true,
|
||||||
|
focused: false,
|
||||||
|
});
|
||||||
|
expect(fake.sent.map((call) => call.method)).toEqual([
|
||||||
|
"DOM.resolveNode",
|
||||||
|
"Runtime.callFunctionOn",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a target that does not implement blur", async () => {
|
||||||
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
const ctx = await sm.start("aa11");
|
||||||
|
ctx.refStore.set("e3", 1234, { tabId: 4 });
|
||||||
|
const fake = makeFakeCdp({
|
||||||
|
"DOM.resolveNode": () => ({ object: { objectId: "focus-target" } }),
|
||||||
|
"Runtime.callFunctionOn": () => ({
|
||||||
|
result: { value: { ok: false, was_focused: false, focused: false } },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await handleBlur(
|
||||||
|
sm,
|
||||||
|
{ session_id: "aa11", ref: "@e3" },
|
||||||
|
{ cdp: fake.cdp, tabsApi: fake.tabsApi },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res).toMatchObject({ code: "invalid_params", message: /does not support blur/ });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("handleFill", () => {
|
describe("handleFill", () => {
|
||||||
it("returns not_found for unknown ref", async () => {
|
it("returns not_found for unknown ref", async () => {
|
||||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { OVERLAY_AUTOMATION_BYPASS } from "@/lib/overlay-bridge";
|
|||||||
import type { SessionManager } from "@/session-manager/manager";
|
import type { SessionManager } from "@/session-manager/manager";
|
||||||
import type { Transport } from "@/transport/transport";
|
import type { Transport } from "@/transport/transport";
|
||||||
import type {
|
import type {
|
||||||
|
BlurParams,
|
||||||
ClickParams,
|
ClickParams,
|
||||||
ConsoleParams,
|
ConsoleParams,
|
||||||
EmulateParams,
|
EmulateParams,
|
||||||
EvaluateParams,
|
EvaluateParams,
|
||||||
FillParams,
|
FillParams,
|
||||||
|
FocusParams,
|
||||||
GetHtmlParams,
|
GetHtmlParams,
|
||||||
HoverParams,
|
HoverParams,
|
||||||
HoverResult,
|
HoverResult,
|
||||||
@@ -35,7 +37,15 @@ import { handleConsole } from "./console";
|
|||||||
import { type EmulateCdpRunner, handleEmulate } from "./emulate";
|
import { type EmulateCdpRunner, handleEmulate } from "./emulate";
|
||||||
import { handleEvaluate } from "./evaluate";
|
import { handleEvaluate } from "./evaluate";
|
||||||
import { handleRequestHelp } from "./human-loop";
|
import { handleRequestHelp } from "./human-loop";
|
||||||
import { handleClick, handleFill, handleHover, handlePress, handleSelect } from "./interaction";
|
import {
|
||||||
|
handleBlur,
|
||||||
|
handleClick,
|
||||||
|
handleFill,
|
||||||
|
handleFocus,
|
||||||
|
handleHover,
|
||||||
|
handlePress,
|
||||||
|
handleSelect,
|
||||||
|
} from "./interaction";
|
||||||
import {
|
import {
|
||||||
handleNavigate,
|
handleNavigate,
|
||||||
handleNavigateBack,
|
handleNavigateBack,
|
||||||
@@ -471,6 +481,28 @@ export class ToolDispatcher {
|
|||||||
);
|
);
|
||||||
return this.rememberHover((req.params as HoverParams).session_id, result);
|
return this.rememberHover((req.params as HoverParams).session_id, result);
|
||||||
}
|
}
|
||||||
|
case "tool.focus":
|
||||||
|
return this.withHoverReleaseForRequest(
|
||||||
|
req.params as FocusParams,
|
||||||
|
() =>
|
||||||
|
handleFocus(
|
||||||
|
this.sessions,
|
||||||
|
req.params as FocusParams,
|
||||||
|
this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsApi, signal } : undefined,
|
||||||
|
),
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
case "tool.blur":
|
||||||
|
return this.withHoverReleaseForRequest(
|
||||||
|
req.params as BlurParams,
|
||||||
|
() =>
|
||||||
|
handleBlur(
|
||||||
|
this.sessions,
|
||||||
|
req.params as BlurParams,
|
||||||
|
this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsApi, signal } : undefined,
|
||||||
|
),
|
||||||
|
signal,
|
||||||
|
);
|
||||||
case "tool.fill":
|
case "tool.fill":
|
||||||
return this.withHoverReleaseForRequest(
|
return this.withHoverReleaseForRequest(
|
||||||
req.params as FillParams,
|
req.params as FillParams,
|
||||||
@@ -718,6 +750,8 @@ function sessionIdForBrowserControlMethod(req: RequestFrame): string | null {
|
|||||||
case "tool.reload":
|
case "tool.reload":
|
||||||
case "tool.click":
|
case "tool.click":
|
||||||
case "tool.hover":
|
case "tool.hover":
|
||||||
|
case "tool.focus":
|
||||||
|
case "tool.blur":
|
||||||
case "tool.fill":
|
case "tool.fill":
|
||||||
case "tool.press":
|
case "tool.press":
|
||||||
case "tool.select":
|
case "tool.select":
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// DOM interaction tools — `tool.click`, `tool.fill`, `tool.press`, and
|
// DOM interaction tools — click, hover, focus/blur, fill, press, and select.
|
||||||
// `tool.select`.
|
|
||||||
//
|
//
|
||||||
// All interaction tools:
|
// All interaction tools:
|
||||||
// 1. Resolve target tab (sandbox: must be inside Agent Window).
|
// 1. Resolve target tab (sandbox: must be inside Agent Window).
|
||||||
@@ -14,10 +13,14 @@ import { ChromiumCdp } from "@/browser-driver/chromium-cdp";
|
|||||||
import type { CdpTarget } from "@/browser-driver/frame-graph";
|
import type { CdpTarget } from "@/browser-driver/frame-graph";
|
||||||
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
||||||
import type {
|
import type {
|
||||||
|
BlurParams,
|
||||||
|
BlurResult,
|
||||||
ClickParams,
|
ClickParams,
|
||||||
ClickResult,
|
ClickResult,
|
||||||
FillParams,
|
FillParams,
|
||||||
FillResult,
|
FillResult,
|
||||||
|
FocusParams,
|
||||||
|
FocusResult,
|
||||||
HoverParams,
|
HoverParams,
|
||||||
HoverResult,
|
HoverResult,
|
||||||
KeyModifier,
|
KeyModifier,
|
||||||
@@ -215,6 +218,157 @@ async function resolveBackendNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FocusCheck = { focused: boolean };
|
||||||
|
type BlurMutation =
|
||||||
|
| { ok: true; was_focused: boolean; focused: boolean }
|
||||||
|
| { ok: false; was_focused: boolean; focused: boolean };
|
||||||
|
|
||||||
|
const DEEP_FOCUS_CHECK = `function() {
|
||||||
|
const deepActiveElement = (root) => {
|
||||||
|
let active = root && root.activeElement;
|
||||||
|
while (active && active.shadowRoot && active.shadowRoot.activeElement) {
|
||||||
|
active = active.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
|
return active;
|
||||||
|
};
|
||||||
|
return { focused: deepActiveElement(this.ownerDocument) === this };
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const BLUR_TARGET = `function() {
|
||||||
|
const deepActiveElement = (root) => {
|
||||||
|
let active = root && root.activeElement;
|
||||||
|
while (active && active.shadowRoot && active.shadowRoot.activeElement) {
|
||||||
|
active = active.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
|
return active;
|
||||||
|
};
|
||||||
|
const wasFocused = deepActiveElement(this.ownerDocument) === this;
|
||||||
|
if (typeof this.blur !== 'function') {
|
||||||
|
return { ok: false, was_focused: wasFocused, focused: wasFocused };
|
||||||
|
}
|
||||||
|
this.blur();
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
was_focused: wasFocused,
|
||||||
|
focused: deepActiveElement(this.ownerDocument) === this,
|
||||||
|
};
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tool.focus / tool.blur
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function handleFocus(
|
||||||
|
manager: SessionManager,
|
||||||
|
params: FocusParams,
|
||||||
|
deps: InteractionDeps = getDefaultDeps(),
|
||||||
|
): Promise<FocusResult | RpcError> {
|
||||||
|
const ctxOrErr = lookupSession(manager, params, "focus");
|
||||||
|
if (isRpcError(ctxOrErr)) return ctxOrErr;
|
||||||
|
const ctx = ctxOrErr;
|
||||||
|
const aborted = throwIfAborted(deps.signal);
|
||||||
|
if (aborted) return aborted;
|
||||||
|
const target = await resolveTargetTab(manager, ctx, params.tab_id, deps.tabsApi);
|
||||||
|
if (isRpcError(target)) return target;
|
||||||
|
const denied = enforceAgentWindow(ctx, target, "focus");
|
||||||
|
if (denied) return denied;
|
||||||
|
const dialogCursor = markDialogCursor(deps.cdp, target.tabId);
|
||||||
|
const node = await resolveBackendNode(deps.cdp, ctx, target, params, "focus");
|
||||||
|
if (isRpcError(node)) return node;
|
||||||
|
const nodeCdp = cdpRunnerForTarget(deps.cdp, node.cdpTarget);
|
||||||
|
|
||||||
|
deps.cdp.trackSessionTab?.(ctx.sessionId, target.tabId);
|
||||||
|
const scrollErr = await scrollElementAndFramesIntoView(
|
||||||
|
deps.cdp,
|
||||||
|
target.tabId,
|
||||||
|
node.cdpTarget,
|
||||||
|
node.backendNodeId,
|
||||||
|
node.frameId,
|
||||||
|
);
|
||||||
|
if (scrollErr) return scrollErr;
|
||||||
|
const abortedBeforeFocus = throwIfAborted(deps.signal);
|
||||||
|
if (abortedBeforeFocus) return abortedBeforeFocus;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nodeCdp.send(target.tabId, "DOM.focus", { backendNodeId: node.backendNodeId });
|
||||||
|
const objectIdOrErr = await backendNodeToObject(nodeCdp, target.tabId, node.backendNodeId);
|
||||||
|
if (isRpcError(objectIdOrErr)) return objectIdOrErr;
|
||||||
|
const evaluated = await nodeCdp.send<{ result?: { value?: FocusCheck } }>(
|
||||||
|
target.tabId,
|
||||||
|
"Runtime.callFunctionOn",
|
||||||
|
{
|
||||||
|
objectId: objectIdOrErr,
|
||||||
|
functionDeclaration: DEEP_FOCUS_CHECK,
|
||||||
|
returnByValue: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (evaluated.result?.value?.focused !== true) {
|
||||||
|
return { code: "invalid_params", message: "target element did not become focused" };
|
||||||
|
}
|
||||||
|
return attachDialogs(deps.cdp, target.tabId, dialogCursor, {
|
||||||
|
tab_id: target.tabId,
|
||||||
|
used_ref: node.usedRef,
|
||||||
|
used_selector: node.usedSelector,
|
||||||
|
focused: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return { code: "cdp_failed", message: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleBlur(
|
||||||
|
manager: SessionManager,
|
||||||
|
params: BlurParams,
|
||||||
|
deps: InteractionDeps = getDefaultDeps(),
|
||||||
|
): Promise<BlurResult | RpcError> {
|
||||||
|
const ctxOrErr = lookupSession(manager, params, "blur");
|
||||||
|
if (isRpcError(ctxOrErr)) return ctxOrErr;
|
||||||
|
const ctx = ctxOrErr;
|
||||||
|
const aborted = throwIfAborted(deps.signal);
|
||||||
|
if (aborted) return aborted;
|
||||||
|
const target = await resolveTargetTab(manager, ctx, params.tab_id, deps.tabsApi);
|
||||||
|
if (isRpcError(target)) return target;
|
||||||
|
const denied = enforceAgentWindow(ctx, target, "blur");
|
||||||
|
if (denied) return denied;
|
||||||
|
const dialogCursor = markDialogCursor(deps.cdp, target.tabId);
|
||||||
|
const node = await resolveBackendNode(deps.cdp, ctx, target, params, "blur");
|
||||||
|
if (isRpcError(node)) return node;
|
||||||
|
const nodeCdp = cdpRunnerForTarget(deps.cdp, node.cdpTarget);
|
||||||
|
deps.cdp.trackSessionTab?.(ctx.sessionId, target.tabId);
|
||||||
|
const abortedBeforeBlur = throwIfAborted(deps.signal);
|
||||||
|
if (abortedBeforeBlur) return abortedBeforeBlur;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const objectIdOrErr = await backendNodeToObject(nodeCdp, target.tabId, node.backendNodeId);
|
||||||
|
if (isRpcError(objectIdOrErr)) return objectIdOrErr;
|
||||||
|
const evaluated = await nodeCdp.send<{ result?: { value?: BlurMutation } }>(
|
||||||
|
target.tabId,
|
||||||
|
"Runtime.callFunctionOn",
|
||||||
|
{
|
||||||
|
objectId: objectIdOrErr,
|
||||||
|
functionDeclaration: BLUR_TARGET,
|
||||||
|
returnByValue: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const mutation = evaluated.result?.value;
|
||||||
|
if (!mutation?.ok) {
|
||||||
|
return { code: "invalid_params", message: "target element does not support blur()" };
|
||||||
|
}
|
||||||
|
if (mutation.focused) {
|
||||||
|
return { code: "cdp_failed", message: "target element remained focused after blur()" };
|
||||||
|
}
|
||||||
|
return attachDialogs(deps.cdp, target.tabId, dialogCursor, {
|
||||||
|
tab_id: target.tabId,
|
||||||
|
used_ref: node.usedRef,
|
||||||
|
used_selector: node.usedSelector,
|
||||||
|
was_focused: mutation.was_focused,
|
||||||
|
focused: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return { code: "cdp_failed", message: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// tool.click
|
// tool.click
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -444,6 +444,39 @@ export interface HoverResult {
|
|||||||
dialogs?: JavaScriptDialogInfo[];
|
dialogs?: JavaScriptDialogInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FocusParams {
|
||||||
|
session_id: string;
|
||||||
|
ref?: string;
|
||||||
|
selector?: string;
|
||||||
|
tab_id?: number;
|
||||||
|
timeout_ms?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FocusResult {
|
||||||
|
tab_id: number;
|
||||||
|
used_ref?: string;
|
||||||
|
used_selector?: string;
|
||||||
|
focused: boolean;
|
||||||
|
dialogs?: JavaScriptDialogInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlurParams {
|
||||||
|
session_id: string;
|
||||||
|
ref?: string;
|
||||||
|
selector?: string;
|
||||||
|
tab_id?: number;
|
||||||
|
timeout_ms?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlurResult {
|
||||||
|
tab_id: number;
|
||||||
|
used_ref?: string;
|
||||||
|
used_selector?: string;
|
||||||
|
was_focused: boolean;
|
||||||
|
focused: boolean;
|
||||||
|
dialogs?: JavaScriptDialogInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FillParams {
|
export interface FillParams {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ bsk navigate <url> --session <id>
|
|||||||
bsk observe --session <id> → primary semantic VOM view; reveals hover/focus surfaces
|
bsk observe --session <id> → primary semantic VOM view; reveals hover/focus surfaces
|
||||||
bsk snapshot --session <id> → static aria tree fallback when VOM is insufficient
|
bsk snapshot --session <id> → static aria tree fallback when VOM is insufficient
|
||||||
bsk hover @e3 --session <id> → reveal hover-triggered menus before re-observing/clicking
|
bsk hover @e3 --session <id> → reveal hover-triggered menus before re-observing/clicking
|
||||||
|
bsk focus @e4 --session <id> → explicitly enter focus-driven UI states
|
||||||
|
bsk blur @e4 --session <id> → explicitly leave focus-driven UI states
|
||||||
bsk click @e4 --session <id> → or bsk fill, bsk select, bsk press
|
bsk click @e4 --session <id> → or bsk fill, bsk select, bsk press
|
||||||
bsk observe --session <id> → again after navigation / DOM change
|
bsk observe --session <id> → again after navigation / DOM change
|
||||||
```
|
```
|
||||||
@@ -202,6 +204,8 @@ Both capture from the moment the tab is attached and read a bounded per-tab buff
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `bsk click <ref-or-selector>` | Click element (`--button`, `--click-count`, `--modifiers`) |
|
| `bsk click <ref-or-selector>` | Click element (`--button`, `--click-count`, `--modifiers`) |
|
||||||
| `bsk hover <ref-or-selector>` | Move the mouse to an element and wait for hover UI to settle (`--settle`, `--modifiers`) |
|
| `bsk hover <ref-or-selector>` | Move the mouse to an element and wait for hover UI to settle (`--settle`, `--modifiers`) |
|
||||||
|
| `bsk focus <ref-or-selector>` | Focus an element and verify it became the deep active element |
|
||||||
|
| `bsk blur <ref-or-selector>` | Remove focus from an element and report whether it was focused |
|
||||||
| `bsk fill <ref-or-selector> --value <text>` | Clear and type into input |
|
| `bsk fill <ref-or-selector> --value <text>` | Clear and type into input |
|
||||||
| `bsk select <ref-or-selector> --value <v>` | Set `<select>` option(s) by `value` (repeat `--value` for multi-select) |
|
| `bsk select <ref-or-selector> --value <v>` | Set `<select>` option(s) by `value` (repeat `--value` for multi-select) |
|
||||||
| `bsk press <key>` | Key/combo (`Enter`, `Ctrl+A`, …; optional `--ref` to focus first) |
|
| `bsk press <key>` | Key/combo (`Enter`, `Ctrl+A`, …; optional `--ref` to focus first) |
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! `bsk click` / `bsk fill` / `bsk press` (M7 interaction tools).
|
//! `bsk click` / `bsk focus` / `bsk blur` / `bsk fill` / `bsk press`
|
||||||
|
//! interaction tools.
|
||||||
//!
|
//!
|
||||||
//! The `<target>` positional accepts either a snapshot ref (`@e3`,
|
//! The `<target>` positional accepts either a snapshot ref (`@e3`,
|
||||||
//! `e3`) or a CSS selector. Use `--ref` / `--selector` explicitly when
|
//! `e3`) or a CSS selector. Use `--ref` / `--selector` explicitly when
|
||||||
@@ -10,8 +11,9 @@ use std::time::Duration;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use bsk_protocol::Method;
|
use bsk_protocol::Method;
|
||||||
use bsk_protocol::tools::{
|
use bsk_protocol::tools::{
|
||||||
ClickParams, ClickResult, FillParams, FillResult, HoverParams, HoverResult, KeyModifier,
|
BlurParams, BlurResult, ClickParams, ClickResult, FillParams, FillResult, FocusParams,
|
||||||
MouseButton, PressParams, PressResult, SelectParams, SelectResult,
|
FocusResult, HoverParams, HoverResult, KeyModifier, MouseButton, PressParams, PressResult,
|
||||||
|
SelectParams, SelectResult,
|
||||||
};
|
};
|
||||||
use clap::{Args, ValueEnum};
|
use clap::{Args, ValueEnum};
|
||||||
|
|
||||||
@@ -274,6 +276,124 @@ pub fn dispatch_hover(args: HoverArgs, format: Format) -> Result<(), CliError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// bsk focus / bsk blur
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct FocusArgs {
|
||||||
|
/// Snapshot ref (`@e3`, `e3`) or CSS selector. Optional when `--ref`/`--selector` is used.
|
||||||
|
pub target: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "ref")]
|
||||||
|
pub ref_: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "selector")]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub session: String,
|
||||||
|
|
||||||
|
#[arg(long = "tab-id")]
|
||||||
|
pub tab_id: Option<i64>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "30s", value_parser = parse_timeout_ms)]
|
||||||
|
pub timeout: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_focus(args: FocusArgs, format: Format) -> Result<(), CliError> {
|
||||||
|
let info = ensure_daemon().context("ensure daemon is running")?;
|
||||||
|
let (ref_, selector) = split_target(args.target, args.ref_, args.selector)?;
|
||||||
|
let params = FocusParams {
|
||||||
|
session_id: args.session,
|
||||||
|
ref_,
|
||||||
|
selector,
|
||||||
|
tab_id: args.tab_id,
|
||||||
|
timeout_ms: Some(args.timeout),
|
||||||
|
};
|
||||||
|
let reply: FocusResult = call(
|
||||||
|
info.sock_path,
|
||||||
|
Method::ToolFocus,
|
||||||
|
params,
|
||||||
|
"focus-1",
|
||||||
|
args.timeout,
|
||||||
|
)?;
|
||||||
|
match format {
|
||||||
|
Format::Json => println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&reply)
|
||||||
|
.map_err(|e| CliError::Local(anyhow::anyhow!(e)))?
|
||||||
|
),
|
||||||
|
Format::Human => {
|
||||||
|
let target =
|
||||||
|
format_used_target(reply.used_ref.as_deref(), reply.used_selector.as_deref());
|
||||||
|
println!(
|
||||||
|
"focus ok tab={} target={target} focused={}",
|
||||||
|
reply.tab_id, reply.focused
|
||||||
|
);
|
||||||
|
print_dialog_summaries(&reply.dialogs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct BlurArgs {
|
||||||
|
/// Snapshot ref (`@e3`, `e3`) or CSS selector. Optional when `--ref`/`--selector` is used.
|
||||||
|
pub target: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "ref")]
|
||||||
|
pub ref_: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "selector")]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub session: String,
|
||||||
|
|
||||||
|
#[arg(long = "tab-id")]
|
||||||
|
pub tab_id: Option<i64>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "30s", value_parser = parse_timeout_ms)]
|
||||||
|
pub timeout: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_blur(args: BlurArgs, format: Format) -> Result<(), CliError> {
|
||||||
|
let info = ensure_daemon().context("ensure daemon is running")?;
|
||||||
|
let (ref_, selector) = split_target(args.target, args.ref_, args.selector)?;
|
||||||
|
let params = BlurParams {
|
||||||
|
session_id: args.session,
|
||||||
|
ref_,
|
||||||
|
selector,
|
||||||
|
tab_id: args.tab_id,
|
||||||
|
timeout_ms: Some(args.timeout),
|
||||||
|
};
|
||||||
|
let reply: BlurResult = call(
|
||||||
|
info.sock_path,
|
||||||
|
Method::ToolBlur,
|
||||||
|
params,
|
||||||
|
"blur-1",
|
||||||
|
args.timeout,
|
||||||
|
)?;
|
||||||
|
match format {
|
||||||
|
Format::Json => println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&reply)
|
||||||
|
.map_err(|e| CliError::Local(anyhow::anyhow!(e)))?
|
||||||
|
),
|
||||||
|
Format::Human => {
|
||||||
|
let target =
|
||||||
|
format_used_target(reply.used_ref.as_deref(), reply.used_selector.as_deref());
|
||||||
|
println!(
|
||||||
|
"blur ok tab={} target={target} was_focused={} focused={}",
|
||||||
|
reply.tab_id, reply.was_focused, reply.focused
|
||||||
|
);
|
||||||
|
print_dialog_summaries(&reply.dialogs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// bsk fill
|
// bsk fill
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ use crate::cli::evaluate::EvaluateArgs;
|
|||||||
use crate::cli::get_html::GetHtmlArgs;
|
use crate::cli::get_html::GetHtmlArgs;
|
||||||
use crate::cli::human_loop::RequestHelpArgs;
|
use crate::cli::human_loop::RequestHelpArgs;
|
||||||
use crate::cli::install_skill::InstallSkillArgs;
|
use crate::cli::install_skill::InstallSkillArgs;
|
||||||
use crate::cli::interaction::{ClickArgs, FillArgs, HoverArgs, PressArgs, SelectArgs};
|
use crate::cli::interaction::{
|
||||||
|
BlurArgs, ClickArgs, FillArgs, FocusArgs, HoverArgs, PressArgs, SelectArgs,
|
||||||
|
};
|
||||||
use crate::cli::navigate::{NavigateCommand, NavigateHistoryArgs, ReloadArgs};
|
use crate::cli::navigate::{NavigateCommand, NavigateHistoryArgs, ReloadArgs};
|
||||||
use crate::cli::network::NetworkArgs;
|
use crate::cli::network::NetworkArgs;
|
||||||
use crate::cli::observe::ObserveArgs;
|
use crate::cli::observe::ObserveArgs;
|
||||||
@@ -167,6 +169,12 @@ pub enum Command {
|
|||||||
/// Hover a snapshot ref or CSS selector.
|
/// Hover a snapshot ref or CSS selector.
|
||||||
Hover(HoverArgs),
|
Hover(HoverArgs),
|
||||||
|
|
||||||
|
/// Focus a snapshot ref or CSS selector.
|
||||||
|
Focus(FocusArgs),
|
||||||
|
|
||||||
|
/// Remove focus from a snapshot ref or CSS selector.
|
||||||
|
Blur(BlurArgs),
|
||||||
|
|
||||||
/// Fill an input / textarea / contenteditable.
|
/// Fill an input / textarea / contenteditable.
|
||||||
Fill(FillArgs),
|
Fill(FillArgs),
|
||||||
|
|
||||||
|
|||||||
@@ -243,6 +243,8 @@ pub fn full_handler(status: DaemonStatus, state: Arc<DaemonState>) -> RpcHandler
|
|||||||
| Method::ToolReload
|
| Method::ToolReload
|
||||||
| Method::ToolClick
|
| Method::ToolClick
|
||||||
| Method::ToolHover
|
| Method::ToolHover
|
||||||
|
| Method::ToolFocus
|
||||||
|
| Method::ToolBlur
|
||||||
| Method::ToolFill
|
| Method::ToolFill
|
||||||
| Method::ToolPress
|
| Method::ToolPress
|
||||||
| Method::ToolSelect
|
| Method::ToolSelect
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ fn dispatch(cli: Cli, format: Format) -> Result<(), CliError> {
|
|||||||
Command::Reload(args) => cli::navigate::dispatch_reload(args, format),
|
Command::Reload(args) => cli::navigate::dispatch_reload(args, format),
|
||||||
Command::Click(args) => cli::interaction::dispatch_click(args, format),
|
Command::Click(args) => cli::interaction::dispatch_click(args, format),
|
||||||
Command::Hover(args) => cli::interaction::dispatch_hover(args, format),
|
Command::Hover(args) => cli::interaction::dispatch_hover(args, format),
|
||||||
|
Command::Focus(args) => cli::interaction::dispatch_focus(args, format),
|
||||||
|
Command::Blur(args) => cli::interaction::dispatch_blur(args, format),
|
||||||
Command::Fill(args) => cli::interaction::dispatch_fill(args, format),
|
Command::Fill(args) => cli::interaction::dispatch_fill(args, format),
|
||||||
Command::Press(args) => cli::interaction::dispatch_press(args, format),
|
Command::Press(args) => cli::interaction::dispatch_press(args, format),
|
||||||
Command::Select(args) => cli::interaction::dispatch_select(args, format),
|
Command::Select(args) => cli::interaction::dispatch_select(args, format),
|
||||||
|
|||||||
@@ -206,6 +206,24 @@ fn parses_hover_with_settle() {
|
|||||||
assert_eq!(args.settle, 300);
|
assert_eq!(args.settle, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_focus_target() {
|
||||||
|
let cli = parse(&["bsk", "focus", "@e2", "--session", "s1"]);
|
||||||
|
let Command::Focus(args) = cli.command else {
|
||||||
|
panic!("expected focus command");
|
||||||
|
};
|
||||||
|
assert_eq!(args.target.as_deref(), Some("@e2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_blur_selector() {
|
||||||
|
let cli = parse(&["bsk", "blur", "--selector", "#search", "--session", "s1"]);
|
||||||
|
let Command::Blur(args) = cli.command else {
|
||||||
|
panic!("expected blur command");
|
||||||
|
};
|
||||||
|
assert_eq!(args.selector.as_deref(), Some("#search"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_zero_click_count() {
|
fn rejects_zero_click_count() {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//! M7 integration: drive the 7 new tool RPCs (navigate /
|
//! M7 integration: drive interaction and navigation tool RPCs
|
||||||
//! navigate_back / navigate_forward / reload / click / fill / press)
|
|
||||||
//! through the IPC + per-session queue + fake extension and assert
|
//! through the IPC + per-session queue + fake extension and assert
|
||||||
//! the wire shapes line up. The extension stub mirrors each method's
|
//! the wire shapes line up. The extension stub mirrors each method's
|
||||||
//! `*Result` so we exercise the daemon's serialise → forward →
|
//! `*Result` so we exercise the daemon's serialise → forward →
|
||||||
@@ -13,10 +12,11 @@ use bsk::daemon::{self, DaemonConfig};
|
|||||||
use bsk::ipc_client::IpcClient;
|
use bsk::ipc_client::IpcClient;
|
||||||
use bsk_protocol::system::{HandshakeParams, HandshakeResult};
|
use bsk_protocol::system::{HandshakeParams, HandshakeResult};
|
||||||
use bsk_protocol::tools::{
|
use bsk_protocol::tools::{
|
||||||
ClickParams, ClickResult, FillParams, FillResult, KeyModifier, MouseButton, NavigateBackParams,
|
BlurParams, BlurResult, ClickParams, ClickResult, FillParams, FillResult, FocusParams,
|
||||||
NavigateBackResult, NavigateForwardParams, NavigateForwardResult, NavigateParams,
|
FocusResult, KeyModifier, MouseButton, NavigateBackParams, NavigateBackResult,
|
||||||
NavigateResult, PressParams, PressResult, ReloadParams, ReloadResult, SelectParams,
|
NavigateForwardParams, NavigateForwardResult, NavigateParams, NavigateResult, PressParams,
|
||||||
SelectResult, SessionStartParams, SessionStartResult, WaitUntil,
|
PressResult, ReloadParams, ReloadResult, SelectParams, SelectResult, SessionStartParams,
|
||||||
|
SessionStartResult, WaitUntil,
|
||||||
};
|
};
|
||||||
use bsk_protocol::{
|
use bsk_protocol::{
|
||||||
BrowserPeerInfo, ErrorCode, Frame, Method, RequestFrame, ResponseBody, ResponseFrame, RpcError,
|
BrowserPeerInfo, ErrorCode, Frame, Method, RequestFrame, ResponseBody, ResponseFrame, RpcError,
|
||||||
@@ -382,6 +382,92 @@ async fn click_round_trips_ref_and_modifiers() {
|
|||||||
handle.shutdown().await;
|
handle.shutdown().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn focus_round_trips_verified_state() {
|
||||||
|
let (handle, sock) = spawn_daemon().await;
|
||||||
|
let mut ws = connect_ext(handle.ws_addr()).await;
|
||||||
|
let _ = do_handshake(&mut ws).await;
|
||||||
|
|
||||||
|
run_extension(ws, |req| {
|
||||||
|
assert_eq!(req.method, Method::ToolFocus);
|
||||||
|
let params = req.params.clone().unwrap();
|
||||||
|
let p: FocusParams = serde_json::from_value(params).unwrap();
|
||||||
|
assert_eq!(p.ref_.as_deref(), Some("@e3"));
|
||||||
|
ResponseBody::Ok(
|
||||||
|
serde_json::to_value(FocusResult {
|
||||||
|
tab_id: 9,
|
||||||
|
used_ref: Some("e3".into()),
|
||||||
|
used_selector: None,
|
||||||
|
focused: true,
|
||||||
|
dialogs: vec![],
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let session_id = ipc_session_start(&sock).await;
|
||||||
|
let result: FocusResult = ipc_tool_call(
|
||||||
|
&sock,
|
||||||
|
Method::ToolFocus,
|
||||||
|
FocusParams {
|
||||||
|
session_id,
|
||||||
|
ref_: Some("@e3".into()),
|
||||||
|
selector: None,
|
||||||
|
tab_id: None,
|
||||||
|
timeout_ms: Some(5_000),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("focus ok");
|
||||||
|
assert!(result.focused);
|
||||||
|
assert_eq!(result.used_ref.as_deref(), Some("e3"));
|
||||||
|
handle.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn blur_round_trips_previous_focus_state() {
|
||||||
|
let (handle, sock) = spawn_daemon().await;
|
||||||
|
let mut ws = connect_ext(handle.ws_addr()).await;
|
||||||
|
let _ = do_handshake(&mut ws).await;
|
||||||
|
|
||||||
|
run_extension(ws, |req| {
|
||||||
|
assert_eq!(req.method, Method::ToolBlur);
|
||||||
|
let params = req.params.clone().unwrap();
|
||||||
|
let p: BlurParams = serde_json::from_value(params).unwrap();
|
||||||
|
assert_eq!(p.selector.as_deref(), Some("#search"));
|
||||||
|
ResponseBody::Ok(
|
||||||
|
serde_json::to_value(BlurResult {
|
||||||
|
tab_id: 9,
|
||||||
|
used_ref: None,
|
||||||
|
used_selector: Some("#search".into()),
|
||||||
|
was_focused: true,
|
||||||
|
focused: false,
|
||||||
|
dialogs: vec![],
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let session_id = ipc_session_start(&sock).await;
|
||||||
|
let result: BlurResult = ipc_tool_call(
|
||||||
|
&sock,
|
||||||
|
Method::ToolBlur,
|
||||||
|
BlurParams {
|
||||||
|
session_id,
|
||||||
|
ref_: None,
|
||||||
|
selector: Some("#search".into()),
|
||||||
|
tab_id: None,
|
||||||
|
timeout_ms: Some(5_000),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("blur ok");
|
||||||
|
assert!(result.was_focused);
|
||||||
|
assert!(!result.focused);
|
||||||
|
assert_eq!(result.used_selector.as_deref(), Some("#search"));
|
||||||
|
handle.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn fill_round_trips_clear_before_default() {
|
async fn fill_round_trips_clear_before_default() {
|
||||||
let (handle, sock) = spawn_daemon().await;
|
let (handle, sock) = spawn_daemon().await;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "BlurParams",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ref": {
|
||||||
|
"description": "Optional `@e<N>` ref allocated by the last observation. Mutually exclusive with `selector`.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"description": "Target tab. Defaults to the Agent Window's active tab.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "BlurResult",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"focused",
|
||||||
|
"tab_id",
|
||||||
|
"was_focused"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"dialogs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"focused": {
|
||||||
|
"description": "Always false for a successful blur; included for machine verification.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"used_ref": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"used_selector": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"was_focused": {
|
||||||
|
"description": "Whether the target held focus before the call.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"JavaScriptDialogHandledAction": {
|
||||||
|
"description": "How the extension resolved the dialog so CDP could continue.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"accepted",
|
||||||
|
"dismissed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"JavaScriptDialogInfo": {
|
||||||
|
"description": "One observed + handled JavaScript dialog during a tool call.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"handled",
|
||||||
|
"message",
|
||||||
|
"sequence",
|
||||||
|
"tab_id",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"default_prompt": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"handled": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogHandledAction"
|
||||||
|
},
|
||||||
|
"has_browser_handler": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"description": "Monotonic per-tab sequence for ordering within a session.",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogType"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"JavaScriptDialogType": {
|
||||||
|
"description": "Native JS dialog kind reported by CDP.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"alert",
|
||||||
|
"confirm",
|
||||||
|
"prompt",
|
||||||
|
"beforeunload"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "FocusParams",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ref": {
|
||||||
|
"description": "Optional `@e<N>` ref allocated by the last observation. Mutually exclusive with `selector`.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"description": "Target tab. Defaults to the Agent Window's active tab.",
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "FocusResult",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"focused",
|
||||||
|
"tab_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"dialogs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"focused": {
|
||||||
|
"description": "Whether the target is the document's deep active element after the call.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"used_ref": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"used_selector": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"JavaScriptDialogHandledAction": {
|
||||||
|
"description": "How the extension resolved the dialog so CDP could continue.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"accepted",
|
||||||
|
"dismissed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"JavaScriptDialogInfo": {
|
||||||
|
"description": "One observed + handled JavaScript dialog during a tool call.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"handled",
|
||||||
|
"message",
|
||||||
|
"sequence",
|
||||||
|
"tab_id",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"default_prompt": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"handled": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogHandledAction"
|
||||||
|
},
|
||||||
|
"has_browser_handler": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sequence": {
|
||||||
|
"description": "Monotonic per-tab sequence for ordering within a session.",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"tab_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/definitions/JavaScriptDialogType"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"JavaScriptDialogType": {
|
||||||
|
"description": "Native JS dialog kind reported by CDP.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"alert",
|
||||||
|
"confirm",
|
||||||
|
"prompt",
|
||||||
|
"beforeunload"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,10 @@ fn main() {
|
|||||||
dump!(ClickResult, "tool_click_result");
|
dump!(ClickResult, "tool_click_result");
|
||||||
dump!(HoverParams, "tool_hover_params");
|
dump!(HoverParams, "tool_hover_params");
|
||||||
dump!(HoverResult, "tool_hover_result");
|
dump!(HoverResult, "tool_hover_result");
|
||||||
|
dump!(FocusParams, "tool_focus_params");
|
||||||
|
dump!(FocusResult, "tool_focus_result");
|
||||||
|
dump!(BlurParams, "tool_blur_params");
|
||||||
|
dump!(BlurResult, "tool_blur_result");
|
||||||
dump!(FillParams, "tool_fill_params");
|
dump!(FillParams, "tool_fill_params");
|
||||||
dump!(FillResult, "tool_fill_result");
|
dump!(FillResult, "tool_fill_result");
|
||||||
dump!(PressParams, "tool_press_params");
|
dump!(PressParams, "tool_press_params");
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ pub enum Method {
|
|||||||
ToolClick,
|
ToolClick,
|
||||||
#[serde(rename = "tool.hover")]
|
#[serde(rename = "tool.hover")]
|
||||||
ToolHover,
|
ToolHover,
|
||||||
|
#[serde(rename = "tool.focus")]
|
||||||
|
ToolFocus,
|
||||||
|
#[serde(rename = "tool.blur")]
|
||||||
|
ToolBlur,
|
||||||
#[serde(rename = "tool.fill")]
|
#[serde(rename = "tool.fill")]
|
||||||
ToolFill,
|
ToolFill,
|
||||||
#[serde(rename = "tool.press")]
|
#[serde(rename = "tool.press")]
|
||||||
@@ -153,6 +157,8 @@ impl Method {
|
|||||||
| Method::ToolNavigateForward
|
| Method::ToolNavigateForward
|
||||||
| Method::ToolReload
|
| Method::ToolReload
|
||||||
| Method::ToolClick
|
| Method::ToolClick
|
||||||
|
| Method::ToolFocus
|
||||||
|
| Method::ToolBlur
|
||||||
| Method::ToolFill
|
| Method::ToolFill
|
||||||
| Method::ToolPress
|
| Method::ToolPress
|
||||||
| Method::ToolSelect
|
| Method::ToolSelect
|
||||||
@@ -283,6 +289,8 @@ mod tests {
|
|||||||
assert!(Method::ToolNavigateForward.is_mutating());
|
assert!(Method::ToolNavigateForward.is_mutating());
|
||||||
assert!(Method::ToolReload.is_mutating());
|
assert!(Method::ToolReload.is_mutating());
|
||||||
assert!(Method::ToolClick.is_mutating());
|
assert!(Method::ToolClick.is_mutating());
|
||||||
|
assert!(Method::ToolFocus.is_mutating());
|
||||||
|
assert!(Method::ToolBlur.is_mutating());
|
||||||
assert!(Method::ToolFill.is_mutating());
|
assert!(Method::ToolFill.is_mutating());
|
||||||
assert!(Method::ToolPress.is_mutating());
|
assert!(Method::ToolPress.is_mutating());
|
||||||
assert!(Method::ToolSelect.is_mutating());
|
assert!(Method::ToolSelect.is_mutating());
|
||||||
@@ -327,6 +335,8 @@ mod tests {
|
|||||||
assert_eq!(Method::ToolHover.effect(), MethodEffect::TransientInput);
|
assert_eq!(Method::ToolHover.effect(), MethodEffect::TransientInput);
|
||||||
assert_eq!(Method::ToolObserve.effect(), MethodEffect::TransientInput);
|
assert_eq!(Method::ToolObserve.effect(), MethodEffect::TransientInput);
|
||||||
assert_eq!(Method::ToolClick.effect(), MethodEffect::BrowserMutation);
|
assert_eq!(Method::ToolClick.effect(), MethodEffect::BrowserMutation);
|
||||||
|
assert_eq!(Method::ToolFocus.effect(), MethodEffect::BrowserMutation);
|
||||||
|
assert_eq!(Method::ToolBlur.effect(), MethodEffect::BrowserMutation);
|
||||||
assert_eq!(Method::Cancel.effect(), MethodEffect::ControlPlane);
|
assert_eq!(Method::Cancel.effect(), MethodEffect::ControlPlane);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +346,8 @@ mod tests {
|
|||||||
assert!(Method::ToolHover.requires_interrupt_gate());
|
assert!(Method::ToolHover.requires_interrupt_gate());
|
||||||
assert!(Method::ToolObserve.requires_interrupt_gate());
|
assert!(Method::ToolObserve.requires_interrupt_gate());
|
||||||
assert!(Method::ToolClick.requires_interrupt_gate());
|
assert!(Method::ToolClick.requires_interrupt_gate());
|
||||||
|
assert!(Method::ToolFocus.requires_interrupt_gate());
|
||||||
|
assert!(Method::ToolBlur.requires_interrupt_gate());
|
||||||
assert!(!Method::Cancel.requires_interrupt_gate());
|
assert!(!Method::Cancel.requires_interrupt_gate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! DOM interaction tools (`tool.click`, `tool.fill`, `tool.press`,
|
//! DOM interaction tools (`tool.click`, `tool.hover`, `tool.focus`,
|
||||||
//! `tool.select`).
|
//! `tool.blur`, `tool.fill`, `tool.press`, `tool.select`).
|
||||||
//!
|
//!
|
||||||
//! Element-targeted tools accept either a snapshot `ref` (`@e<N>` form,
|
//! Element-targeted tools accept either a snapshot `ref` (`@e<N>` form,
|
||||||
//! normalised against the session's RefStore) **or** a CSS selector
|
//! normalised against the session's RefStore) **or** a CSS selector
|
||||||
@@ -131,6 +131,82 @@ pub struct HoverResult {
|
|||||||
pub dialogs: Vec<JavaScriptDialogInfo>,
|
pub dialogs: Vec<JavaScriptDialogInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// focus / blur
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct FocusParams {
|
||||||
|
pub session_id: String,
|
||||||
|
/// Optional `@e<N>` ref allocated by the last observation.
|
||||||
|
/// Mutually exclusive with `selector`.
|
||||||
|
#[serde(
|
||||||
|
rename = "ref",
|
||||||
|
alias = "ref_",
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub ref_: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
/// Target tab. Defaults to the Agent Window's active tab.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tab_id: Option<i64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(range(min = 1))]
|
||||||
|
pub timeout_ms: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct FocusResult {
|
||||||
|
pub tab_id: i64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub used_ref: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub used_selector: Option<String>,
|
||||||
|
/// Whether the target is the document's deep active element after the call.
|
||||||
|
pub focused: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub dialogs: Vec<JavaScriptDialogInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct BlurParams {
|
||||||
|
pub session_id: String,
|
||||||
|
/// Optional `@e<N>` ref allocated by the last observation.
|
||||||
|
/// Mutually exclusive with `selector`.
|
||||||
|
#[serde(
|
||||||
|
rename = "ref",
|
||||||
|
alias = "ref_",
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub ref_: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
/// Target tab. Defaults to the Agent Window's active tab.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tab_id: Option<i64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(range(min = 1))]
|
||||||
|
pub timeout_ms: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct BlurResult {
|
||||||
|
pub tab_id: i64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub used_ref: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub used_selector: Option<String>,
|
||||||
|
/// Whether the target held focus before the call.
|
||||||
|
pub was_focused: bool,
|
||||||
|
/// Always false for a successful blur; included for machine verification.
|
||||||
|
pub focused: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub dialogs: Vec<JavaScriptDialogInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// fill
|
// fill
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -325,6 +401,37 @@ mod tests {
|
|||||||
assert_eq!(round, p);
|
assert_eq!(round, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focus_params_serialise_ref_field_name() {
|
||||||
|
let p = FocusParams {
|
||||||
|
session_id: "abcd".into(),
|
||||||
|
ref_: Some("@e4".into()),
|
||||||
|
selector: None,
|
||||||
|
tab_id: Some(42),
|
||||||
|
timeout_ms: Some(5_000),
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&p).unwrap();
|
||||||
|
assert_eq!(v.get("ref").and_then(|v| v.as_str()), Some("@e4"));
|
||||||
|
assert!(v.get("ref_").is_none());
|
||||||
|
let round: FocusParams = serde_json::from_value(v).unwrap();
|
||||||
|
assert_eq!(round, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blur_result_round_trips_focus_state() {
|
||||||
|
let result = BlurResult {
|
||||||
|
tab_id: 42,
|
||||||
|
used_ref: Some("e4".into()),
|
||||||
|
used_selector: None,
|
||||||
|
was_focused: true,
|
||||||
|
focused: false,
|
||||||
|
dialogs: vec![],
|
||||||
|
};
|
||||||
|
let value = serde_json::to_value(&result).unwrap();
|
||||||
|
let round: BlurResult = serde_json::from_value(value).unwrap();
|
||||||
|
assert_eq!(round, result);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modifiers_render_as_lowercase_strings() {
|
fn modifiers_render_as_lowercase_strings() {
|
||||||
let v = serde_json::to_value(KeyModifier::Ctrl).unwrap();
|
let v = serde_json::to_value(KeyModifier::Ctrl).unwrap();
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ bsk navigate <url> --session <id>
|
|||||||
bsk observe --session <id> → primary semantic VOM view; reveals hover/focus surfaces
|
bsk observe --session <id> → primary semantic VOM view; reveals hover/focus surfaces
|
||||||
bsk snapshot --session <id> → static aria tree fallback when VOM is insufficient
|
bsk snapshot --session <id> → static aria tree fallback when VOM is insufficient
|
||||||
bsk hover @e3 --session <id> → reveal hover-triggered menus before re-observing/clicking
|
bsk hover @e3 --session <id> → reveal hover-triggered menus before re-observing/clicking
|
||||||
|
bsk focus @e4 --session <id> → explicitly enter focus-driven UI states
|
||||||
|
bsk blur @e4 --session <id> → explicitly leave focus-driven UI states
|
||||||
bsk click @e4 --session <id> → or bsk fill, bsk select, bsk press
|
bsk click @e4 --session <id> → or bsk fill, bsk select, bsk press
|
||||||
bsk observe --session <id> → again after navigation / DOM change
|
bsk observe --session <id> → again after navigation / DOM change
|
||||||
```
|
```
|
||||||
@@ -202,6 +204,8 @@ Both capture from the moment the tab is attached and read a bounded per-tab buff
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `bsk click <ref-or-selector>` | Click element (`--button`, `--click-count`, `--modifiers`) |
|
| `bsk click <ref-or-selector>` | Click element (`--button`, `--click-count`, `--modifiers`) |
|
||||||
| `bsk hover <ref-or-selector>` | Move the mouse to an element and wait for hover UI to settle (`--settle`, `--modifiers`) |
|
| `bsk hover <ref-or-selector>` | Move the mouse to an element and wait for hover UI to settle (`--settle`, `--modifiers`) |
|
||||||
|
| `bsk focus <ref-or-selector>` | Focus an element and verify it became the deep active element |
|
||||||
|
| `bsk blur <ref-or-selector>` | Remove focus from an element and report whether it was focused |
|
||||||
| `bsk fill <ref-or-selector> --value <text>` | Clear and type into input |
|
| `bsk fill <ref-or-selector> --value <text>` | Clear and type into input |
|
||||||
| `bsk select <ref-or-selector> --value <v>` | Set `<select>` option(s) by `value` (repeat `--value` for multi-select) |
|
| `bsk select <ref-or-selector> --value <v>` | Set `<select>` option(s) by `value` (repeat `--value` for multi-select) |
|
||||||
| `bsk press <key>` | Key/combo (`Enter`, `Ctrl+A`, …; optional `--ref` to focus first) |
|
| `bsk press <key>` | Key/combo (`Enter`, `Ctrl+A`, …; optional `--ref` to focus first) |
|
||||||
|
|||||||
Reference in New Issue
Block a user