fix: validate browser snapshot numbers

This commit is contained in:
Peter Steinberger
2026-05-28 15:32:19 -04:00
parent f99259d25c
commit 503d8d5542
2 changed files with 50 additions and 4 deletions

View File

@@ -159,6 +159,26 @@ describe("browser cli snapshot defaults", () => {
expect(params?.query?.urls).toBe(true);
});
it("rejects non-integer snapshot numeric options before dispatch", async () => {
await expect(runSnapshot(["--limit", "1e3"])).rejects.toThrow("__exit__:1");
expect(runtime.error.mock.calls.at(-1)?.[0]).toContain(
"Invalid --limit: must be an integer >= 1",
);
resetRuntimeCapture();
await expect(runSnapshot(["--depth", "-1"])).rejects.toThrow("__exit__:1");
expect(runtime.error.mock.calls.at(-1)?.[0]).toContain(
"Invalid --depth: must be an integer >= 0",
);
expect(sharedMocks.callBrowserRequest).not.toHaveBeenCalled();
});
it("passes zero snapshot depth because root depth is valid", async () => {
const params = await runSnapshot(["--depth", "0"]);
expect(params?.query?.depth).toBe(0);
});
it("sends screenshot request with trimmed target id and jpeg type", async () => {
const params = await runBrowserInspect(["screenshot", " tab-1 ", "--type", "jpeg"], true);
expect(params?.path).toBe("/screenshot");

View File

@@ -10,6 +10,24 @@ import {
type SnapshotResult,
} from "./core-api.js";
function parseOptionalIntegerOption(
value: string | undefined,
label: string,
opts: { min: number },
): number | undefined {
if (value === undefined) {
return undefined;
}
const raw = value.trim();
const parsed = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
if (!Number.isSafeInteger(parsed) || parsed < opts.min) {
defaultRuntime.error(danger(`Invalid ${label}: must be an integer >= ${opts.min}`));
defaultRuntime.exit(1);
return undefined;
}
return parsed;
}
export function registerBrowserInspectCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
@@ -60,12 +78,12 @@ export function registerBrowserInspectCommands(
.description("Capture a snapshot (default: ai; aria is the accessibility tree)")
.option("--format <aria|ai>", "Snapshot format (default: ai)", "ai")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.option("--limit <n>", "Max nodes (default: 500/800)", (v: string) => Number(v))
.option("--limit <n>", "Max nodes (default: 500/800)")
.option("--mode <efficient>", "Snapshot preset (efficient)")
.option("--efficient", "Use the efficient snapshot preset", false)
.option("--interactive", "Role snapshot: interactive elements only", false)
.option("--compact", "Role snapshot: compact output", false)
.option("--depth <n>", "Role snapshot: max depth", (v: string) => Number(v))
.option("--depth <n>", "Role snapshot: max depth")
.option("--selector <sel>", "Role snapshot: scope to CSS selector")
.option("--frame <sel>", "Role snapshot: scope to an iframe selector")
.option("--labels", "Include viewport label overlay screenshot", false)
@@ -85,14 +103,22 @@ export function registerBrowserInspectCommands(
? "efficient"
: undefined;
const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode;
const limit = parseOptionalIntegerOption(opts.limit, "--limit", { min: 1 });
const depth = parseOptionalIntegerOption(opts.depth, "--depth", { min: 0 });
if (
(opts.limit !== undefined && limit === undefined) ||
(opts.depth !== undefined && depth === undefined)
) {
return;
}
try {
const query: Record<string, string | number | boolean | undefined> = {
format,
targetId: normalizeOptionalString(opts.targetId),
limit: Number.isFinite(opts.limit) ? opts.limit : undefined,
limit,
interactive: opts.interactive ? true : undefined,
compact: opts.compact ? true : undefined,
depth: Number.isFinite(opts.depth) ? opts.depth : undefined,
depth,
selector: normalizeOptionalString(opts.selector),
frame: normalizeOptionalString(opts.frame),
labels: opts.labels ? true : undefined,