Skip to main content
Glama
ImeAction.test.ts16.6 kB
import { assert } from "chai"; import { ImeAction } from "../../../src/features/action/ImeAction"; import { AdbUtils } from "../../../src/utils/android-cmdline-tools/adb"; import { ObserveScreen } from "../../../src/features/observe/ObserveScreen"; import { Window } from "../../../src/features/observe/Window"; import { AwaitIdle } from "../../../src/features/observe/AwaitIdle"; import { ExecResult, ObserveResult } from "../../../src/models"; import sinon from "sinon"; describe("ImeAction", () => { let imeAction: ImeAction; let mockAdb: sinon.SinonStubbedInstance<AdbUtils>; let mockObserveScreen: sinon.SinonStubbedInstance<ObserveScreen>; let mockWindow: sinon.SinonStubbedInstance<Window>; let mockAwaitIdle: sinon.SinonStubbedInstance<AwaitIdle>; beforeEach(() => { // Create stubs for dependencies mockAdb = sinon.createStubInstance(AdbUtils); mockObserveScreen = sinon.createStubInstance(ObserveScreen); mockWindow = sinon.createStubInstance(Window); mockAwaitIdle = sinon.createStubInstance(AwaitIdle); // Stub the constructors and static calls sinon.stub(AdbUtils.prototype, "executeCommand").callsFake(mockAdb.executeCommand); sinon.stub(ObserveScreen.prototype, "execute").callsFake(mockObserveScreen.execute); sinon.stub(ObserveScreen.prototype, "getMostRecentCachedObserveResult").callsFake(mockObserveScreen.getMostRecentCachedObserveResult); sinon.stub(Window.prototype, "getCachedActiveWindow").callsFake(mockWindow.getCachedActiveWindow); sinon.stub(Window.prototype, "getActive").callsFake(mockWindow.getActive); sinon.stub(AwaitIdle.prototype, "initializeUiStabilityTracking").callsFake(mockAwaitIdle.initializeUiStabilityTracking); sinon.stub(AwaitIdle.prototype, "waitForUiStability").callsFake(mockAwaitIdle.waitForUiStability); sinon.stub(AwaitIdle.prototype, "waitForUiStabilityWithState").callsFake(mockAwaitIdle.waitForUiStabilityWithState); // Set up default mock responses mockWindow.getCachedActiveWindow.resolves(null); mockWindow.getActive.resolves({ appId: "com.test.app", activityName: "MainActivity", layoutSeqSum: 123 }); mockAwaitIdle.initializeUiStabilityTracking.resolves(); mockAwaitIdle.waitForUiStability.resolves(); mockAwaitIdle.waitForUiStabilityWithState.resolves(); // Set up default observe screen responses with valid viewHierarchy const defaultObserveResult = createMockObserveResult(); mockObserveScreen.getMostRecentCachedObserveResult.resolves(defaultObserveResult); mockObserveScreen.execute.resolves(defaultObserveResult); imeAction = new ImeAction("test-device"); }); afterEach(() => { sinon.restore(); }); // Helper function to create mock ExecResult const createMockExecResult = (stdout: string = ""): ExecResult => ({ stdout, stderr: "", toString: () => stdout, trim: () => stdout.trim(), includes: (searchString: string) => stdout.includes(searchString) }); // Helper function to create mock ObserveResult const createMockObserveResult = (): ObserveResult => ({ timestamp: Date.now(), screenSize: { width: 1080, height: 1920 }, systemInsets: { top: 0, bottom: 0, left: 0, right: 0 }, viewHierarchy: { hierarchy: { node: { $: {} } } } }); describe("execute", () => { it("should execute IME action 'done'", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("done"); assert.isTrue(result.success); assert.equal(result.action, "done"); assert.isDefined(result.observation); // Verify correct ADB command was called sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should execute IME action 'next'", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("next"); assert.isTrue(result.success); assert.equal(result.action, "next"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_TAB"); }); it("should execute IME action 'search'", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("search"); assert.isTrue(result.success); assert.equal(result.action, "search"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_SEARCH"); }); it("should execute IME action 'send'", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("send"); assert.isTrue(result.success); assert.equal(result.action, "send"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should execute IME action 'go'", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("go"); assert.isTrue(result.success); assert.equal(result.action, "go"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should execute IME action 'previous' with key combination", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await imeAction.execute("previous"); assert.isTrue(result.success); assert.equal(result.action, "previous"); // Should call both key events for Shift+Tab sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_SHIFT_LEFT"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_TAB"); // At least 2 calls for the key combination, but BaseVisualChange might make additional calls assert.isAtLeast(mockAdb.executeCommand.callCount, 2); }); it("should handle empty action string", async () => { const result = await imeAction.execute("" as any); assert.isFalse(result.success); assert.equal(result.action, ""); assert.equal(result.error, "No IME action provided"); // Should not call ADB commands sinon.assert.notCalled(mockAdb.executeCommand); }); it("should work with progress callback", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const progressCallback = sinon.stub().resolves(); const result = await imeAction.execute("done", progressCallback); assert.isTrue(result.success); // Progress callback should be called by BaseVisualChange assert.isTrue(progressCallback.called); }); it("should handle ADB command failure", async () => { const error = new Error("Failed to execute keyevent"); mockAdb.executeCommand.rejects(error); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); try { const result = await imeAction.execute("done"); // If we get here, BaseVisualChange caught the error assert.equal(result.action, "done"); assert.include(result.error || "", "Failed to execute IME action"); } catch (caughtError) { // If the error bubbled up, that's also valid behavior assert.include((caughtError as Error).message, "Failed to execute keyevent"); } sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should handle ADB command failure for multi-key actions", async () => { const error = new Error("Failed to execute keyevent"); mockAdb.executeCommand.onFirstCall().resolves(createMockExecResult()); mockAdb.executeCommand.onSecondCall().rejects(error); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); try { const result = await imeAction.execute("previous"); // If we get here, BaseVisualChange handled the error assert.equal(result.action, "previous"); } catch (caughtError) { // If the error bubbled up, that's also valid behavior assert.include((caughtError as Error).message, "Failed to execute keyevent"); } // Should have attempted both key events sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_SHIFT_LEFT"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_TAB"); }); }); describe("constructor", () => { it("should work with null deviceId", () => { const imeActionInstance = new ImeAction("test-device"); assert.isDefined(imeActionInstance); }); it("should work with custom AdbUtils", () => { const customAdb = new AdbUtils("custom-device"); const imeActionInstance = new ImeAction("test-device", customAdb); assert.isDefined(imeActionInstance); }); }); describe("timing", () => { it("should include delay before executing keyevent", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const startTime = Date.now(); const result = await imeAction.execute("done"); const elapsedTime = Date.now() - startTime; assert.isTrue(result.success); // Should include at least 100ms delay plus some execution time assert.isAtLeast(elapsedTime, 100); }); }); describe("error handling", () => { it("should handle missing view hierarchy gracefully", async () => { // Mock getMostRecentCachedObserveResult to reject with error mockObserveScreen.getMostRecentCachedObserveResult.rejects(new Error("Cannot perform action without view hierarchy")); // Also mock execute to fail since BaseVisualChange falls back to execute() mockObserveScreen.execute.rejects(new Error("Cannot perform action without view hierarchy")); try { await imeAction.execute("done"); assert.fail("Expected an error to be thrown"); } catch (caughtError) { assert.include((caughtError as Error).message, "Cannot perform action without view hierarchy"); } }); it("should handle observation failure", async () => { // Set up valid cached result but make execute fail const mockCachedObservation = createMockObserveResult(); mockObserveScreen.getMostRecentCachedObserveResult.resolves(mockCachedObservation); mockAdb.executeCommand.resolves(createMockExecResult()); const observationError = new Error("Failed to observe screen"); mockObserveScreen.execute.rejects(observationError); try { const result = await imeAction.execute("done"); // If we get here, BaseVisualChange handled the observation error assert.equal(result.action, "done"); } catch (caughtError) { // If the error bubbled up, that's also valid behavior assert.include((caughtError as Error).message, "Failed to observe screen"); } }); it("should handle null action gracefully", async () => { const result = await imeAction.execute(null as any); assert.isFalse(result.success); assert.equal(result.action, ""); assert.equal(result.error, "No IME action provided"); }); it("should handle undefined action gracefully", async () => { const result = await imeAction.execute(undefined as any); assert.isFalse(result.success); assert.equal(result.action, ""); assert.equal(result.error, "No IME action provided"); }); }); describe("edge cases", () => { it("should handle all valid IME actions", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const validActions: Array<"done" | "next" | "search" | "send" | "go" | "previous"> = ["done", "next", "search", "send", "go", "previous"]; for (const action of validActions) { mockAdb.executeCommand.resetHistory(); const result = await imeAction.execute(action); assert.isTrue(result.success, `Action '${action}' should succeed`); assert.equal(result.action, action); assert.isAtLeast(mockAdb.executeCommand.callCount, 1, `Action '${action}' should call ADB`); } }); it("should handle rapid consecutive calls", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const promises = [ imeAction.execute("done"), imeAction.execute("next"), imeAction.execute("search") ]; const results = await Promise.all(promises); results.forEach((result, index) => { assert.isTrue(result.success, `Call ${index} should succeed`); }); // Should have called ADB command for each action assert.isAtLeast(mockAdb.executeCommand.callCount, 3); }); }); describe("key mapping", () => { it("should map done to KEYCODE_ENTER", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("done"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should map next to KEYCODE_TAB", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("next"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_TAB"); }); it("should map search to KEYCODE_SEARCH", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("search"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_SEARCH"); }); it("should map send to KEYCODE_ENTER", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("send"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should map go to KEYCODE_ENTER", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("go"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_ENTER"); }); it("should map previous to SHIFT+TAB combination", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); await imeAction.execute("previous"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_SHIFT_LEFT"); sinon.assert.calledWith(mockAdb.executeCommand, "shell input keyevent KEYCODE_TAB"); // At least 2 calls for the key combination, but BaseVisualChange might make additional calls assert.isAtLeast(mockAdb.executeCommand.callCount, 2); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zillow/auto-mobile'

If you have feedback or need assistance with the MCP directory API, please join our Discord server