diff --git a/extensions/browser/src/cli/browser-cli-state.option-collisions.test.ts b/extensions/browser/src/cli/browser-cli-state.option-collisions.test.ts index 1f4889994abd..ac6034808bd7 100644 --- a/extensions/browser/src/cli/browser-cli-state.option-collisions.test.ts +++ b/extensions/browser/src/cli/browser-cli-state.option-collisions.test.ts @@ -172,6 +172,31 @@ describe("browser state option collisions", () => { expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1); }); + it("rejects invalid geolocation numbers before dispatch", async () => { + await runBrowserCommand(["set", "geo", "48.208", "16.373", "--accuracy", "fast"]); + + expect(mocks.callBrowserRequest).not.toHaveBeenCalled(); + expectErrorMessage("Invalid --accuracy: must be a finite number"); + expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1); + }); + + it("passes valid decimal geolocation numbers", async () => { + const request = await runBrowserCommandAndGetRequest([ + "set", + "geo", + "48.2082", + "16.3738", + "--accuracy", + "12.5", + ]); + + expect(request.body).toMatchObject({ + latitude: 48.2082, + longitude: 16.3738, + accuracy: 12.5, + }); + }); + it("errors when headers JSON is missing", async () => { await runBrowserCommand(["set", "headers"]); diff --git a/extensions/browser/src/cli/browser-cli-state.ts b/extensions/browser/src/cli/browser-cli-state.ts index 522062ff2b56..82dff79af9ee 100644 --- a/extensions/browser/src/cli/browser-cli-state.ts +++ b/extensions/browser/src/cli/browser-cli-state.ts @@ -25,6 +25,22 @@ function parsePositiveInteger(value: unknown, label: string): number | undefined return parsed; } +function parseFiniteNumberOption(value: string | undefined, label: string): number | undefined { + if (value === undefined) { + return undefined; + } + const raw = value.trim(); + const parsed = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(raw) + ? Number(raw) + : Number.NaN; + if (!Number.isFinite(parsed)) { + defaultRuntime.error(danger(`Invalid ${label}: must be a finite number`)); + defaultRuntime.exit(1); + return undefined; + } + return parsed; +} + function runBrowserCommand(action: () => Promise) { return runCommandWithRuntime(defaultRuntime, action, (err) => { defaultRuntime.error(danger(String(err))); @@ -189,27 +205,39 @@ export function registerBrowserStateCommands( .command("geo") .description("Set geolocation (and grant permission)") .option("--clear", "Clear geolocation + permissions", false) - .argument("[latitude]", "Latitude", (v: string) => Number(v)) - .argument("[longitude]", "Longitude", (v: string) => Number(v)) - .option("--accuracy ", "Accuracy in meters", (v: string) => Number(v)) + .argument("[latitude]", "Latitude") + .argument("[longitude]", "Longitude") + .option("--accuracy ", "Accuracy in meters") .option("--origin ", "Origin to grant permissions for") .option("--target-id ", "CDP target id (or unique prefix)") - .action(async (latitude: number | undefined, longitude: number | undefined, opts, cmd) => { - const parent = parentOpts(cmd); - await runBrowserSetRequest({ - parent, - path: "/set/geolocation", - body: { - latitude: Number.isFinite(latitude) ? latitude : undefined, - longitude: Number.isFinite(longitude) ? longitude : undefined, - accuracy: Number.isFinite(opts.accuracy) ? opts.accuracy : undefined, - origin: normalizeOptionalString(opts.origin), - clear: Boolean(opts.clear), - targetId: normalizeOptionalString(opts.targetId), - }, - successMessage: opts.clear ? "geolocation cleared" : "geolocation set", - }); - }); + .action( + async (latitudeRaw: string | undefined, longitudeRaw: string | undefined, opts, cmd) => { + const parent = parentOpts(cmd); + const latitude = parseFiniteNumberOption(latitudeRaw, "latitude"); + const longitude = parseFiniteNumberOption(longitudeRaw, "longitude"); + const accuracy = parseFiniteNumberOption(opts.accuracy, "--accuracy"); + if ( + (latitudeRaw !== undefined && latitude === undefined) || + (longitudeRaw !== undefined && longitude === undefined) || + (opts.accuracy !== undefined && accuracy === undefined) + ) { + return; + } + await runBrowserSetRequest({ + parent, + path: "/set/geolocation", + body: { + latitude, + longitude, + accuracy, + origin: normalizeOptionalString(opts.origin), + clear: Boolean(opts.clear), + targetId: normalizeOptionalString(opts.targetId), + }, + successMessage: opts.clear ? "geolocation cleared" : "geolocation set", + }); + }, + ); set .command("media")