Files
qiming/qimingclaw/crates/agent-gui-server/tests/desktop/imageSearch.test.ts

134 lines
4.0 KiB
TypeScript

/**
* Unit tests for desktop/imageSearch.ts — findImage, waitForImage.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock fs
const mockWriteFile = vi.fn();
const mockUnlink = vi.fn();
vi.mock('fs', () => ({
promises: {
writeFile: (...args: any[]) => mockWriteFile(...args),
unlink: (...args: any[]) => mockUnlink(...args),
},
}));
// Mock nut.js
const mockFind = vi.fn();
const mockLoadImage = vi.fn();
const mockScreenConfig = { confidence: 0.9 };
vi.mock('@nut-tree-fork/nut-js', () => ({
screen: {
find: (...args: any[]) => mockFind(...args),
config: mockScreenConfig,
},
loadImage: (...args: any[]) => mockLoadImage(...args),
}));
import { findImage, waitForImage } from '../../src/desktop/imageSearch.js';
describe('findImage', () => {
beforeEach(() => {
vi.clearAllMocks();
mockWriteFile.mockResolvedValue(undefined);
mockUnlink.mockResolvedValue(undefined);
mockLoadImage.mockResolvedValue('template-obj');
});
it('returns found=true with region when match found', async () => {
mockFind.mockResolvedValue({ left: 10, top: 20, width: 50, height: 30 });
const result = await findImage('dGVzdA==', 0.85);
expect(result.found).toBe(true);
expect(result.region).toEqual({ x: 10, y: 20, width: 50, height: 30 });
expect(result.confidence).toBe(0.85);
expect(mockScreenConfig.confidence).toBe(0.85);
});
it('returns found=false when no match', async () => {
mockFind.mockRejectedValue(new Error('no match'));
const result = await findImage('dGVzdA==');
expect(result.found).toBe(false);
expect(result.region).toBeUndefined();
});
it('writes template to temp file and cleans up', async () => {
mockFind.mockResolvedValue({ left: 0, top: 0, width: 10, height: 10 });
await findImage('dGVzdA==');
expect(mockWriteFile).toHaveBeenCalledTimes(1);
expect(mockUnlink).toHaveBeenCalledTimes(1);
});
it('cleans up temp file even when match fails', async () => {
mockFind.mockRejectedValue(new Error('no match'));
await findImage('dGVzdA==');
expect(mockUnlink).toHaveBeenCalledTimes(1);
});
it('uses default confidence of 0.9', async () => {
mockFind.mockResolvedValue({ left: 0, top: 0, width: 10, height: 10 });
const result = await findImage('dGVzdA==');
expect(result.confidence).toBe(0.9);
});
});
describe('waitForImage', () => {
beforeEach(() => {
vi.clearAllMocks();
mockWriteFile.mockResolvedValue(undefined);
mockUnlink.mockResolvedValue(undefined);
mockLoadImage.mockResolvedValue('template-obj');
});
it('returns found=true with elapsed when image found immediately', async () => {
mockFind.mockResolvedValue({ left: 5, top: 10, width: 20, height: 15 });
const result = await waitForImage('dGVzdA==', 5000, 0.8);
expect(result.found).toBe(true);
expect(result.region).toEqual({ x: 5, y: 10, width: 20, height: 15 });
expect(result.elapsed).toBeDefined();
expect(result.elapsed).toBeGreaterThanOrEqual(0);
});
it('returns found=false with elapsed after timeout', async () => {
mockFind.mockRejectedValue(new Error('no match'));
// Use a very short timeout to avoid slow tests
const result = await waitForImage('dGVzdA==', 100, 0.9);
expect(result.found).toBe(false);
expect(result.elapsed).toBeGreaterThanOrEqual(100);
});
it('cleans up temp file after completion', async () => {
mockFind.mockResolvedValue({ left: 0, top: 0, width: 10, height: 10 });
await waitForImage('dGVzdA==', 5000);
expect(mockUnlink).toHaveBeenCalledTimes(1);
});
it('cleans up temp file even after timeout', async () => {
mockFind.mockRejectedValue(new Error('no match'));
await waitForImage('dGVzdA==', 100);
expect(mockUnlink).toHaveBeenCalledTimes(1);
});
it('writes temp file only once for reuse across polls', async () => {
mockFind.mockRejectedValue(new Error('no match'));
await waitForImage('dGVzdA==', 100);
// Only 1 writeFile call despite potentially multiple polls
expect(mockWriteFile).toHaveBeenCalledTimes(1);
});
});