Skip to main content
Glama
Window.test.ts16.9 kB
import { expect } from "chai"; import { describe, it, beforeEach } from "mocha"; import { Window } from "../../../src/features/observe/Window"; import sinon from "sinon"; import { AdbUtils } from "../../../src/utils/android-cmdline-tools/adb"; import { ExecResult } from "../../../src/models/ExecResult"; import { BootedDevice } from "../../../src/models/DeviceInfo"; import fs from "fs"; import path from "path"; describe("Window", () => { let window: Window; let adbStub: sinon.SinonStubbedInstance<AdbUtils>; let mockDevice: BootedDevice; beforeEach(() => { mockDevice = { deviceId: "test-device", name: "Test Device", platform: "android" }; adbStub = sinon.createStubInstance(AdbUtils); window = new Window(mockDevice, adbStub as unknown as AdbUtils); // Clear cache before each test to prevent stale results window.clearCache(); }); describe("constructor", () => { it("should create instance with provided deviceId and adb", () => { const mockDevice: BootedDevice = { deviceId: "test-device", name: "Test Device", platform: "android" }; const customAdb = sinon.createStubInstance(AdbUtils); const windowInstance = new Window(mockDevice, customAdb as unknown as AdbUtils); expect(windowInstance).to.be.instanceOf(Window); }); it("should create instance with default values when no parameters provided", () => { const mockDevice: BootedDevice = { deviceId: "default-device", name: "Default Device", platform: "android" }; const windowInstance = new Window(mockDevice); expect(windowInstance).to.be.instanceOf(Window); }); }); describe("getActive", () => { it("should parse package name and activity name correctly", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{12345678 u0 com.example.app/com.example.app.MainActivity} mLayoutSeq=123 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.example.app"); expect(result.activityName).to.equal("com.example.app.MainActivity"); expect(result.layoutSeqSum).to.equal(123); }); it("should handle multiple layout sequence values", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{12345678 u0 com.test.app/com.test.MainActivity} mLayoutSeq=123 mLayoutSeq=456 mLayoutSeq=789 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.test.app"); expect(result.activityName).to.equal("com.test.MainActivity"); expect(result.layoutSeqSum).to.equal(1368); // 123 + 456 + 789 }); it("should handle missing window info and return default values", async () => { const dumpsysOutput = ` Some other output without window info mLayoutSeq=100 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal(""); expect(result.activityName).to.equal(""); expect(result.layoutSeqSum).to.equal(100); }); it("should handle missing layout sequence and return zero", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{12345678 u0 com.example.app/com.example.app.MainActivity} Some other content without mLayoutSeq `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.example.app"); expect(result.activityName).to.equal("com.example.app.MainActivity"); expect(result.layoutSeqSum).to.equal(0); }); it("should handle non-numeric layout sequence values", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{12345678 u0 com.example.app/com.example.app.MainActivity} mLayoutSeq=abc mLayoutSeq=123 mLayoutSeq=def mLayoutSeq=456 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.example.app"); expect(result.activityName).to.equal("com.example.app.MainActivity"); expect(result.layoutSeqSum).to.equal(579); // 123 + 456 (ignores non-numeric values) }); it("should handle adb command failure gracefully", async () => { adbStub.executeCommand.rejects(new Error("ADB command failed")); const result = await window.getActive(true); expect(result.appId).to.equal(""); expect(result.activityName).to.equal(""); expect(result.layoutSeqSum).to.equal(0); }); it("should handle empty dumpsys output", async () => { adbStub.executeCommand.resolves({ stdout: "", stderr: "", toString: () => "", trim: () => "", includes: (str: string) => false } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal(""); expect(result.activityName).to.equal(""); expect(result.layoutSeqSum).to.equal(0); }); it("should parse Pop-Up Window and extract activity from mActivityRecord", async () => { // Read the actual dumpsys output with Pop-Up Window const dumpsysOutput = fs.readFileSync( path.join(__dirname, "windowDumps", "active-window-with-popup.log"), "utf8" ); adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); // Should extract the package and activity from the mActivityRecord line within the Pop-Up Window block expect(result.appId).to.equal("com.zillow.android.zillowmap"); expect(result.activityName).to.equal("com.zillow.android.appshell.MainTabActivity"); expect(result.layoutSeqSum).to.be.greaterThan(0); }); it("should handle Pop-Up Window when imeControlTarget doesn't have package/activity format", async () => { const dumpsysOutput = ` imeLayeringTarget in display# 0 Window{ddf8489 u0 Pop-Up Window} imeInputTarget in display# 0 Window{ddf8489 u0 Pop-Up Window} imeControlTarget in display# 0 Window{ddf8489 u0 Pop-Up Window} Window #9 Window{ddf8489 u0 Pop-Up Window}: mDisplayId=0 rootTaskId=8 mSession=Session{8b3234c 4199:u0a10207} mOwnerUid=10207 showForAllUsers=false package=com.zillow.android.zillowmap appop=NONE mActivityRecord=ActivityRecord{5cd319f u0 com.zillow.android.zillowmap/com.zillow.android.appshell.MainTabActivity t8} mViewVisibility=0x0 mHaveFrame=true mObscured=false Window #10 Window{10634fe u0 com.zillow.android.zillowmap/com.zillow.android.appshell.MainTabActivity}: mDisplayId=0 rootTaskId=8 mLayoutSeq=258 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.zillow.android.zillowmap"); expect(result.activityName).to.equal("com.zillow.android.appshell.MainTabActivity"); expect(result.layoutSeqSum).to.equal(258); }); it("should fall back to visible app windows when Pop-Up Window parsing fails", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{ddf8489 u0 Pop-Up Window} Window #9 Window{ddf8489 u0 Pop-Up Window}: mDisplayId=0 rootTaskId=8 // No mActivityRecord line mViewVisibility=0x0 mHaveFrame=true Window #10 Window{10634fe u0 com.example.testapp/com.example.MainActivity}: mDisplayId=0 rootTaskId=8 mViewVisibility=0x0 mHaveFrame=true mObscured=false isOnScreen=true isVisible=true mLayoutSeq=123 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.example.testapp"); expect(result.activityName).to.equal("com.example.MainActivity"); expect(result.layoutSeqSum).to.equal(123); }); it("should fall back to BASE_APPLICATION pattern when other methods fail", async () => { const dumpsysOutput = ` imeControlTarget in display# 0 Window{ddf8489 u0 Pop-Up Window} Window #9 Window{ddf8489 u0 Pop-Up Window}: mDisplayId=0 rootTaskId=8 // No mActivityRecord line mViewVisibility=0x0 mHaveFrame=true Window #10 Window{10634fe u0 com.example.testapp/com.example.MainActivity}: mDisplayId=0 rootTaskId=8 ty=BASE_APPLICATION mLayoutSeq=456 `; adbStub.executeCommand.resolves({ stdout: dumpsysOutput, stderr: "", toString: () => dumpsysOutput, trim: () => dumpsysOutput.trim(), includes: (str: string) => dumpsysOutput.includes(str) } as ExecResult); const result = await window.getActive(true); expect(result.appId).to.equal("com.example.testapp"); expect(result.activityName).to.equal("com.example.MainActivity"); expect(result.layoutSeqSum).to.equal(456); }); }); describe("getActiveHash", () => { it("should generate different hashes for different window states", async () => { // First UI state with one visible window const firstState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true mLayoutSeq=123 `; // Second UI state with an additional popup window const secondState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true Window #3 Window{g7h8i9 popupWindow} isVisible=true mLayoutSeq=124 `; // Set up stub to return different outputs for consecutive calls adbStub.executeCommand.onFirstCall().resolves({ stdout: firstState, stderr: "", toString: () => firstState, trim: () => firstState.trim(), includes: (str: string) => firstState.includes(str) } as ExecResult); adbStub.executeCommand.onSecondCall().resolves({ stdout: secondState, stderr: "", toString: () => secondState, trim: () => secondState.trim(), includes: (str: string) => secondState.includes(str) } as ExecResult); // Get hashes for both states const firstHash = await window.getActiveHash(); const secondHash = await window.getActiveHash(); // Verify that the hashes are different expect(firstHash).to.not.equal(secondHash); }); it("should generate the same hash for the same window state", async () => { // Same UI state returned twice const uiState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true mLayoutSeq=123 `; // Set up stub to return the same output twice adbStub.executeCommand.resolves({ stdout: uiState, stderr: "", toString: () => uiState, trim: () => uiState.trim(), includes: (str: string) => uiState.includes(str) } as ExecResult); // Get hashes twice const firstHash = await window.getActiveHash(); const secondHash = await window.getActiveHash(); // Verify that the hashes are the same expect(firstHash).to.equal(secondHash); }); it("should ignore invisible windows", async () => { // UI state with a mix of visible and invisible windows const uiState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true Window #3 Window{g7h8i9 hiddenWindow} isVisible=false mLayoutSeq=123 `; // UI state with the same visible windows but different invisible window const sameVisibleState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true Window #3 Window{j0k1l2 differentHiddenWindow} isVisible=false mLayoutSeq=123 `; // Set up stub to return different outputs adbStub.executeCommand.onFirstCall().resolves({ stdout: uiState, stderr: "", toString: () => uiState, trim: () => uiState.trim(), includes: (str: string) => uiState.includes(str) } as ExecResult); adbStub.executeCommand.onSecondCall().resolves({ stdout: sameVisibleState, stderr: "", toString: () => sameVisibleState, trim: () => sameVisibleState.trim(), includes: (str: string) => sameVisibleState.includes(str) } as ExecResult); // Get hashes for both states const firstHash = await window.getActiveHash(); const secondHash = await window.getActiveHash(); // Verify that the hashes are the same since only invisible windows differ expect(firstHash).to.equal(secondHash); }); it("should handle transaction sequence changes", async () => { // Same windows but different transaction sequence const firstState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true mLayoutSeq=123 `; const secondState = ` Window #1 Window{a1b2c3 statusBar} isVisible=true Window #2 Window{d4e5f6 mainActivity} isVisible=true mLayoutSeq=124 `; // Set up stub to return different outputs adbStub.executeCommand.onFirstCall().resolves({ stdout: firstState, stderr: "", toString: () => firstState, trim: () => firstState.trim(), includes: (str: string) => firstState.includes(str) } as ExecResult); adbStub.executeCommand.onSecondCall().resolves({ stdout: secondState, stderr: "", toString: () => secondState, trim: () => secondState.trim(), includes: (str: string) => secondState.includes(str) } as ExecResult); // Get hashes for both states const firstHash = await window.getActiveHash(); const secondHash = await window.getActiveHash(); // Verify that the hashes are different due to transaction sequence change expect(firstHash).to.not.equal(secondHash); }); it("should return consistent hash format", async () => { const uiState = ` imeControlTarget in display# 0 Window{12345678 u0 com.example.app/com.example.app.MainActivity} mLayoutSeq=123 `; adbStub.executeCommand.resolves({ stdout: uiState, stderr: "", toString: () => uiState, trim: () => uiState.trim(), includes: (str: string) => uiState.includes(str) } as ExecResult); const hash = await window.getActiveHash(); // MD5 hash should be 32 characters long and contain only hexadecimal characters expect(hash).to.match(/^[a-f0-9]{32}$/); }); }); });

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