Files

176 lines
7.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* T2.1 — CoordinateResolver unit tests
*
* Covers: 4 coordinate modes × Gemini yx swap × Retina/HiDPI × multi-display offset × boundary clamp
*/
import { describe, it, expect } from 'vitest';
import { resolveCoordinate, type ScreenshotMeta, type DisplayInfo } from '../../src/coordinates/resolver.js';
import type { ModelProfile } from '../../src/coordinates/modelProfiles.js';
// --- Helpers ---
const primaryDisplay: DisplayInfo = { origin: { x: 0, y: 0 }, bounds: { width: 1920, height: 1080 } };
const secondaryDisplay: DisplayInfo = { origin: { x: -2560, y: 0 }, bounds: { width: 2560, height: 1440 } };
const retinaMeta: ScreenshotMeta = { imageWidth: 1440, imageHeight: 900, logicalWidth: 1440, logicalHeight: 900 };
const fullHdMeta: ScreenshotMeta = { imageWidth: 1920, imageHeight: 1080, logicalWidth: 1920, logicalHeight: 1080 };
const xyProfile = (mode: ModelProfile['coordinateMode']): ModelProfile => ({ coordinateMode: mode, coordinateOrder: 'xy' });
const geminiProfile: ModelProfile = { coordinateMode: 'normalized-999', coordinateOrder: 'yx' };
// --- Tests ---
describe('resolveCoordinate', () => {
describe('image-absolute mode', () => {
it('converts absolute pixel coordinates on 1920x1080', () => {
const result = resolveCoordinate(960, 540, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(960);
expect(result.globalY).toBe(540);
expect(result.normalizedX).toBeCloseTo(0.5, 2);
expect(result.normalizedY).toBeCloseTo(0.5, 2);
});
it('handles top-left corner (0, 0)', () => {
const result = resolveCoordinate(0, 0, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(0);
expect(result.globalY).toBe(0);
});
it('handles bottom-right corner', () => {
const result = resolveCoordinate(1920, 1080, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
// 1920/1920 * 1920 = 1920, clamped to bounds.width-1 = 1919
expect(result.globalX).toBe(1919);
expect(result.globalY).toBe(1079);
});
});
describe('normalized-1000 mode', () => {
it('converts 500, 500 → center of display', () => {
const result = resolveCoordinate(500, 500, xyProfile('normalized-1000'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(960);
expect(result.globalY).toBe(540);
});
it('converts 0, 0 → top-left', () => {
const result = resolveCoordinate(0, 0, xyProfile('normalized-1000'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(0);
expect(result.globalY).toBe(0);
});
it('converts 1000, 1000 → bottom-right (clamped)', () => {
const result = resolveCoordinate(1000, 1000, xyProfile('normalized-1000'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(1919);
expect(result.globalY).toBe(1079);
});
});
describe('normalized-999 mode', () => {
it('converts 499, 499 → approximate center', () => {
const result = resolveCoordinate(499, 499, xyProfile('normalized-999'), fullHdMeta, primaryDisplay);
// 499/999 ≈ 0.4995 → 0.4995 * 1920 ≈ 959
expect(result.globalX).toBeGreaterThanOrEqual(958);
expect(result.globalX).toBeLessThanOrEqual(960);
});
});
describe('normalized-0-1 mode', () => {
it('converts 0.5, 0.5 → center', () => {
const result = resolveCoordinate(0.5, 0.5, xyProfile('normalized-0-1'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(960);
expect(result.globalY).toBe(540);
});
it('converts 0, 0 → top-left', () => {
const result = resolveCoordinate(0, 0, xyProfile('normalized-0-1'), fullHdMeta, primaryDisplay);
expect(result.globalX).toBe(0);
expect(result.globalY).toBe(0);
});
});
describe('Gemini yx coordinate order swap', () => {
it('swaps yx to xy before normalization', () => {
// Gemini outputs [y, x] = [499, 200] → should become rawX=200, rawY=499
const result = resolveCoordinate(499, 200, geminiProfile, fullHdMeta, primaryDisplay);
// rawX = 200 (from modelY), rawY = 499 (from modelX)
// normX = 200/999 ≈ 0.2002, normY = 499/999 ≈ 0.4995
// localX = 0.2002 * 1920 ≈ 384, localY = 0.4995 * 1080 ≈ 539
expect(result.normalizedX).toBeCloseTo(200 / 999, 3);
expect(result.normalizedY).toBeCloseTo(499 / 999, 3);
expect(result.globalX).toBeGreaterThanOrEqual(383);
expect(result.globalX).toBeLessThanOrEqual(385);
});
it('Gemini center point [500, 500] should still map to center', () => {
// [y=500, x=500] → swap → rawX=500, rawY=500
const result = resolveCoordinate(500, 500, geminiProfile, fullHdMeta, primaryDisplay);
expect(result.normalizedX).toBeCloseTo(500 / 999, 3);
expect(result.normalizedY).toBeCloseTo(500 / 999, 3);
});
});
describe('Retina display (scaleFactor absorbed at screenshot level)', () => {
// Retina: physical 2880x1800, scaleFactor=2, logical 1440x900
// After screenshot pipeline: imageWidth=1440, imageHeight=900 (already logical)
// logicalWidth=1440, logicalHeight=900
it('converts center on Retina display (image-absolute)', () => {
const result = resolveCoordinate(720, 450, xyProfile('image-absolute'), retinaMeta, {
origin: { x: 0, y: 0 },
bounds: { width: 1440, height: 900 },
});
expect(result.globalX).toBe(720);
expect(result.globalY).toBe(450);
expect(result.normalizedX).toBeCloseTo(0.5, 2);
expect(result.normalizedY).toBeCloseTo(0.5, 2);
});
});
describe('HiDPI Windows (scaleFactor=1.5)', () => {
// Physical 2880x1620, scaleFactor=1.5, logical 1920x1080
// Screenshot pipeline outputs imageWidth=1920, imageHeight=1080
const hidpiMeta: ScreenshotMeta = { imageWidth: 1920, imageHeight: 1080, logicalWidth: 1920, logicalHeight: 1080 };
it('converts absolute coords on HiDPI display', () => {
const result = resolveCoordinate(960, 540, xyProfile('image-absolute'), hidpiMeta, primaryDisplay);
expect(result.globalX).toBe(960);
expect(result.globalY).toBe(540);
});
});
describe('multi-display offset', () => {
it('applies negative origin offset for secondary display on left', () => {
// Secondary display at origin (-2560, 0), 2560x1440
const meta: ScreenshotMeta = { imageWidth: 1920, imageHeight: 1080, logicalWidth: 2560, logicalHeight: 1440 };
const result = resolveCoordinate(960, 540, xyProfile('image-absolute'), meta, secondaryDisplay);
// normX = 960/1920 = 0.5, localX = 0.5 * 2560 = 1280
// globalX = 1280 + (-2560) = -1280
expect(result.localX).toBe(1280);
expect(result.globalX).toBe(-1280);
expect(result.localY).toBe(720);
expect(result.globalY).toBe(720);
});
it('primary display has zero offset', () => {
const result = resolveCoordinate(100, 100, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
expect(result.localX).toBe(result.globalX);
expect(result.localY).toBe(result.globalY);
});
});
describe('boundary clamp', () => {
it('clamps coordinates exceeding display bounds', () => {
// Send coordinates beyond the display
const result = resolveCoordinate(2500, 1500, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
// normX = 2500/1920 ≈ 1.302, localX = 1.302 * 1920 ≈ 2500 → clamped to 1919
expect(result.globalX).toBe(1919);
expect(result.globalY).toBe(1079);
});
it('clamps negative coordinates', () => {
const result = resolveCoordinate(-100, -100, xyProfile('image-absolute'), fullHdMeta, primaryDisplay);
// normX = -100/1920 < 0, localX < 0 → clamped to 0
expect(result.globalX).toBe(0);
expect(result.globalY).toBe(0);
});
});
});