mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
fix(cli): pass positional values starting with - through to tools (#3084)
`chrome-devtools fill 1 <uid> -` reported success but cleared the field, `type_text 1 -` typed an empty string, and values like `--weird` could not be passed at all, not even after `--`. yargs re-parses command positionals as `--name value` pairs. In that pass yargs-parser treats any value starting with `-` (other than a negative number) as the next flag and substitutes the string default `''`. yargs also never fills positionals from the tokens after `--`. Neither can be fixed via configuration (`unknown-options-as-args` and `nargs` hit the same re-parse), and the upstream fix for the lone `-` (yargs/yargs-parser#472) is unreleased and only covers that one case. The CLI now shields such tokens before yargs sees them: a lone `-` is always shielded, and everything after the first `--` is shielded with the separator dropped. Shielded tokens are replaced by opaque placeholders and restored when building the tool arguments. Unknown flags such as `--weird` or `--pageid` without `--` are still rejected by strict mode; the error tip and docs/cli.md now explain the `--` form. Related upstream issues: - https://github.com/yargs/yargs/issues/1312 - https://github.com/yargs/yargs/issues/1821 - https://github.com/yargs/yargs-parser/pull/472 Fixes #2984
This commit is contained in:
1 parent
e1703847fa
commit
9de05ae6e5
7 files changed
+720
-30
No files matched your search
+14
-2
@@ -23,11 +23,19 @@ const OUTPUT_PATH = path.join(
|
||||
'../src/config/cli-options.ts',
|
||||
);
|
||||
|
||||
/**
|
||||
* Optional args that intentionally remain positional for backwards-compatible
|
||||
* CLI syntax. Every required arg is positional; every other optional arg is a
|
||||
* flag.
|
||||
*/
|
||||
const OPTIONAL_POSITIONAL_ARGS = new Set(['evaluate_script:function']);
|
||||
|
||||
interface CliOption {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
positional: boolean;
|
||||
default?: unknown;
|
||||
enum?: unknown[];
|
||||
}
|
||||
@@ -41,7 +49,7 @@ interface JsonSchema {
|
||||
enum?: unknown[];
|
||||
}
|
||||
|
||||
function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
|
||||
function schemaToCLIOptions(toolName: string, schema: JsonSchema): CliOption[] {
|
||||
if (!schema || !schema.properties) {
|
||||
return [];
|
||||
}
|
||||
@@ -60,6 +68,8 @@ function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
|
||||
type: prop.type,
|
||||
description,
|
||||
required: isRequired,
|
||||
positional:
|
||||
isRequired || OPTIONAL_POSITIONAL_ARGS.has(`${toolName}:${name}`),
|
||||
default: prop.default,
|
||||
enum: prop.enum,
|
||||
};
|
||||
@@ -109,7 +119,7 @@ async function generateCli() {
|
||||
const inputSchema = zod.toJSONSchema(zod.object(tool.schema), {
|
||||
io: 'input',
|
||||
}) as JsonSchema;
|
||||
const options = schemaToCLIOptions(inputSchema);
|
||||
const options = schemaToCLIOptions(tool.name, inputSchema);
|
||||
const args: Record<string, CliOption> = {};
|
||||
for (const opt of options) {
|
||||
args[opt.name] = opt;
|
||||
@@ -171,6 +181,8 @@ export interface ArgDef {
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
/** Whether the arg is a positional on the command line, otherwise a flag. */
|
||||
positional: boolean;
|
||||
default?: string | number | boolean;
|
||||
enum?: ReadonlyArray<string | number>;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ import {hideBin, yargs, type CallToolResult} from '../third_party/index.js';
|
||||
import {checkForUpdates} from '../utils/check-for-updates.js';
|
||||
import {VERSION} from '../version.js';
|
||||
|
||||
import {buildCommand, isOptionalPositionalArg} from '../config/cli-commands.js';
|
||||
import {
|
||||
buildCommand,
|
||||
getCommandArgSpec,
|
||||
shieldLiteralArgs,
|
||||
} from '../config/cli-commands.js';
|
||||
import {commands} from '../config/cli-options.js';
|
||||
import {mcpOptions, getCliOptions} from '../config/mcp-options.js';
|
||||
|
||||
@@ -61,7 +65,15 @@ async function start(
|
||||
logDisclaimers(parsedArgs);
|
||||
}
|
||||
|
||||
const y = yargs(hideBin(process.argv))
|
||||
const {argv: cliArgs, restore: restoreLiteralArg} = shieldLiteralArgs(
|
||||
hideBin(process.argv),
|
||||
commandName => {
|
||||
const commandDef = commands[commandName];
|
||||
return commandDef ? getCommandArgSpec(commandDef.args) : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const y = yargs(cliArgs)
|
||||
.locale('en') // Force English to ensure error string matching works in .fail, all custom messages we output are in English anyways
|
||||
.scriptName('chrome-devtools')
|
||||
.showHelpOnFail(true)
|
||||
@@ -212,7 +224,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
|
||||
? 'array'
|
||||
: 'string';
|
||||
|
||||
if (opt.required || isOptionalPositionalArg(commandName, argName)) {
|
||||
if (opt.positional) {
|
||||
const options: PositionalOptions = {
|
||||
describe: opt.description,
|
||||
type: type as PositionalOptions['type'],
|
||||
@@ -255,7 +267,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
|
||||
const commandArgs: Record<string, unknown> = {};
|
||||
for (const argName of Object.keys(args)) {
|
||||
if (argName in argv) {
|
||||
commandArgs[argName] = argv[argName];
|
||||
commandArgs[argName] = restoreLiteralArg(argv[argName]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+193
-16
@@ -6,22 +6,13 @@
|
||||
|
||||
import type {ArgDef} from './cli-options.js';
|
||||
|
||||
const OPTIONAL_POSITIONAL_ARGS = new Set(['evaluate_script:function']);
|
||||
|
||||
export function isOptionalPositionalArg(
|
||||
commandName: string,
|
||||
argName: string,
|
||||
): boolean {
|
||||
return OPTIONAL_POSITIONAL_ARGS.has(`${commandName}:${argName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the yargs command string and usage line for a CLI command.
|
||||
*
|
||||
* Optional flag args are rendered as `[--flag]` in the usage line only: as
|
||||
* part of the command string yargs parses each one as a trailing positional,
|
||||
* and a variadic positional has to be the last one. A small set of optional
|
||||
* args intentionally remain positional for backwards-compatible CLI syntax.
|
||||
* and a variadic positional has to be the last one. Which args are positional
|
||||
* is decided at build time by `scripts/generate-cli.ts`.
|
||||
*/
|
||||
export function buildCommand(
|
||||
commandName: string,
|
||||
@@ -30,14 +21,200 @@ export function buildCommand(
|
||||
let command = commandName;
|
||||
let flags = '';
|
||||
for (const [name, arg] of Object.entries(args)) {
|
||||
if (arg.required) {
|
||||
command += arg.type === 'array' ? ` <${name}..>` : ` <${name}>`;
|
||||
} else if (isOptionalPositionalArg(commandName, name)) {
|
||||
command += arg.type === 'array' ? ` [${name}..]` : ` [${name}]`;
|
||||
} else {
|
||||
if (!arg.positional) {
|
||||
flags += ` [--${name}]`;
|
||||
} else if (arg.required) {
|
||||
command += arg.type === 'array' ? ` <${name}..>` : ` <${name}>`;
|
||||
} else {
|
||||
command += arg.type === 'array' ? ` [${name}..]` : ` [${name}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return {command, usage: `$0 ${command}${flags}`};
|
||||
}
|
||||
|
||||
export type OptionKind = 'boolean' | 'array' | 'value';
|
||||
|
||||
/**
|
||||
* Options that are available on every tool command and are consumed by yargs
|
||||
* rather than forwarded to the tool.
|
||||
*/
|
||||
export const GLOBAL_CLI_OPTIONS: ReadonlyMap<string, OptionKind> = new Map([
|
||||
['sessionId', 'value'],
|
||||
['outputFormat', 'value'],
|
||||
['help', 'boolean'],
|
||||
['version', 'boolean'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Describes what a CLI command accepts, as needed by `shieldLiteralArgs`.
|
||||
*/
|
||||
export interface CommandArgSpec {
|
||||
/** Known option names (camelCase and aliases) and how they take values. */
|
||||
options: Map<string, OptionKind>;
|
||||
/** Number of positional arguments. */
|
||||
positionalCount: number;
|
||||
/** Whether the last positional is variadic. */
|
||||
variadic: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a `CommandArgSpec` for a tool command from its generated arg
|
||||
* definitions and the options available on every command.
|
||||
*/
|
||||
export function getCommandArgSpec(
|
||||
args: Record<string, ArgDef>,
|
||||
globalOptions: ReadonlyMap<string, OptionKind> = GLOBAL_CLI_OPTIONS,
|
||||
): CommandArgSpec {
|
||||
const options = new Map(globalOptions);
|
||||
let positionalCount = 0;
|
||||
let variadic = false;
|
||||
for (const [name, arg] of Object.entries(args)) {
|
||||
if (arg.positional) {
|
||||
positionalCount++;
|
||||
variadic = arg.type === 'array';
|
||||
} else {
|
||||
options.set(name, toOptionKind(arg.type));
|
||||
}
|
||||
}
|
||||
return {options, positionalCount, variadic};
|
||||
}
|
||||
|
||||
export function toOptionKind(type: string | undefined): OptionKind {
|
||||
if (type === 'boolean') {
|
||||
return 'boolean';
|
||||
}
|
||||
if (type === 'array') {
|
||||
return 'array';
|
||||
}
|
||||
return 'value';
|
||||
}
|
||||
|
||||
const LITERAL_ARG_PREFIX = '\0literal:';
|
||||
// Mirrors yargs-parser's negative number detection.
|
||||
const NEGATIVE_NUMBER = /^-\d+(\.\d*)?(e-?\d+)?$/;
|
||||
|
||||
function optionName(token: string): string {
|
||||
let name = token.replace(/^-+/, '');
|
||||
const eq = name.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
name = name.slice(0, eq);
|
||||
}
|
||||
if (name.startsWith('no-')) {
|
||||
name = name.slice(3);
|
||||
}
|
||||
return name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shields positional values that yargs cannot carry through its parsing.
|
||||
*
|
||||
* yargs re-parses command positionals as `--name value` pairs, and in that
|
||||
* pass yargs-parser treats any value starting with `-` (other than a negative
|
||||
* number) as the next flag, replacing the value with `''`. It also never fills
|
||||
* positionals from the tokens after `--`. See
|
||||
* https://github.com/yargs/yargs/issues/1312 and
|
||||
* https://github.com/yargs/yargs/issues/1821.
|
||||
*
|
||||
* Shielded tokens are replaced by opaque placeholders that `restore` maps back
|
||||
* to the original values. A token is shielded when:
|
||||
*
|
||||
* - it is a lone `-`, which can never be an option;
|
||||
* - it follows the first `--`, which is dropped so the tokens populate
|
||||
* positionals;
|
||||
* - it starts with `-`, is not a known option of the command, and the command
|
||||
* still has an unfilled positional. Known options and their values are
|
||||
* skipped the way yargs consumes them, so a dash-prefixed value such as
|
||||
* `--flag` in a positional slot is passed through as text, while an unknown
|
||||
* flag after all positionals are filled is still rejected by yargs.
|
||||
*
|
||||
* `getSpec` resolves the spec of a command by name and returns `undefined`
|
||||
* for tokens that are not a command.
|
||||
*/
|
||||
export function shieldLiteralArgs(
|
||||
argv: readonly string[],
|
||||
getSpec: (commandName: string) => CommandArgSpec | undefined,
|
||||
): {
|
||||
argv: string[];
|
||||
restore: (value: unknown) => unknown;
|
||||
} {
|
||||
const literals = new Map<string, string>();
|
||||
const result: string[] = [];
|
||||
const shield = (token: string): void => {
|
||||
const placeholder = `${LITERAL_ARG_PREFIX}${literals.size}`;
|
||||
literals.set(placeholder, token);
|
||||
result.push(placeholder);
|
||||
};
|
||||
|
||||
let spec: CommandArgSpec | undefined;
|
||||
let filled = 0;
|
||||
let afterDoubleDash = false;
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const token = argv[i];
|
||||
if (afterDoubleDash) {
|
||||
shield(token);
|
||||
continue;
|
||||
}
|
||||
if (token === '--') {
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
if (token === '-') {
|
||||
shield(token);
|
||||
filled++;
|
||||
continue;
|
||||
}
|
||||
if (!token.startsWith('-') || NEGATIVE_NUMBER.test(token)) {
|
||||
result.push(token);
|
||||
if (!spec) {
|
||||
spec = getSpec(token);
|
||||
} else {
|
||||
filled++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const kind = spec?.options.get(optionName(token));
|
||||
if (kind !== undefined) {
|
||||
result.push(token);
|
||||
if (token.includes('=')) {
|
||||
continue;
|
||||
}
|
||||
if (kind === 'boolean') {
|
||||
if (/^(true|false)$/.test(argv[i + 1] ?? '')) {
|
||||
result.push(argv[++i]);
|
||||
}
|
||||
} else if (kind === 'array') {
|
||||
while (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
||||
result.push(argv[++i]);
|
||||
}
|
||||
} else if (i + 1 < argv.length) {
|
||||
result.push(argv[++i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasUnfilledPositional =
|
||||
spec !== undefined &&
|
||||
(filled < spec.positionalCount ||
|
||||
(spec.variadic && spec.positionalCount > 0));
|
||||
if (hasUnfilledPositional) {
|
||||
shield(token);
|
||||
filled++;
|
||||
} else {
|
||||
result.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
const restore = (value: unknown): unknown => {
|
||||
if (typeof value === 'string') {
|
||||
return literals.get(value) ?? value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(restore);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return {argv: result, restore};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+141
-7
@@ -14,7 +14,11 @@ import {
|
||||
mcpOptions,
|
||||
DEFAULT_FILESYSTEM_ROOT,
|
||||
} from '../src/config/mcp-options.js';
|
||||
import {buildCommand} from '../src/config/cli-commands.js';
|
||||
import {
|
||||
buildCommand,
|
||||
getCommandArgSpec,
|
||||
shieldLiteralArgs,
|
||||
} from '../src/config/cli-commands.js';
|
||||
import {commands} from '../src/config/cli-options.js';
|
||||
import {computeFlagUsage} from '../src/telemetry/flagUtils.js';
|
||||
import {puppeteerOptions} from '../src/config/puppeteer-options.js';
|
||||
@@ -1264,19 +1268,26 @@ describe('cli args parsing', () => {
|
||||
});
|
||||
|
||||
describe('cli command strings', () => {
|
||||
const required = {description: '', required: true, positional: true};
|
||||
const dummyArgsVariadic = {
|
||||
arg1: {name: 'arg1', type: 'string', description: '', required: true},
|
||||
arrArg: {name: 'arrArg', type: 'array', description: '', required: true},
|
||||
arg1: {name: 'arg1', type: 'string', ...required},
|
||||
arrArg: {name: 'arrArg', type: 'array', ...required},
|
||||
};
|
||||
|
||||
const dummyArgsPlain = {
|
||||
arg1: {name: 'arg1', type: 'string', description: '', required: true},
|
||||
arg2: {name: 'arg2', type: 'string', description: '', required: true},
|
||||
arg1: {name: 'arg1', type: 'string', ...required},
|
||||
arg2: {name: 'arg2', type: 'string', ...required},
|
||||
};
|
||||
|
||||
const dummyArgsOptional = {
|
||||
arg1: {name: 'arg1', type: 'string', description: '', required: true},
|
||||
optArg: {name: 'optArg', type: 'boolean', description: '', required: false},
|
||||
arg1: {name: 'arg1', type: 'string', ...required},
|
||||
optArg: {
|
||||
name: 'optArg',
|
||||
type: 'boolean',
|
||||
description: '',
|
||||
required: false,
|
||||
positional: false,
|
||||
},
|
||||
};
|
||||
|
||||
it('renders a required array arg as a variadic positional', () => {
|
||||
@@ -1336,6 +1347,129 @@ describe('cli command strings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cli literal args', () => {
|
||||
const getSpec = (name: string) =>
|
||||
name in commands ? getCommandArgSpec(commands[name].args) : undefined;
|
||||
|
||||
function shield(argv: string[]) {
|
||||
const {argv: shielded, restore} = shieldLiteralArgs(argv, getSpec);
|
||||
return {
|
||||
shielded,
|
||||
restored: shielded.map(restore),
|
||||
literalIndexes: shielded.flatMap((t, i) => (t !== argv[i] ? [i] : [])),
|
||||
};
|
||||
}
|
||||
|
||||
it('passes regular args through unchanged', () => {
|
||||
const input = ['fill', '1', '1_1', 'a-b', '-5', '--output-format', 'json'];
|
||||
assert.deepStrictEqual(shield(input).shielded, input);
|
||||
});
|
||||
|
||||
it('shields a lone dash', () => {
|
||||
const {shielded, restored} = shield(['fill', '1', '1_1', '-']);
|
||||
assert.ok(!shielded[3].startsWith('-'));
|
||||
assert.deepStrictEqual(restored, ['fill', '1', '1_1', '-']);
|
||||
});
|
||||
|
||||
it('shields unknown dash tokens in unfilled positional slots', () => {
|
||||
for (const value of ['--weird', '-abc', '- item', '--pageId=2']) {
|
||||
const {shielded, restored} = shield(['fill', '1', '1_1', value]);
|
||||
assert.ok(!shielded[3].startsWith('-'), value);
|
||||
assert.deepStrictEqual(restored, ['fill', '1', '1_1', value]);
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves unknown dash tokens alone once positionals are filled', () => {
|
||||
const input = ['fill', '1', '1_1', 'x', '--pageid', '2'];
|
||||
assert.deepStrictEqual(shield(input).shielded, input);
|
||||
});
|
||||
|
||||
it('skips known options and their values', () => {
|
||||
const input = [
|
||||
'--sessionId',
|
||||
'abc',
|
||||
'click',
|
||||
'--dblClick',
|
||||
'true',
|
||||
'--output-format=json',
|
||||
'1',
|
||||
'1_1',
|
||||
'--help',
|
||||
];
|
||||
assert.deepStrictEqual(shield(input).shielded, input);
|
||||
|
||||
const {restored, literalIndexes} = shield([
|
||||
'evaluate_script',
|
||||
'--pageId',
|
||||
'1',
|
||||
'--args',
|
||||
'a',
|
||||
'b',
|
||||
'--weird',
|
||||
]);
|
||||
assert.deepStrictEqual(literalIndexes, [6]);
|
||||
assert.strictEqual(restored[6], '--weird');
|
||||
});
|
||||
|
||||
it('accepts kebab-case and negated forms of known options', () => {
|
||||
const input = ['click', '1', '1_1', '--dbl-click', '--no-dblClick'];
|
||||
assert.deepStrictEqual(shield(input).shielded, input);
|
||||
});
|
||||
|
||||
it('fills a variadic positional with any dash tokens', () => {
|
||||
const {restored, literalIndexes} = shield([
|
||||
'upload_file',
|
||||
'1',
|
||||
'1_1',
|
||||
'-',
|
||||
'--a',
|
||||
'-b',
|
||||
]);
|
||||
assert.deepStrictEqual(literalIndexes, [3, 4, 5]);
|
||||
assert.deepStrictEqual(restored, [
|
||||
'upload_file',
|
||||
'1',
|
||||
'1_1',
|
||||
'-',
|
||||
'--a',
|
||||
'-b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shields every token after -- and drops the separator', () => {
|
||||
const {argv, restore} = shieldLiteralArgs(
|
||||
['fill', '1', '--', '--weird', '- item', '--', '-abc'],
|
||||
getSpec,
|
||||
);
|
||||
assert.strictEqual(argv.length, 6);
|
||||
assert.deepStrictEqual(argv.slice(0, 2), ['fill', '1']);
|
||||
for (const token of argv.slice(2)) {
|
||||
assert.ok(!token.startsWith('-'), `unexpected token ${token}`);
|
||||
}
|
||||
assert.deepStrictEqual(argv.slice(2).map(restore), [
|
||||
'--weird',
|
||||
'- item',
|
||||
'--',
|
||||
'-abc',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not shield unknown flags of non-tool commands', () => {
|
||||
const input = ['start', '--weird', 'status', '-x'];
|
||||
assert.deepStrictEqual(shield(input).shielded, input);
|
||||
});
|
||||
|
||||
it('restores values nested in arrays and leaves other values alone', () => {
|
||||
const {argv, restore} = shieldLiteralArgs(
|
||||
['cmd', '--', '-a', '-b'],
|
||||
getSpec,
|
||||
);
|
||||
assert.deepStrictEqual(restore([argv[1], argv[2], 'x']), ['-a', '-b', 'x']);
|
||||
assert.strictEqual(restore(42), 42);
|
||||
assert.strictEqual(restore(undefined), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flag usage telemetry', () => {
|
||||
it('reports the stable channel for a default launch', async () => {
|
||||
const usage = computeFlagUsage(parseConfig([]), mcpOptions);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import crypto from 'node:crypto';
|
||||
import {describe, it, afterEach, beforeEach} from 'node:test';
|
||||
|
||||
import {assertDaemonIsNotRunning, runCli} from '../utils.js';
|
||||
|
||||
describe('chrome-devtools positional values starting with a hyphen', () => {
|
||||
let sessionId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
sessionId = crypto.randomUUID();
|
||||
await runCli(['stop'], sessionId);
|
||||
await assertDaemonIsNotRunning(sessionId);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await runCli(['stop'], sessionId);
|
||||
await assertDaemonIsNotRunning(sessionId);
|
||||
});
|
||||
|
||||
async function startDaemon(): Promise<void> {
|
||||
const startResult = await runCli(['start'], sessionId);
|
||||
assert.strictEqual(
|
||||
startResult.status,
|
||||
0,
|
||||
`start command failed: ${startResult.stderr}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Starts the daemon, opens a page with a textarea and returns its uid. */
|
||||
async function startWithTextarea(): Promise<string> {
|
||||
await startDaemon();
|
||||
|
||||
const navigateResult = await runCli(
|
||||
[
|
||||
'navigate_page',
|
||||
'1',
|
||||
'--url',
|
||||
'data:text/html,<textarea id="t">x</textarea>',
|
||||
],
|
||||
sessionId,
|
||||
);
|
||||
assert.strictEqual(
|
||||
navigateResult.status,
|
||||
0,
|
||||
`navigate_page command failed: ${navigateResult.stderr}`,
|
||||
);
|
||||
|
||||
const snapshotResult = await runCli(['take_snapshot', '1'], sessionId);
|
||||
assert.strictEqual(
|
||||
snapshotResult.status,
|
||||
0,
|
||||
`take_snapshot command failed: ${snapshotResult.stderr}`,
|
||||
);
|
||||
const uidMatch = snapshotResult.stdout.match(/uid=(\S+) textbox/);
|
||||
assert.ok(
|
||||
uidMatch,
|
||||
'textarea uid not found in snapshot: ' + snapshotResult.stdout,
|
||||
);
|
||||
return uidMatch[1];
|
||||
}
|
||||
|
||||
async function readTextareaValue(): Promise<string> {
|
||||
const result = await runCli(
|
||||
[
|
||||
'evaluate_script',
|
||||
'() => document.getElementById("t").value',
|
||||
'--pageId',
|
||||
'1',
|
||||
],
|
||||
sessionId,
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`evaluate_script command failed: ${result.stderr}`,
|
||||
);
|
||||
const match = result.stdout.match(/"(.*)"/);
|
||||
assert.ok(match, 'unexpected evaluate_script output: ' + result.stdout);
|
||||
return JSON.parse(`"${match[1]}"`);
|
||||
}
|
||||
|
||||
const fillCases: Array<{value: string[]; expected: string}> = [
|
||||
{value: ['-'], expected: '-'},
|
||||
{value: ['-5'], expected: '-5'},
|
||||
{value: ['a-b'], expected: 'a-b'},
|
||||
{value: ['--weird'], expected: '--weird'},
|
||||
{value: ['- item'], expected: '- item'},
|
||||
{value: ['-abc'], expected: '-abc'},
|
||||
{value: ['--', '--weird'], expected: '--weird'},
|
||||
];
|
||||
for (const {value, expected} of fillCases) {
|
||||
it(`fill passes ${JSON.stringify(value.join(' '))} through`, async () => {
|
||||
const uid = await startWithTextarea();
|
||||
|
||||
const args = ['fill', '1', uid, ...value];
|
||||
const result = await runCli(args, sessionId);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`${args.join(' ')} failed: ${result.stderr}`,
|
||||
);
|
||||
assert.strictEqual(await readTextareaValue(), expected);
|
||||
});
|
||||
}
|
||||
|
||||
it('type_text passes a lone hyphen through', async () => {
|
||||
await startDaemon();
|
||||
|
||||
const result = await runCli(['type_text', '1', '-'], sessionId);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`type_text command failed: ${result.stderr}`,
|
||||
);
|
||||
assert.match(result.stdout, /Typed text "-"/);
|
||||
});
|
||||
|
||||
it('press_key passes a lone hyphen through', async () => {
|
||||
await startDaemon();
|
||||
|
||||
const result = await runCli(['press_key', '1', '-'], sessionId);
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`press_key command failed: ${result.stderr}`,
|
||||
);
|
||||
assert.match(result.stdout, /Successfully pressed key: -/);
|
||||
});
|
||||
|
||||
it('rejects unknown flags that are not preceded by --', async () => {
|
||||
await startDaemon();
|
||||
|
||||
const result = await runCli(
|
||||
['fill', '1', 'x', 'x', '--pageid', '2'],
|
||||
sessionId,
|
||||
);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.match(result.stderr, /Unknown arguments?: .*pageid/);
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -438,7 +438,8 @@ export async function runCli(
|
||||
return await new Promise((resolve, reject) => {
|
||||
const finalArgs = [...args];
|
||||
if (sessionId) {
|
||||
finalArgs.push('--sessionId', sessionId);
|
||||
// Prepend so that the flag is not swallowed by a trailing `--`.
|
||||
finalArgs.unshift('--sessionId', sessionId);
|
||||
}
|
||||
const child = spawn('node', [CLI_PATH, ...finalArgs], {
|
||||
env: process.env,
|
||||
|
||||
Reference in new issue
Block a user