mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(browser): unify fill field normalization
This commit is contained in:
32
src/browser/form-fields.ts
Normal file
32
src/browser/form-fields.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { BrowserFormField } from "./client-actions-core.js";
|
||||
|
||||
export const DEFAULT_FILL_FIELD_TYPE = "text";
|
||||
|
||||
type BrowserFormFieldValue = NonNullable<BrowserFormField["value"]>;
|
||||
|
||||
export function normalizeBrowserFormFieldRef(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export function normalizeBrowserFormFieldType(value: unknown): string {
|
||||
const type = typeof value === "string" ? value.trim() : "";
|
||||
return type || DEFAULT_FILL_FIELD_TYPE;
|
||||
}
|
||||
|
||||
export function normalizeBrowserFormFieldValue(value: unknown): BrowserFormFieldValue | undefined {
|
||||
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function normalizeBrowserFormField(
|
||||
record: Record<string, unknown>,
|
||||
): BrowserFormField | null {
|
||||
const ref = normalizeBrowserFormFieldRef(record.ref);
|
||||
if (!ref) {
|
||||
return null;
|
||||
}
|
||||
const type = normalizeBrowserFormFieldType(record.type);
|
||||
const value = normalizeBrowserFormFieldValue(record.value);
|
||||
return value === undefined ? { ref, type } : { ref, type, value };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BrowserFormField } from "./client-actions-core.js";
|
||||
import { DEFAULT_FILL_FIELD_TYPE } from "./form-fields.js";
|
||||
import { DEFAULT_UPLOAD_DIR, resolveStrictExistingPathsWithinRoot } from "./paths.js";
|
||||
import {
|
||||
ensurePageState,
|
||||
@@ -188,7 +189,7 @@ export async function fillFormViaPlaywright(opts: {
|
||||
const timeout = Math.max(500, Math.min(60_000, opts.timeoutMs ?? 8000));
|
||||
for (const field of opts.fields) {
|
||||
const ref = field.ref.trim();
|
||||
const type = field.type.trim();
|
||||
const type = (field.type || DEFAULT_FILL_FIELD_TYPE).trim() || DEFAULT_FILL_FIELD_TYPE;
|
||||
const rawValue = field.value;
|
||||
const value =
|
||||
typeof rawValue === "string"
|
||||
@@ -196,7 +197,7 @@ export async function fillFormViaPlaywright(opts: {
|
||||
: typeof rawValue === "number" || typeof rawValue === "boolean"
|
||||
? String(rawValue)
|
||||
: "";
|
||||
if (!ref || !type) {
|
||||
if (!ref) {
|
||||
continue;
|
||||
}
|
||||
const locator = refLocator(page, ref);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BrowserFormField } from "../client-actions-core.js";
|
||||
import { normalizeBrowserFormField } from "../form-fields.js";
|
||||
import type { BrowserRouteContext } from "../server-context.js";
|
||||
import { registerBrowserAgentActDownloadRoutes } from "./agent.act.download.js";
|
||||
import { registerBrowserAgentActHookRoutes } from "./agent.act.hooks.js";
|
||||
@@ -190,21 +191,7 @@ export function registerBrowserAgentActRoutes(
|
||||
if (!field || typeof field !== "object") {
|
||||
return null;
|
||||
}
|
||||
const rec = field as Record<string, unknown>;
|
||||
const ref = toStringOrEmpty(rec.ref);
|
||||
const type = toStringOrEmpty(rec.type) || "text";
|
||||
if (!ref) {
|
||||
return null;
|
||||
}
|
||||
const value =
|
||||
typeof rec.value === "string" ||
|
||||
typeof rec.value === "number" ||
|
||||
typeof rec.value === "boolean"
|
||||
? rec.value
|
||||
: undefined;
|
||||
const parsed: BrowserFormField =
|
||||
value === undefined ? { ref, type } : { ref, type, value };
|
||||
return parsed;
|
||||
return normalizeBrowserFormField(field as Record<string, unknown>);
|
||||
})
|
||||
.filter((field): field is BrowserFormField => field !== null);
|
||||
if (!fields.length) {
|
||||
|
||||
@@ -58,27 +58,35 @@ describe("browser control server", () => {
|
||||
values: ["a", "b"],
|
||||
});
|
||||
|
||||
const fill = await postJson<{ ok: boolean }>(`${base}/act`, {
|
||||
kind: "fill",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
});
|
||||
expect(fill.ok).toBe(true);
|
||||
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: state.cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
});
|
||||
|
||||
const fillWithoutType = await postJson<{ ok: boolean }>(`${base}/act`, {
|
||||
kind: "fill",
|
||||
fields: [{ ref: "7", value: "world" }],
|
||||
});
|
||||
expect(fillWithoutType.ok).toBe(true);
|
||||
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: state.cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fields: [{ ref: "7", type: "text", value: "world" }],
|
||||
});
|
||||
const fillCases: Array<{
|
||||
input: Record<string, unknown>;
|
||||
expected: Record<string, unknown>;
|
||||
}> = [
|
||||
{
|
||||
input: { ref: "6", type: "textbox", value: "hello" },
|
||||
expected: { ref: "6", type: "textbox", value: "hello" },
|
||||
},
|
||||
{
|
||||
input: { ref: "7", value: "world" },
|
||||
expected: { ref: "7", type: "text", value: "world" },
|
||||
},
|
||||
{
|
||||
input: { ref: "8", type: " ", value: "trimmed-default" },
|
||||
expected: { ref: "8", type: "text", value: "trimmed-default" },
|
||||
},
|
||||
];
|
||||
for (const { input, expected } of fillCases) {
|
||||
const fill = await postJson<{ ok: boolean }>(`${base}/act`, {
|
||||
kind: "fill",
|
||||
fields: [input],
|
||||
});
|
||||
expect(fill.ok).toBe(true);
|
||||
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: state.cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fields: [expected],
|
||||
});
|
||||
}
|
||||
|
||||
const resize = await postJson<{ ok: boolean }>(`${base}/act`, {
|
||||
kind: "resize",
|
||||
|
||||
@@ -2,10 +2,24 @@ import { describe, expect, it } from "vitest";
|
||||
import { readFields } from "./shared.js";
|
||||
|
||||
describe("readFields", () => {
|
||||
it("defaults missing type to text", async () => {
|
||||
await expect(readFields({ fields: '[{"ref":"7","value":"world"}]' })).resolves.toEqual([
|
||||
{ ref: "7", type: "text", value: "world" },
|
||||
]);
|
||||
it.each([
|
||||
{
|
||||
name: "keeps explicit type",
|
||||
fields: '[{"ref":"6","type":"textbox","value":"hello"}]',
|
||||
expected: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
},
|
||||
{
|
||||
name: "defaults missing type to text",
|
||||
fields: '[{"ref":"7","value":"world"}]',
|
||||
expected: [{ ref: "7", type: "text", value: "world" }],
|
||||
},
|
||||
{
|
||||
name: "defaults blank type to text",
|
||||
fields: '[{"ref":"8","type":" ","value":"blank"}]',
|
||||
expected: [{ ref: "8", type: "text", value: "blank" }],
|
||||
},
|
||||
])("$name", async ({ fields, expected }) => {
|
||||
await expect(readFields({ fields })).resolves.toEqual(expected);
|
||||
});
|
||||
|
||||
it("requires ref", async () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Command } from "commander";
|
||||
import type { BrowserFormField } from "../../browser/client-actions-core.js";
|
||||
import {
|
||||
normalizeBrowserFormField,
|
||||
normalizeBrowserFormFieldValue,
|
||||
} from "../../browser/form-fields.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
@@ -68,21 +72,16 @@ export async function readFields(opts: {
|
||||
throw new Error(`fields[${index}] must be an object`);
|
||||
}
|
||||
const rec = entry as Record<string, unknown>;
|
||||
const ref = typeof rec.ref === "string" ? rec.ref.trim() : "";
|
||||
const type = typeof rec.type === "string" ? rec.type.trim() : "";
|
||||
if (!ref) {
|
||||
const parsedField = normalizeBrowserFormField(rec);
|
||||
if (!parsedField) {
|
||||
throw new Error(`fields[${index}] must include ref`);
|
||||
}
|
||||
const resolvedType = type || "text";
|
||||
if (
|
||||
typeof rec.value === "string" ||
|
||||
typeof rec.value === "number" ||
|
||||
typeof rec.value === "boolean"
|
||||
rec.value === undefined ||
|
||||
rec.value === null ||
|
||||
normalizeBrowserFormFieldValue(rec.value) !== undefined
|
||||
) {
|
||||
return { ref, type: resolvedType, value: rec.value };
|
||||
}
|
||||
if (rec.value === undefined || rec.value === null) {
|
||||
return { ref, type: resolvedType };
|
||||
return parsedField;
|
||||
}
|
||||
throw new Error(`fields[${index}].value must be string, number, boolean, or null`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user