Skip to content

Commit ca6a22a

Browse files
quanruclaude
andauthored
fix(core): handle null data in WaitFor and support array keyName in KeyboardPress (#1354)
* fix(core): handle null data in WaitFor and support array keyName in KeyboardPress This commit fixes two critical bugs: 1. **Fix null data handling in task execution** - Fixed TypeError when AI extract() returns null for WaitFor operations - Added null/undefined check before accessing data properties - WaitFor operations now return false when data is null (condition not met) - Other operations (Assert, Query, String, Number) return null when data is null - Location: src/agent/tasks.ts:936-938 2. **Add array support for keyName in KeyboardPress** - Updated actionKeyboardPressParamSchema to accept string | string[] - Allows key combinations like ['Control', 'A'] for keyboard shortcuts - Maintains backward compatibility with string format - Updated type definitions in aiKeyboardPress method - Locations: - src/device/index.ts:197-199 - src/agent/agent.ts:575-622 **Test Coverage:** - Added comprehensive unit tests for null data handling (8 test cases) - Added unit tests for keyName array validation (7 test cases) - All tests verify edge cases and expected behavior Fixes issue where executor crashed with: "TypeError: Cannot read properties of null (reading 'StatementIsTruthy')" And fixes parameter validation error: "Invalid parameters for action KeyboardPress: Expected string, received array" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(ios,android): handle array keyName in KeyboardPress action - Updated iOS and Android device implementations to handle keyName as string | string[] - For mobile devices, array keys are joined with '+' (e.g., ['Control', 'A'] becomes 'Control+A') - This fixes TypeScript compilation errors in iOS and Android packages - Maintains backward compatibility with string format Related to the KeyboardPress array support added in the previous commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(ios,android): improve KeyboardPress array handling - Remove incorrect join('+') approach that doesn't work on mobile devices - Use last key from array instead (e.g., ['Control', 'A'] → 'A') - Add clear warning messages when array input is used on mobile platforms - Mobile devices don't support keyboard combinations, this is a graceful degradation This makes the behavior more predictable and provides better feedback to developers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test(core): fix TaskExecutor constructor arguments in null data tests - Fixed TaskExecutor constructor call to match actual signature - Constructor requires (interface, insight, options) instead of (insight, interface) - All 8 tests now passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(ios,android): improve logging for unsupported key combinations in device input * fix(core): handle null data in WaitFor and improve keyName parameter description This commit fixes the null data handling bug and improves the KeyboardPress parameter description. ## Changes: ### 1. Fix null data handling in task execution - Fixed TypeError when AI extract() returns null for WaitFor operations - Added null/undefined check before accessing data properties (tasks.ts:936-938) - WaitFor operations now return false when data is null (condition not met) - Other operations (Assert, Query, String, Number) return null when data is null ### 2. Improve KeyboardPress parameter description - Reverted keyName to only accept string type (not array) - Added clear description: "Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'" - This provides better guidance to AI for generating key combinations - Simplified iOS/Android implementations (no special array handling needed) ### 3. Test coverage - Added 8 unit tests for null data handling - Updated KeyboardPress tests to validate string-only format - Added test for key combination strings (e.g., 'Control+A') - Added test to verify arrays are rejected - Fixed unused variable warning in test file ## Fixed Issues: **Issue 1:** Executor crashes with null data ``` TypeError: Cannot read properties of null (reading 'StatementIsTruthy') ``` **Issue 2:** Unclear how to specify key combinations - Now clearly documented in parameter description with examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(core): align KeyboardPress action description with parameter schema Updated the KeyboardPress action description to explicitly mention support for key combinations (e.g., "Control+A", "Shift+Enter"), making it consistent with the keyName parameter description that already documented this functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): handle null and undefined data in WaitFor output processing --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c19cb5b commit ca6a22a

File tree

6 files changed

+388
-9
lines changed

6 files changed

+388
-9
lines changed

packages/android/src/device.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,7 @@ export class AndroidDevice implements AbstractInterface {
212212
);
213213
}),
214214
defineActionKeyboardPress(async (param) => {
215-
const key = param.keyName;
216-
await this.keyboardPress(key);
215+
await this.keyboardPress(param.keyName);
217216
}),
218217
defineAction({
219218
name: 'AndroidBackButton',

packages/core/src/agent/tasks.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,9 +933,17 @@ export class TaskExecutor {
933933
// If AI returned a plain string instead of structured format, use it directly
934934
if (typeof data === 'string') {
935935
outputResult = data;
936+
} else if (type === 'WaitFor') {
937+
if (data === null || data === undefined) {
938+
outputResult = false;
939+
} else {
940+
outputResult = (data as any)[keyOfResult];
941+
}
942+
} else if (data === null || data === undefined) {
943+
outputResult = null;
936944
} else {
937945
assert(
938-
type !== 'WaitFor' ? data?.[keyOfResult] !== undefined : true,
946+
data?.[keyOfResult] !== undefined,
939947
'No result in query data',
940948
);
941949
outputResult = (data as any)[keyOfResult];

packages/core/src/device/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@ export const actionKeyboardPressParamSchema = z.object({
194194
locate: getMidsceneLocationSchema()
195195
.describe('The element to be clicked before pressing the key')
196196
.optional(),
197-
keyName: z.string().describe('The key to be pressed'),
197+
keyName: z
198+
.string()
199+
.describe(
200+
"The key to be pressed. Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'",
201+
),
198202
});
199203
export type ActionKeyboardPressParam = {
200204
locate?: LocateResultElement;
@@ -210,7 +214,7 @@ export const defineActionKeyboardPress = (
210214
>({
211215
name: 'KeyboardPress',
212216
description:
213-
'Press a function key, like "Enter", "Tab", "Escape". Do not use this to type text.',
217+
'Press a key or key combination, like "Enter", "Tab", "Escape", or "Control+A", "Shift+Enter". Do not use this to type text.',
214218
interfaceAlias: 'aiKeyboardPress',
215219
paramSchema: actionKeyboardPressParamSchema,
216220
call,

packages/core/tests/unit-test/action-param-validation.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getMidsceneLocationSchema, parseActionParam } from '@/ai-model';
2-
import { defineAction } from '@/device';
2+
import { actionKeyboardPressParamSchema, defineAction } from '@/device';
33
import { describe, expect, it } from 'vitest';
44
import { z } from 'zod';
55

@@ -255,7 +255,7 @@ describe('Action Parameter Validation', () => {
255255
.default(false)
256256
.describe('Append instead of replace'),
257257
}),
258-
call: async (param) => {
258+
call: async () => {
259259
// Mock implementation
260260
},
261261
});
@@ -321,4 +321,68 @@ describe('Action Parameter Validation', () => {
321321
expect(parsed.value).toBe('test');
322322
});
323323
});
324+
325+
describe('KeyboardPress Action', () => {
326+
it('should accept keyName as a string', () => {
327+
const rawParam = {
328+
keyName: 'Enter',
329+
};
330+
331+
const parsed = parseActionParam(rawParam, actionKeyboardPressParamSchema);
332+
333+
expect(parsed).toEqual({
334+
keyName: 'Enter',
335+
});
336+
});
337+
338+
it('should accept keyName as key combination string', () => {
339+
const rawParam = {
340+
keyName: 'Control+A',
341+
};
342+
343+
const parsed = parseActionParam(rawParam, actionKeyboardPressParamSchema);
344+
345+
expect(parsed).toEqual({
346+
keyName: 'Control+A',
347+
});
348+
});
349+
350+
it('should accept keyName with optional locate parameter', () => {
351+
const rawParam = {
352+
keyName: 'Control+V',
353+
locate: {
354+
prompt: 'text input field',
355+
deepThink: false,
356+
},
357+
};
358+
359+
const parsed = parseActionParam(rawParam, actionKeyboardPressParamSchema);
360+
361+
expect(parsed.keyName).toEqual('Control+V');
362+
expect(parsed.locate).toEqual({
363+
prompt: 'text input field',
364+
deepThink: false,
365+
});
366+
});
367+
368+
it('should reject keyName with invalid type', () => {
369+
const rawParam = {
370+
keyName: 123, // Invalid type
371+
};
372+
373+
expect(() =>
374+
parseActionParam(rawParam, actionKeyboardPressParamSchema),
375+
).toThrow();
376+
});
377+
378+
it('should reject keyName as array', () => {
379+
const rawParam = {
380+
keyName: ['Control', 'A'], // Arrays not supported
381+
};
382+
383+
expect(() =>
384+
parseActionParam(rawParam, actionKeyboardPressParamSchema),
385+
).toThrow();
386+
});
387+
});
324388
});

0 commit comments

Comments
 (0)