Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/__tests__/cli-batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('batch --steps parses JSON and forwards batchSteps only', async () => {
]);
assert.equal(result.code, null);
assert.equal(result.calls.length, 1);
const req = result.calls[0];
const req = result.calls[0]!;
assert.equal(req.command, 'batch');
assert.equal(req.session, 'sim');
assert.equal(req.flags?.platform, 'ios');
Expand All @@ -57,7 +57,7 @@ test('batch --steps-file parses file payload', async () => {
const result = await runCliCapture(['batch', '--steps-file', stepsPath, '--json']);
assert.equal(result.code, null);
assert.equal(result.calls.length, 1);
const req = result.calls[0];
const req = result.calls[0]!;
assert.equal(req.command, 'batch');
assert.equal((req.flags?.batchSteps ?? [])[0]?.command, 'wait');
});
Expand Down Expand Up @@ -88,7 +88,7 @@ test('batch accepts legacy positionals/flags steps with deprecation warning', as
assert.equal(result.code, null);
assert.match(result.stderr, /positionals\/flags are deprecated.*next major version/);
assert.equal(result.calls.length, 1);
const req = result.calls[0];
const req = result.calls[0]!;
assert.equal(req.command, 'batch');
assert.deepEqual((req.flags?.batchSteps ?? [])[0], {
command: 'open',
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/client-companion-tunnel-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function decodeWebSocketPayload(payload: Buffer, mask: Buffer | null): Buffer {

const decoded = Buffer.from(payload);
for (let index = 0; index < decoded.length; index += 1) {
decoded[index] ^= mask[index % 4];
decoded[index]! ^= mask[index % 4]!;
}
return decoded;
}
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/selectors-public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const nodes: SnapshotNode[] = [

test('public selector subpath exposes platform-aware matching helpers', () => {
const chain: SelectorChain = parseSelectorChain('role=button label="Continue" visible=true');
const firstSelector: Selector = chain.selectors[0];
const firstSelector: Selector = chain.selectors[0]!;
assert.equal(firstSelector.raw, 'role=button label="Continue" visible=true');
assert.equal(tryParseSelectorChain(chain.raw)?.raw, chain.raw);
assert.equal(isSelectorToken('visible=true'), true);
Expand All @@ -55,8 +55,8 @@ test('public selector subpath exposes platform-aware matching helpers', () => {
});
assert.equal(resolved?.node.ref, 'e1');

assert.equal(isNodeVisible(nodes[0]), true);
assert.equal(isNodeEditable(nodes[1], 'android'), true);
assert.equal(isNodeVisible(nodes[0]!), true);
assert.equal(isNodeEditable(nodes[1]!, 'android'), true);
});

test('public selector diagnostics format failures', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/upload-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ test('uploadArtifact disables macOS AppleDouble entries when archiving app bundl
(cmd, args, options) => {
if (cmd !== 'tar') return undefined;
tarEnv = options.env;
const archivePath = args[1];
const archivePath = args[1]!;
assert.equal(args[0], 'czf');
assert.equal(typeof archivePath, 'string');
fs.writeFileSync(archivePath, 'fake-archive');
Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ function hasExplicitMetroRuntimeOverrides(explicitFlagKeys: Set<FlagKey>): boole

function guessSessionFromArgv(argv: string[]): string | null {
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
const token = argv[i]!;
if (token.startsWith('--session=')) {
const inline = token.slice('--session='.length).trim();
return inline.length > 0 ? inline : null;
Expand Down
9 changes: 5 additions & 4 deletions src/commands/cli-grammar/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,16 @@ function readWaitOptionsFromPositionals(
}

export function parseWaitPositionals(args: string[]): WaitParsed | null {
if (args.length === 0) return null;
const sleepMs = parseTimeout(args[0]);
const firstArg = args[0];
if (firstArg === undefined) return null;
const sleepMs = parseTimeout(firstArg);
if (sleepMs !== null) return { kind: 'sleep', durationMs: sleepMs };
const timeoutMs = parseTimeout(args[args.length - 1]);
if (args[0] === 'text') {
if (firstArg === 'text') {
const text = timeoutMs !== null ? args.slice(1, -1).join(' ') : args.slice(1).join(' ');
return { kind: 'text', text: text.trim(), timeoutMs };
}
if (args[0].startsWith('@')) return { kind: 'ref', rawRef: args[0], timeoutMs };
if (firstArg.startsWith('@')) return { kind: 'ref', rawRef: firstArg, timeoutMs };
const argsWithoutTimeout = timeoutMs !== null ? args.slice(0, -1) : args.slice();
const split = splitSelectorFromArgs(argsWithoutTimeout);
if (split && split.rest.length === 0 && tryParseSelectorChain(split.selectorExpression)) {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/cli-grammar/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,15 @@ function readLongPressTargetFromPositionals(positionals: string[]): LongPressOpt
}

export function readFillTargetFromPositionals(positionals: string[]): DecodedFillTarget {
if (positionals[0]?.startsWith('@')) {
const firstPositional = positionals[0];
if (firstPositional?.startsWith('@')) {
const text =
positionals.length >= 3 ? positionals.slice(2).join(' ') : positionals.slice(1).join(' ');
return {
kind: 'ref',
target: {
ref: positionals[0],
label: positionals.length >= 3 ? optionalTrimmedText([positionals[1]]) : undefined,
ref: firstPositional,
label: positionals.length >= 3 ? optionalTrimmedText(positionals.slice(1, 2)) : undefined,
},
text,
};
Expand Down
4 changes: 3 additions & 1 deletion src/commands/interaction-targeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function findPreferredActionableDescendant(
if (sameRectChildren.length !== 1) {
break;
}
current = sameRectChildren[0];
const child = sameRectChildren[0];
if (child === undefined) break;
current = child;
}

return current === node ? null : current;
Expand Down
20 changes: 11 additions & 9 deletions src/commands/react-native/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,17 @@ function actionFromDismissNode(node: SnapshotNode): ReactNativeOverlayDismissTar
function chooseCollapsedWarningNode(nodes: SnapshotNode[]): SnapshotNode | null {
const withRect = nodes.filter((node) => node.rect);
if (withRect.length === 0) return null;
return withRect.sort((a, b) => {
const aHittable = a.hittable === true ? 1 : 0;
const bHittable = b.hittable === true ? 1 : 0;
if (aHittable !== bHittable) return bHittable - aHittable;
const aWidth = a.rect?.width ?? 0;
const bWidth = b.rect?.width ?? 0;
if (aWidth !== bWidth) return bWidth - aWidth;
return (b.rect?.y ?? 0) - (a.rect?.y ?? 0);
})[0];
return (
withRect.sort((a, b) => {
const aHittable = a.hittable === true ? 1 : 0;
const bHittable = b.hittable === true ? 1 : 0;
if (aHittable !== bHittable) return bHittable - aHittable;
const aWidth = a.rect?.width ?? 0;
const bWidth = b.rect?.width ?? 0;
if (aWidth !== bWidth) return bWidth - aWidth;
return (b.rect?.y ?? 0) - (a.rect?.y ?? 0);
})[0] ?? null
);
}

function collapsedBannerClosePoint(node: SnapshotNode): Point {
Expand Down
11 changes: 6 additions & 5 deletions src/compat/maestro/replay-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function optimizeInputTextActions(
const mergedActions: SessionAction[] = [];
const mergedLines: number[] = [];
for (let index = 0; index < actions.length; index += 1) {
const action = actions[index];
const action = actions[index]!;
const optimized = optimizeTypedAfterTap(actions, actionLines, index);
if (optimized) {
mergedActions.push(...optimized.actions);
Expand All @@ -104,16 +104,17 @@ function optimizeTypedAfterTap(
actionLines: number[],
index: number,
): { actions: SessionAction[]; actionLines: number[]; consumed: number } | null {
const action = actions[index];
const action = actions[index]!;
const nextAction = actions[index + 1];
const typedAfterTap = readPlainTypeText(nextAction);
const tapSelector = readPlainMaestroTapSelector(action);
if (typedAfterTap === null || tapSelector === null) return null;
if (!nextAction || typedAfterTap === null || tapSelector === null) return null;
const line = actionLines[index] ?? 1;
if (!isLikelyTextEntrySelector(tapSelector)) {
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
}
if (actions[index + 2]?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) {
const pressEnterAction = actions[index + 2];
if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) {
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
}
return {
Expand All @@ -129,7 +130,7 @@ function optimizeTypedAfterTap(
positionals: [tapSelector, typedAfterTap],
flags: action.flags,
},
actions[index + 2] as SessionAction,
pressEnterAction,
],
actionLines: [line, line, actionLines[index + 2] ?? line],
consumed: 3,
Expand Down
3 changes: 2 additions & 1 deletion src/compat/maestro/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export function requireStringValue(command: string, value: unknown): string {

export function resolveMaestroString(value: string, context: MaestroParseContext): string {
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (match, key: string) => {
return Object.prototype.hasOwnProperty.call(context.env, key) ? context.env[key] : match;
if (!Object.prototype.hasOwnProperty.call(context.env, key)) return match;
return String(context.env[key]);
});
}

Expand Down
3 changes: 1 addition & 2 deletions src/core/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ export async function runBatch(
const steps = validateAndNormalizeBatchSteps(flags?.batchSteps, batchMaxSteps);
const startedAt = Date.now();
const partialResults: BatchStepResult[] = [];
for (let index = 0; index < steps.length; index += 1) {
const step = steps[index];
for (const [index, step] of steps.entries()) {
const stepResponse = await runBatchStep(req, sessionName, step, invoke, index + 1);
if (!stepResponse.ok) {
return {
Expand Down
33 changes: 15 additions & 18 deletions src/core/dispatch-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,10 @@ export async function handleLongPressCommand(
interactor: Interactor,
positionals: string[],
): Promise<Record<string, unknown>> {
const x = Number(positionals[0]);
const y = Number(positionals[1]);
const { x, y } = readPoint(positionals, 'longpress requires x y [durationMs]', {
hint: 'Direct platform longpress requires coordinates. In an open daemon session, use agent-device longpress @ref|selector [durationMs]; otherwise run snapshot -i -c, use the target rect center as x y, then retry longpress x y durationMs.',
});
const durationMs = positionals[2] ? Number(positionals[2]) : undefined;
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new AppError('INVALID_ARGS', 'longpress requires x y [durationMs]', {
hint: 'Direct platform longpress requires coordinates. In an open daemon session, use agent-device longpress @ref|selector [durationMs]; otherwise run snapshot -i -c, use the target rect center as x y, then retry longpress x y durationMs.',
});
}
await interactor.longPress(x, y, durationMs);
return { x, y, durationMs, ...successText(`Long pressed (${x}, ${y})`) };
}
Expand All @@ -51,10 +47,7 @@ export async function handleFocusCommand(
interactor: Interactor,
positionals: string[],
): Promise<Record<string, unknown>> {
const [x, y] = positionals.map(Number);
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new AppError('INVALID_ARGS', 'focus requires x y');
}
const { x, y } = readPoint(positionals, 'focus requires x y');
await interactor.focus(x, y);
return { x, y, ...successText(`Focused (${x}, ${y})`) };
}
Expand Down Expand Up @@ -184,9 +177,16 @@ type PressSeriesOptions = {
doubleTap: boolean;
};

function readPoint(positionals: string[], errorMessage: string): Point {
const [x, y] = positionals.map(Number);
if (Number.isNaN(x) || Number.isNaN(y)) throw new AppError('INVALID_ARGS', errorMessage);
function readPoint(
positionals: string[],
errorMessage: string,
details?: Record<string, unknown>,
): Point {
const x = Number(positionals[0]);
const y = Number(positionals[1]);
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new AppError('INVALID_ARGS', errorMessage, details);
}
return { x, y };
}

Expand Down Expand Up @@ -817,10 +817,7 @@ export async function handleReadCommand(
positionals: string[],
context: DispatchContext | undefined,
): Promise<Record<string, unknown>> {
const [x, y] = positionals.map(Number);
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new AppError('INVALID_ARGS', 'read requires x y');
}
const { x, y } = readPoint(positionals, 'read requires x y');
if (device.platform === 'android') {
const text = await readAndroidTextAtPoint(device, x, y);
return { action: 'read', text: text ?? '' };
Expand Down
2 changes: 1 addition & 1 deletion src/core/dispatch-series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function shouldUseIosDragSeries(device: DeviceInfo, count: number): boole

export function computeDeterministicJitter(index: number, jitterPx: number): [number, number] {
if (jitterPx <= 0) return [0, 0];
const [dx, dy] = DETERMINISTIC_JITTER_PATTERN[index % DETERMINISTIC_JITTER_PATTERN.length];
const [dx, dy] = DETERMINISTIC_JITTER_PATTERN[index % DETERMINISTIC_JITTER_PATTERN.length]!;
return [dx * jitterPx, dy * jitterPx];
}

Expand Down
3 changes: 3 additions & 0 deletions src/core/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ async function handleSettingsCommand(
context: DispatchContext | undefined,
): Promise<Record<string, unknown>> {
const [setting, state, target, mode] = positionals;
if (!setting || !state) {
throw new AppError('INVALID_ARGS', 'settings requires setting state');
}
const isLocationSet = setting === 'location' && state === 'set';
const usesPayloadAppBundleSlot = setting === 'permission' || isLocationSet;
const appBundleId =
Expand Down
15 changes: 8 additions & 7 deletions src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,12 @@ async function prepareRemoteRequest(
clientArtifactPaths,
});

if (
!isRemoteDaemon(info) ||
(req.command !== 'install' && req.command !== 'reinstall') ||
positionals.length < 2
) {
if (!isRemoteDaemon(info) || (req.command !== 'install' && req.command !== 'reinstall')) {
return baseResult();
}

const rawPath = positionals[1]!;
const rawPath = positionals[1];
if (rawPath === undefined) return baseResult();
if (rawPath.startsWith('remote:')) {
positionals[1] = rawPath.slice('remote:'.length);
return createPreparedRemoteRequest({ positionals, flags, clientArtifactPaths });
Expand Down Expand Up @@ -959,7 +956,11 @@ function resolveDaemonLaunchSpec(): DaemonLaunchSpec {
path.join(root, 'dist', 'src', 'internal', 'daemon.js'),
path.join(root, 'dist', 'src', 'daemon.js'),
];
const distPath = distPaths.find((candidate) => fs.existsSync(candidate)) ?? distPaths[0]!;
const defaultDistPath = distPaths[0];
if (defaultDistPath === undefined) {
throw new AppError('COMMAND_FAILED', 'Daemon dist path list is empty');
}
const distPath = distPaths.find((candidate) => fs.existsSync(candidate)) ?? defaultDistPath;
const srcPath = path.join(root, 'src', 'daemon.ts');

const hasDist = distPaths.some((candidate) => fs.existsSync(candidate));
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/__tests__/device-ready.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test('ensureDeviceReady caches successful simulator readiness checks', async ()

test('ensureDeviceReady caches successful iOS physical device readiness checks', async () => {
mockRunCmd.mockImplementation(async (_cmd, args) => {
const jsonPath = args[args.indexOf('--json-output') + 1];
const jsonPath = args[args.indexOf('--json-output') + 1]!;
await fs.writeFile(
jsonPath,
JSON.stringify({
Expand Down
4 changes: 2 additions & 2 deletions src/daemon/__tests__/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ const nodes: SnapshotState['nodes'] = [
test('parseSelectorChain parses fallback and boolean terms', () => {
const chain = parseSelectorChain('id=auth_continue || role=button label="Continue" visible=true');
assert.equal(chain.selectors.length, 2);
assert.equal(chain.selectors[0].terms[0].key, 'id');
assert.equal(chain.selectors[1].terms[2].key, 'visible');
assert.equal(chain.selectors[0]!.terms[0]!.key, 'id');
assert.equal(chain.selectors[1]!.terms[2]!.key, 'visible');
});

test('resolveSelectorChain resolves unique match', () => {
Expand Down
10 changes: 5 additions & 5 deletions src/daemon/__tests__/snapshot-processing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ test('pruneGroupNodes drops unlabeled group wrappers and rebalances depth', () =
];
const pruned = pruneGroupNodes(raw);
assert.equal(pruned.length, 2);
assert.equal(pruned[1].depth, 1);
assert.equal(pruned[1].label, 'Continue');
assert.equal(pruned[1]!.depth, 1);
assert.equal(pruned[1]!.label, 'Continue');
});

test('findNearestHittableAncestor walks parents until hittable node', () => {
Expand All @@ -30,7 +30,7 @@ test('findNearestHittableAncestor walks parents until hittable node', () => {
{ index: 1, parentIndex: 0, hittable: false, rect: { x: 0, y: 0, width: 50, height: 20 } },
{ index: 2, parentIndex: 1, hittable: false, rect: { x: 0, y: 0, width: 20, height: 20 } },
]);
const ancestor = findNearestHittableAncestor(nodes, nodes[2]);
const ancestor = findNearestHittableAncestor(nodes, nodes[2]!);
assert.equal(ancestor?.ref, 'e1');
});

Expand All @@ -47,6 +47,6 @@ test('extractNodeReadText ignores generic implementation identifiers as fallback
identifier: '_NS:248',
},
]);
assert.equal(extractNodeReadText(nodes[0]), '');
assert.equal(extractNodeReadText(nodes[1]), '');
assert.equal(extractNodeReadText(nodes[0]!), '');
assert.equal(extractNodeReadText(nodes[1]!), '');
});
3 changes: 2 additions & 1 deletion src/daemon/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export function inferFillText(action: SessionAction): string {
}
const positionals = action.positionals ?? [];
if (positionals.length === 0) return '';
if (positionals[0].startsWith('@')) {
const first = positionals[0];
if (first?.startsWith('@')) {
if (positionals.length >= 3) return positionals.slice(2).join(' ').trim();
return positionals.slice(1).join(' ').trim();
}
Expand Down
Loading
Loading