/** Test doubles for the desktop helper and tosu. */ import type { TosuFeed, TosuFrame } from '../tosu'; import type { CaptureOptions, Desktop, OcrLine, SessionInfo, WindowInfo } from './desktop'; export function windowInfo(overrides: Partial = {}): WindowInfo { return { hwnd: 100, pid: 10, title: 'osu!', className: 'WindowsForms10.Window.8.app.0.1', exe: 'C:\\vash\\osu\\osu!.exe', visible: true, minimized: false, x: 0, y: 0, width: 1296, height: 959, clientX: 8, clientY: 31, clientWidth: 1280, clientHeight: 920, ...overrides }; } export class FakeDesktop implements Desktop { sessionInfo: SessionInfo = { sessionId: 1, consoleSessionId: 1, inputDesktop: 'Default', screenWidth: 1920, screenHeight: 1080, idleMs: 0 }; running: { pid: number; path: string }[] = []; windowList: WindowInfo[] = []; ocrLines: OcrLine[] = []; calls: { cmd: string; args: unknown[] }[] = []; focusResult = true; private note(cmd: string, ...args: unknown[]) { this.calls.push({ cmd, args }); } called(cmd: string) { return this.calls.filter((call) => call.cmd === cmd); } async session() { return this.sessionInfo; } async windows(under: string) { this.note('windows', under); return this.windowList; } async window(hwnd: number) { return this.windowList.find((window) => window.hwnd === hwnd) ?? null; } async processes(under: string) { return this.running.filter((process) => process.path.toLowerCase().startsWith(under.toLowerCase()) ); } async kill(pids: number[]) { this.note('kill', pids); this.running = this.running.filter((process) => !pids.includes(process.pid)); return pids.length; } async close(pid: number) { this.note('close', pid); this.running = this.running.filter((process) => process.pid !== pid); this.windowList = this.windowList.filter((window) => window.pid !== pid); return 1; } async focus(hwnd: number) { this.note('focus', hwnd); return this.focusResult; } async move(hwnd: number, x: number, y: number) { this.note('move', hwnd, x, y); return true; } async setTitle(hwnd: number, title: string) { this.note('title', hwnd, title); const window = this.windowList.find((each) => each.hwnd === hwnd); if (window) window.title = title; return true; } async capture(options: CaptureOptions) { this.note('capture', options); const scale = options.scale ?? 1; return { width: Math.round(options.width * scale), height: Math.round(options.height * scale) }; } async ocr(path: string) { this.note('ocr', path); return this.ocrLines; } async ocrReady() { return { language: 'en-US', maxDimension: 2600 }; } async click(x: number, y: number) { this.note('click', x, y); return true; } async type(text: string) { return text.length; } async key() {} async stayAwake() { return true; } stop() {} } /** Stands in for TosuFeed: whatever frame the test sets is the fresh one. */ export function fakeTosu(frame: TosuFrame | null = null) { const feed = { frame, fresh: () => feed.frame }; return feed as unknown as TosuFeed & { frame: TosuFrame | null }; } export function tourneyFrame(left: string, right: string): TosuFrame { return { ipcState: 1, beatmapId: null, progress: null, teams: { left, right }, clients: [] }; }