Skip to main content
Glama
Shake.test.ts13.4 kB
import { assert } from "chai"; import { Shake } from "../../../src/features/action/Shake"; 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("Shake", () => { let shake: Shake; let mockAdb: sinon.SinonStubbedInstance<AdbUtils>; let mockObserveScreen: sinon.SinonStubbedInstance<ObserveScreen>; let mockWindow: sinon.SinonStubbedInstance<Window>; let mockAwaitIdle: sinon.SinonStubbedInstance<AwaitIdle>; // 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: { node: {} } }); beforeEach(() => { // Create stubs for dependencies mockAdb = sinon.createStubInstance(AdbUtils); mockObserveScreen = sinon.createStubInstance(ObserveScreen); mockWindow = sinon.createStubInstance(Window); mockAwaitIdle = sinon.createStubInstance(AwaitIdle); // Stub the constructors/functions 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); shake = new Shake("test-device"); }); afterEach(() => { sinon.restore(); }); describe("execute", () => { it("should execute shake with default parameters", async () => { // Mock successful ADB commands mockAdb.executeCommand.resolves(createMockExecResult()); // Mock observation (BaseVisualChange calls observeScreen.execute at the end) const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute(); assert.isTrue(result.success); assert.equal(result.duration, 1000); assert.equal(result.intensity, 100); assert.isDefined(result.observation); // Verify ADB commands were called correctly (2 for shake, +1 for BaseVisualChange observation) sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 100:100:100"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should execute shake with custom duration", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ duration: 100 }); // Reduced for faster test assert.isTrue(result.success); assert.equal(result.duration, 100); assert.equal(result.intensity, 100); // Verify default intensity is used sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 100:100:100"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should execute shake with custom intensity", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ intensity: 200, duration: 100 }); // Reduced duration assert.isTrue(result.success); assert.equal(result.duration, 100); assert.equal(result.intensity, 200); // Verify custom intensity is used sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 200:200:200"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should execute shake with custom duration and intensity", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ duration: 100, intensity: 150 }); // Reduced duration assert.isTrue(result.success); assert.equal(result.duration, 100); assert.equal(result.intensity, 150); // Verify custom parameters are used sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 150:150:150"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should execute shake with empty options object", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({}); assert.isTrue(result.success); assert.equal(result.duration, 1000); assert.equal(result.intensity, 100); }); it("should handle zero duration", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ duration: 0 }); assert.isTrue(result.success); assert.equal(result.duration, 0); assert.equal(result.intensity, 100); // Should still call both commands even with 0 duration sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 100:100:100"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should handle zero intensity", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ intensity: 0, duration: 100 }); assert.isTrue(result.success); assert.equal(result.duration, 100); assert.equal(result.intensity, 0); // Should call with 0 intensity (no shake effect) sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); it("should work with progress callback", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const progressCallback = sinon.spy(); const result = await shake.execute({ duration: 50 }, progressCallback); assert.isTrue(result.success); // Progress callback should be called by BaseVisualChange assert.isTrue(progressCallback.called); }); it("should handle ADB command failure during shake start", async () => { const error = new Error("Failed to set acceleration"); mockAdb.executeCommand.onFirstCall().rejects(error); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); try { const result = await shake.execute({ duration: 100 }); // If we get here, BaseVisualChange caught the error assert.equal(result.duration, 100); assert.equal(result.intensity, 100); } catch (caughtError) { // If the error bubbled up, that's also valid behavior assert.include((caughtError as Error).message, "Failed to set acceleration"); } // Should have tried to start shake sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 100:100:100"); }); it("should handle ADB command failure during shake stop", async () => { const error = new Error("Failed to reset acceleration"); mockAdb.executeCommand.onFirstCall().resolves(createMockExecResult()); mockAdb.executeCommand.onSecondCall().rejects(error); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); try { const result = await shake.execute({ duration: 50 }); // Very short duration // If we get here, BaseVisualChange caught the error assert.isDefined(result); } catch (caughtError) { // If the error bubbled up, that's also valid behavior assert.include((caughtError as Error).message, "Failed to reset acceleration"); } // Check that both commands were attempted sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 100:100:100"); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 0:0:0"); }); }); describe("constructor", () => { it("should work with null deviceId", () => { const shakeInstance = new Shake("test-device"); assert.isDefined(shakeInstance); }); it("should work with custom AdbUtils", () => { const customAdb = new AdbUtils("custom-device"); const shakeInstance = new Shake("test-device", customAdb); assert.isDefined(shakeInstance); }); }); describe("timing", () => { it("should respect the duration timing", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const startTime = Date.now(); const duration = 200; const result = await shake.execute({ duration }); const elapsedTime = Date.now() - startTime; assert.isTrue(result.success); // Allow some tolerance for timing (should be at least the duration) assert.isAtLeast(elapsedTime, duration - 50); // But not too much longer (within 1000ms tolerance for test execution and BaseVisualChange overhead) assert.isAtMost(elapsedTime, duration + 1000); }); }); describe("edge cases", () => { it("should handle very high intensity values", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ intensity: 9999, duration: 100 }); assert.isTrue(result.success); assert.equal(result.intensity, 9999); sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration 9999:9999:9999"); }); it("should handle very long duration", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); // Use shorter duration to avoid test timeout const result = await shake.execute({ duration: 200 }); assert.isTrue(result.success); assert.equal(result.duration, 200); // Both commands should still be called assert.isAtLeast(mockAdb.executeCommand.callCount, 2); }); it("should handle negative values gracefully", async () => { mockAdb.executeCommand.resolves(createMockExecResult()); const mockObservation = createMockObserveResult(); mockObserveScreen.execute.resolves(mockObservation); const result = await shake.execute({ duration: 100, intensity: -50 }); // Use positive duration assert.isTrue(result.success); assert.equal(result.duration, 100); assert.equal(result.intensity, -50); // Should use the negative intensity as provided sinon.assert.calledWith(mockAdb.executeCommand, "emu sensor set acceleration -50:-50:-50"); }); }); });

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