Skip to main content
Glama
TakeScreenshot.test.ts12.2 kB
import { expect } from "chai"; import { describe, it, beforeEach, afterEach } from "mocha"; import fs from "fs-extra"; import { TakeScreenshot } from "../../../src/features/observe/TakeScreenshot"; import { AdbUtils } from "../../../src/utils/android-cmdline-tools/adb"; import { Image } from "../../../src/utils/image-utils"; import { logger } from "../../../src/utils/logger"; import { AwaitIdle } from "../../../src/features/observe/AwaitIdle"; import { BootedDevice } from "../../../src/models/DeviceInfo"; import path from "path"; import sinon from "sinon"; describe("TakeScreenshot", function() { describe("Unit Tests for Extracted Methods", function() { let takeScreenshot: TakeScreenshot; let mockAdb: AdbUtils; let mockDevice: BootedDevice; beforeEach(function() { mockDevice = { name: "test-device", platform: "android", deviceId: "test-device-id", source: "local" }; // Create a simple mock ADB for unit testing mockAdb = { executeCommand: async () => ({ stdout: "", stderr: "" }) } as unknown as AdbUtils; takeScreenshot = new TakeScreenshot(mockDevice, mockAdb); }); it("should generate correct screenshot path with png format", function() { const timestamp = 1234567890123; const options = { format: "png" as const }; const result = takeScreenshot.generateScreenshotPath(timestamp, options); expect(result).to.include("screenshot_1234567890123"); expect(result).to.match(/screenshot_1234567890123\.png$/); }); it("should generate correct screenshot path with webp format", function() { const timestamp = 1234567890456; const options = { format: "webp" as const }; const result = takeScreenshot.generateScreenshotPath(timestamp, options); expect(result).to.include("screenshot_1234567890456"); expect(result).to.match(/screenshot_1234567890456\.webp$/); }); it("should generate different timestamps for consecutive calls", async function() { const timestamp1 = Date.now(); const options = { format: "png" as const }; const result1 = takeScreenshot.generateScreenshotPath(timestamp1, options); // Add a small delay to ensure different timestamps await new Promise(resolve => setTimeout(resolve, 10)); const timestamp2 = Date.now(); const result2 = takeScreenshot.generateScreenshotPath(timestamp2, options); expect(result1).to.not.equal(result2); }); it("should use single optimized ADB command for screenshot capture", async function() { // Create minimal valid PNG base64 data const base64PngData = Buffer.from("fake-png-data").toString("base64"); const mockExecuteCommand = sinon.spy(async (command: string) => { if (command.includes("screencap") && command.includes("base64")) { return { stdout: base64PngData, stderr: "" }; } return { stdout: "", stderr: "" }; }); const mockAdb = { executeCommand: mockExecuteCommand } as unknown as AdbUtils; // Mock all fs operations to avoid actual file I/O const mockFsWriteFile = sinon.stub(fs, "writeFile").resolves(); const mockFsExistsSync = sinon.stub(fs, "existsSync").returns(true); const mockFsMkdirSync = sinon.stub(fs, "mkdirSync").returns(undefined); const mockFsReaddir = sinon.stub(fs, "readdir").resolves([]); const mockFsStat = sinon.stub(fs, "stat").resolves({ size: 0, mtime: new Date() } as any); try { const takeScreenshot = new TakeScreenshot(mockDevice, mockAdb); // Mock the window dependency to avoid additional ADB calls const mockWindow = { getActiveHash: sinon.stub().resolves("mock-hash") }; (takeScreenshot as any).window = mockWindow; const result = await takeScreenshot.execute(); // Verify only one ADB command was executed (optimized) expect(mockExecuteCommand.calledOnce).to.be.true; // Verify the command uses the optimized base64 approach const calledCommand = mockExecuteCommand.getCall(0).args[0]; expect(calledCommand).to.include("screencap"); expect(calledCommand).to.include("base64"); expect(calledCommand).to.include("rm"); // Should cleanup temp file in same command expect(result.success).to.be.true; } finally { // Restore stubs mockFsWriteFile.restore(); mockFsExistsSync.restore(); mockFsMkdirSync.restore(); mockFsReaddir.restore(); mockFsStat.restore(); } }); }); describe("Integration Tests", function() { this.timeout(30000); let takeScreenshot: TakeScreenshot; let adb: AdbUtils; let awaitIdle: AwaitIdle; let testDevice: BootedDevice; const CLOCK_PACKAGE = "com.google.android.deskclock"; beforeEach(async function() { testDevice = { name: "test-device", platform: "android", deviceId: "test-device-id", source: "local" }; // Initialize with real ADB connection adb = new AdbUtils(testDevice); takeScreenshot = new TakeScreenshot(testDevice, adb); awaitIdle = new AwaitIdle(testDevice, adb); // Check if any devices are connected try { const devices = await adb.executeCommand("devices"); const deviceLines = devices.stdout.split("\n").filter(line => line.trim() && !line.includes("List of devices")); if (deviceLines.length === 0) { this.skip(); // Skip tests if no devices are connected return; } } catch (error) { this.skip(); // Skip tests if ADB command fails return; } // Make sure the app is not running await adb.executeCommand(`shell am force-stop ${CLOCK_PACKAGE}`); // Clear app data to ensure consistent state await adb.executeCommand(`shell pm clear ${CLOCK_PACKAGE}`); // Launch the clock app await adb.executeCommand(`shell am start -n ${CLOCK_PACKAGE}/com.android.deskclock.DeskClock`); // Wait for app to fully launch and UI to be stable await awaitIdle.waitForUiStability(CLOCK_PACKAGE, 250); }); afterEach(async function() { // Only run cleanup if this test wasn't skipped if (this.currentTest?.state === "pending") { return; } try { await adb.executeCommand(`shell am force-stop ${CLOCK_PACKAGE}`); } catch (error) { // Ignore cleanup errors } }); it("should take a screenshot of the Clock app with optimized performance", async function() { const startTime = Date.now(); const result = await takeScreenshot.execute({ format: "png" }); const duration = Date.now() - startTime; expect(result.success).to.be.true; expect(result.path).to.not.be.undefined; // Verify performance improvement with single ADB command expect(duration).to.be.lessThan(3000); // Should be faster with optimization const fileExists = fs.existsSync(result.path!); expect(fileExists).to.be.true; const fileStats = fs.statSync(result.path!); expect(fileStats.size).to.be.greaterThan(1000); // Sanitize path before reading file const normalizedPath = path.normalize(result.path!); if (normalizedPath.includes("..")) { throw new Error("Path traversal attempt detected"); } const imageBuffer = fs.readFileSync(normalizedPath); const image = Image.fromBuffer(imageBuffer); const metadata = await image.getMetadata(); expect(metadata.format).to.equal("png"); expect(metadata.width).to.be.greaterThan(100); expect(metadata.height).to.be.greaterThan(100); logger.info(`Screenshot saved at: ${result.path} (took ${duration}ms)`); }); it("should always create new screenshots with unique paths (no screenshot caching)", async function() { const result1 = await takeScreenshot.execute({ format: "png" }); expect(result1.success).to.be.true; await new Promise(resolve => setTimeout(resolve, 100)); const result2 = await takeScreenshot.execute({ format: "png" }); expect(result2.success).to.be.true; // Screenshots are NOT cached - each call creates a new file expect(result2.path).to.not.equal(result1.path); const filename1 = path.basename(result1.path!); const filename2 = path.basename(result2.path!); // Both should use "screenshot" prefix with unique timestamps expect(filename1.startsWith("screenshot_")).to.be.true; expect(filename2.startsWith("screenshot_")).to.be.true; // Verify both files exist and are different expect(fs.existsSync(result1.path!)).to.be.true; expect(fs.existsSync(result2.path!)).to.be.true; }); it("should convert format correctly when using webp", async function() { const pngResult = await takeScreenshot.execute({ format: "png" }); expect(pngResult.success).to.be.true; expect(pngResult.path!.endsWith(".png")).to.be.true; const webpResult = await takeScreenshot.execute({ format: "webp", quality: 80 }); expect(webpResult.success).to.be.true; expect(webpResult.path!.endsWith(".webp")).to.be.true; expect(fs.existsSync(pngResult.path!)).to.be.true; expect(fs.existsSync(webpResult.path!)).to.be.true; expect(pngResult.path).to.not.equal(webpResult.path); // Sanitize path before reading file const normalizedPath = path.normalize(webpResult.path!); if (normalizedPath.includes("..")) { throw new Error("Path traversal attempt detected"); } const webpBuffer = fs.readFileSync(normalizedPath); const webpImage = Image.fromBuffer(webpBuffer); const metadata = await webpImage.getMetadata(); expect(metadata.format).to.equal("webp"); }); it("should handle webp lossless conversion correctly", async function() { const webpResult = await takeScreenshot.execute({ format: "webp", quality: 90, lossless: true }); expect(webpResult.success).to.be.true; expect(webpResult.path!.endsWith(".webp")).to.be.true; expect(fs.existsSync(webpResult.path!)).to.be.true; // Sanitize path before reading file const normalizedPath = path.normalize(webpResult.path!); if (normalizedPath.includes("..")) { throw new Error("Path traversal attempt detected"); } const webpBuffer = fs.readFileSync(normalizedPath); const webpImage = Image.fromBuffer(webpBuffer); const metadata = await webpImage.getMetadata(); expect(metadata.format).to.equal("webp"); }); it("should always create new screenshots with unique timestamps", async function() { const results = []; for (let i = 0; i < 3; i++) { const result = await takeScreenshot.execute({ format: "png" }); expect(result.success).to.be.true; results.push(result); await new Promise(resolve => setTimeout(resolve, 50)); } const paths = results.map(r => r.path); const uniquePaths = new Set(paths); expect(uniquePaths.size).to.equal(results.length); // All should use "screenshot" prefix with unique timestamps for (const path of paths) { const filename = path ? path.split("/").pop() : ""; expect(filename!.startsWith("screenshot_")).to.be.true; expect(filename!.endsWith(".png")).to.be.true; } }); it("should handle errors gracefully when ADB command fails", async function() { // Create a takeScreenshot instance with a mock that fails const failingAdb = { executeCommand: async () => { throw new Error("ADB command failed"); } } as unknown as AdbUtils; const failingTakeScreenshot = new TakeScreenshot(mockDevice, failingAdb); const result = await failingTakeScreenshot.execute({ format: "png" }); expect(result.success).to.be.false; expect(result.error).to.include("ADB command failed"); expect(result.path).to.be.undefined; }); }); });

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