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

295 lines
8.7 KiB
TypeScript

/**
* Unit tests for desktop/display.ts — macOS system_profiler parsing,
* nut.js fallback with scaleFactor detection, getDisplay, getPrimaryDisplay.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// --- Mock platform ---
const mockGetPlatform = vi.fn();
vi.mock('../../src/utils/platform.js', () => ({
getPlatform: () => mockGetPlatform(),
}));
// --- Mock logger ---
vi.mock('../../src/utils/logger.js', () => ({
logInfo: vi.fn(),
logWarn: vi.fn(),
logError: vi.fn(),
logDebug: vi.fn(),
}));
// --- Mock child_process (for macOS system_profiler) ---
const mockExecSync = vi.fn();
vi.mock('child_process', () => ({
execSync: (...args: any[]) => mockExecSync(...args),
}));
// --- Mock nut.js (for fallback) ---
const mockScreenWidth = vi.fn();
const mockScreenHeight = vi.fn();
const mockGrabRegion = vi.fn();
vi.mock('@nut-tree-fork/nut-js', () => {
class MockRegion {
constructor(
public left: number,
public top: number,
public width: number,
public height: number,
) {}
}
return {
screen: {
width: () => mockScreenWidth(),
height: () => mockScreenHeight(),
grabRegion: (r: any) => mockGrabRegion(r),
},
Region: MockRegion,
};
});
import { listDisplays, getDisplay, getPrimaryDisplay } from '../../src/desktop/display.js';
// --- Helpers: system_profiler JSON fixtures ---
function macSingleDisplay(opts: {
resolution?: string;
pixels?: string;
retina?: string;
name?: string;
main?: string;
} = {}) {
return JSON.stringify({
SPDisplaysDataType: [
{
spdisplays_ndrvs: [
{
_spdisplays_resolution: opts.resolution ?? '1440 x 900 @ 60.00Hz',
_spdisplays_pixels: opts.pixels ?? '2880 x 1800',
spdisplays_retina: opts.retina ?? 'spdisplays_yes',
_name: opts.name ?? 'Built-in Retina Display',
spdisplays_main: opts.main ?? 'spdisplays_yes',
},
],
},
],
});
}
function macDualDisplay() {
return JSON.stringify({
SPDisplaysDataType: [
{
spdisplays_ndrvs: [
{
_spdisplays_resolution: '1440 x 900 @ 60.00Hz',
_spdisplays_pixels: '2880 x 1800',
spdisplays_retina: 'spdisplays_yes',
_name: 'Built-in Retina Display',
spdisplays_main: 'spdisplays_yes',
},
{
_spdisplays_resolution: '2560 x 1440',
_spdisplays_pixels: '2560 x 1440',
spdisplays_retina: 'spdisplays_no',
_name: 'External Monitor',
},
],
},
],
});
}
describe('listDisplays', () => {
beforeEach(() => {
vi.clearAllMocks();
});
// --- macOS via system_profiler ---
describe('macOS (system_profiler)', () => {
beforeEach(() => {
mockGetPlatform.mockReturnValue('macos');
});
it('parses single Retina display', async () => {
mockExecSync.mockReturnValue(macSingleDisplay());
const displays = await listDisplays();
expect(displays).toHaveLength(1);
expect(displays[0]).toMatchObject({
index: 0,
label: 'Built-in Retina Display',
width: 1440,
height: 900,
scaleFactor: 2,
isPrimary: true,
origin: { x: 0, y: 0 },
});
});
it('parses dual display setup', async () => {
mockExecSync.mockReturnValue(macDualDisplay());
const displays = await listDisplays();
expect(displays).toHaveLength(2);
expect(displays[0].label).toBe('Built-in Retina Display');
expect(displays[0].isPrimary).toBe(true);
expect(displays[0].scaleFactor).toBe(2);
expect(displays[1].label).toBe('External Monitor');
expect(displays[1].width).toBe(2560);
expect(displays[1].height).toBe(1440);
expect(displays[1].scaleFactor).toBe(1);
});
it('detects Retina via flag when pixel resolution missing', async () => {
mockExecSync.mockReturnValue(macSingleDisplay({
pixels: '', // no pixel resolution
retina: 'spdisplays_yes',
resolution: '1440 x 900',
}));
const displays = await listDisplays();
expect(displays).toHaveLength(1);
expect(displays[0].scaleFactor).toBe(2); // falls back to retina flag
});
it('defaults scaleFactor 1 for non-Retina without pixel info', async () => {
mockExecSync.mockReturnValue(macSingleDisplay({
pixels: '',
retina: 'spdisplays_no',
resolution: '1920 x 1080',
}));
const displays = await listDisplays();
expect(displays[0].scaleFactor).toBe(1);
});
it('falls back to nut.js when system_profiler throws', async () => {
mockExecSync.mockImplementation(() => { throw new Error('command not found'); });
mockScreenWidth.mockResolvedValue(1920);
mockScreenHeight.mockResolvedValue(1080);
mockGrabRegion.mockResolvedValue({ width: 10 });
const displays = await listDisplays();
expect(displays).toHaveLength(1);
expect(displays[0].width).toBe(1920);
expect(displays[0].scaleFactor).toBe(1);
});
it('falls back when system_profiler returns empty displays', async () => {
mockExecSync.mockReturnValue(JSON.stringify({ SPDisplaysDataType: [] }));
mockScreenWidth.mockResolvedValue(1920);
mockScreenHeight.mockResolvedValue(1080);
mockGrabRegion.mockResolvedValue({ width: 20 });
const displays = await listDisplays();
expect(displays).toHaveLength(1);
expect(displays[0].scaleFactor).toBe(2); // 20/10 = 2
});
it('uses display name from _name field', async () => {
mockExecSync.mockReturnValue(macSingleDisplay({ name: 'LG UltraFine' }));
const displays = await listDisplays();
expect(displays[0].label).toBe('LG UltraFine');
});
});
// --- Windows/Linux fallback ---
describe('fallback (nut.js)', () => {
beforeEach(() => {
mockGetPlatform.mockReturnValue('windows');
});
it('returns single display with correct dimensions', async () => {
mockScreenWidth.mockResolvedValue(1920);
mockScreenHeight.mockResolvedValue(1080);
mockGrabRegion.mockResolvedValue({ width: 10 });
const displays = await listDisplays();
expect(displays).toHaveLength(1);
expect(displays[0]).toMatchObject({
index: 0,
label: 'Primary',
width: 1920,
height: 1080,
scaleFactor: 1,
isPrimary: true,
origin: { x: 0, y: 0 },
});
});
it('detects HiDPI scaleFactor from capture', async () => {
mockScreenWidth.mockResolvedValue(1920);
mockScreenHeight.mockResolvedValue(1080);
mockGrabRegion.mockResolvedValue({ width: 20 }); // 20/10 = 2x
const displays = await listDisplays();
expect(displays[0].scaleFactor).toBe(2);
});
it('defaults scaleFactor to 1 when capture fails', async () => {
mockScreenWidth.mockResolvedValue(1920);
mockScreenHeight.mockResolvedValue(1080);
mockGrabRegion.mockRejectedValue(new Error('no display'));
const displays = await listDisplays();
expect(displays[0].scaleFactor).toBe(1);
});
it('works on Linux', async () => {
mockGetPlatform.mockReturnValue('linux');
mockScreenWidth.mockResolvedValue(2560);
mockScreenHeight.mockResolvedValue(1440);
mockGrabRegion.mockResolvedValue({ width: 10 });
const displays = await listDisplays();
expect(displays[0].width).toBe(2560);
expect(displays[0].height).toBe(1440);
});
});
});
describe('getDisplay', () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetPlatform.mockReturnValue('macos');
mockExecSync.mockReturnValue(macDualDisplay());
});
it('returns display by index', async () => {
const d = await getDisplay(1);
expect(d.index).toBe(1);
expect(d.label).toBe('External Monitor');
});
it('throws DesktopError for out-of-range index', async () => {
await expect(getDisplay(5)).rejects.toThrow('Display index 5 not found');
});
});
describe('getPrimaryDisplay', () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetPlatform.mockReturnValue('macos');
});
it('returns the primary display', async () => {
mockExecSync.mockReturnValue(macDualDisplay());
const d = await getPrimaryDisplay();
expect(d.isPrimary).toBe(true);
expect(d.label).toBe('Built-in Retina Display');
});
it('falls back to first display if none marked primary', async () => {
// Single display with no main flag — first display still returned
mockExecSync.mockReturnValue(macSingleDisplay({ main: '' }));
const d = await getPrimaryDisplay();
// First display is always treated as primary when displays.length === 0 before push
expect(d.index).toBe(0);
});
});