Skip to main content
Glama
DetectIntentChooser.test.ts10.9 kB
import { expect } from "chai"; import { DetectIntentChooser } from "../../../src/features/observe/DetectIntentChooser"; import { DeepLinkManager } from "../../../src/utils/deepLinkManager"; import { ObserveScreen } from "../../../src/features/observe/ObserveScreen"; import { Window } from "../../../src/features/observe/Window"; import { AwaitIdle } from "../../../src/features/observe/AwaitIdle"; import { ObserveResult, ViewHierarchyResult } from "../../../src/models"; import sinon from "sinon"; describe("DetectIntentChooser", () => { let detectIntentChooser: DetectIntentChooser; let mockDeepLinkManager: DeepLinkManager; let mockObserveScreen: sinon.SinonStubbedInstance<ObserveScreen>; let mockWindow: sinon.SinonStubbedInstance<Window>; let mockAwaitIdle: sinon.SinonStubbedInstance<AwaitIdle>; const mockObserveResult: ObserveResult = { timestamp: "2025-01-01T00:00:00.000Z", screenSize: { width: 1080, height: 1920 }, systemInsets: { top: 0, right: 0, bottom: 0, left: 0 }, viewHierarchy: { hierarchy: { node: { $: { class: "com.android.internal.app.ChooserActivity" }, node: [ { $: { text: "Choose an app" } }, { $: { text: "Always", class: "android.widget.Button" } }, { $: { text: "Just once", class: "android.widget.Button" } } ] } } } }; beforeEach(() => { // Create stubs for dependencies mockObserveScreen = sinon.createStubInstance(ObserveScreen); mockWindow = sinon.createStubInstance(Window); mockAwaitIdle = sinon.createStubInstance(AwaitIdle); // Stub the constructors 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(); mockObserveScreen.getMostRecentCachedObserveResult.resolves(mockObserveResult); mockObserveScreen.execute.resolves(mockObserveResult); // Create DetectIntentChooser instance detectIntentChooser = new DetectIntentChooser("test-device"); // Create mock DeepLinkManager mockDeepLinkManager = { detectIntentChooser: (viewHierarchy: any) => { // Look for common intent chooser indicators const indicators = [ "com.android.internal.app.ChooserActivity", "com.android.internal.app.ResolverActivity", "Choose an app", "Open with", "Complete action using", "Always", "Just once", "android:id/button_always", "android:id/button_once", "resolver_list", "chooser_list" ]; const hierarchy = viewHierarchy.hierarchy; if (hierarchy && hierarchy.node) { const node = hierarchy.node; if (node.$ && node.$.class && indicators.includes(node.$.class)) { return true; } if (node.$ && node.$.text && indicators.includes(node.$.text)) { return true; } if (node.node) { for (const childNode of node.node) { if (childNode.$ && childNode.$.text && indicators.includes(childNode.$.text)) { return true; } } } } return false; } } as any; // Replace the internal managers with our mocks (detectIntentChooser as any).deepLinkManager = mockDeepLinkManager; }); afterEach(() => { sinon.restore(); }); describe("constructor", () => { it("should create DetectIntentChooser with device ID", () => { const instance = new DetectIntentChooser("test-device"); expect(instance).to.be.instanceOf(DetectIntentChooser); }); }); describe("execute", () => { it("should detect intent chooser when provided with view hierarchy", async () => { const result = await detectIntentChooser.execute(); expect(result.success).to.be.true; expect(result.detected).to.be.true; expect(result.observation).to.equal(mockObserveResult); }); it("should not detect intent chooser in normal app view hierarchy", async () => { const normalObserveResult = { ...mockObserveResult, viewHierarchy: { hierarchy: { node: { $: { class: "android.widget.LinearLayout" }, node: [ { $: { text: "Normal app content" } } ] } } } }; mockObserveScreen.getMostRecentCachedObserveResult.resolves(normalObserveResult); mockObserveScreen.execute.resolves(normalObserveResult); const result = await detectIntentChooser.execute(); expect(result.success).to.be.true; expect(result.detected).to.be.false; expect(result.observation).to.equal(normalObserveResult); }); it("should observe screen when no view hierarchy provided", async () => { const result = await detectIntentChooser.execute(); expect(result.success).to.be.true; expect(result.detected).to.be.true; // mockObserveResult contains ChooserActivity expect(result.observation).to.equal(mockObserveResult); }); it("should handle observe screen failure", 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 detectIntentChooser.execute(); expect.fail("Expected an error to be thrown"); } catch (error) { expect((error as Error).message).to.include("Cannot perform action without view hierarchy"); } }); it("should handle observe screen returning null view hierarchy", 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 detectIntentChooser.execute(); expect.fail("Expected an error to be thrown"); } catch (error) { expect((error as Error).message).to.include("Cannot perform action without view hierarchy"); } }); it("should handle deep link manager detection failure", async () => { // Mock deep link manager to throw error mockDeepLinkManager.detectIntentChooser = () => { throw new Error("Detection failed"); }; const result = await detectIntentChooser.execute(); expect(result.success).to.be.false; expect(result.detected).to.be.false; expect(result.error).to.include("Detection failed"); }); it("should detect various intent chooser indicators", async () => { const testCases: ViewHierarchyResult[] = [ { hierarchy: { node: { $: { class: "com.android.internal.app.ChooserActivity" } } } }, { hierarchy: { node: { $: { class: "com.android.internal.app.ResolverActivity" } } } }, { hierarchy: { node: { $: { text: "Choose an app" } } } }, { hierarchy: { node: { $: { text: "Open with" } } } }, { hierarchy: { node: { $: {}, node: [ { $: { text: "Always" } }, { $: { text: "Just once" } } ] } } } ]; for (const viewHierarchy of testCases) { mockObserveScreen.getMostRecentCachedObserveResult.resolves({ ...mockObserveResult, viewHierarchy }); const result = await detectIntentChooser.execute(); expect(result.success).to.be.true; expect(result.detected).to.be.true; } }); it("should not detect intent chooser in non-chooser screens", async () => { const testCases: ViewHierarchyResult[] = [ { hierarchy: { node: { $: { class: "android.widget.Button", text: "Click me" } } } }, { hierarchy: { node: { $: { text: "Welcome to the app" } } } }, { hierarchy: { node: { $: { class: "com.example.MainActivity" } } } } ]; for (const viewHierarchy of testCases) { mockObserveScreen.getMostRecentCachedObserveResult.resolves({ ...mockObserveResult, viewHierarchy }); const result = await detectIntentChooser.execute(); expect(result.success).to.be.true; expect(result.detected).to.be.false; } }); }); });

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