// Unit tests for topology spatial predicates (V1.1) import { describe, it } from 'node:test'; import assert from 'node:assert'; import { above, below, leftOf, rightOf, inside, overlaps, PredicateTruth, } from './predicate-queries.js'; import { GeometryWorld, StringTable, internString, } from 'imhotep-geometry'; // --------------------------------------------------------------------------- // Helpers: build a minimal GeometryWorld with just enough box data // --------------------------------------------------------------------------- function makeMinimalWorldWithBoxes( boxes: Array<{ subjectId: number; left: number; top: number; right: number; bottom: number }>, ): GeometryWorld { const strings: StringTable = { values: [] }; const n = boxes.length; const boxId = new Uint32Array(n); const subjectId = new Uint32Array(n); const frameId = new Uint32Array(n); const borderLeft = new Float64Array(n); const borderTop = new Float64Array(n); const borderRight = new Float64Array(n); const borderBottom = new Float64Array(n); const paddingLeft = new Float64Array(n); const paddingTop = new Float64Array(n); const paddingRight = new Float64Array(n); const paddingBottom = new Float64Array(n); const contentLeft = new Float64Array(n); const contentTop = new Float64Array(n); const contentRight = new Float64Array(n); const contentBottom = new Float64Array(n); for (let i = 0; i < n; i++) { const b = boxes[i]; boxId[i] = i + 1; subjectId[i] = b.subjectId; frameId[i] = 0; contentLeft[i] = b.left; contentTop[i] = b.top; contentRight[i] = b.right; contentBottom[i] = b.bottom; } const emptyEnv = { viewportWidth: 1440, viewportHeight: 900, deviceScaleFactor: 1, colorScheme: internString(strings, 'dark'), pointer: internString(strings, 'fine'), hover: internString(strings, 'none'), reducedMotion: false, locale: internString(strings, 'en-US'), writingMode: internString(strings, 'horizontal-tb'), }; const emptySource = { url: internString(strings, 'https://example.test'), browserName: internString(strings, 'Chromium'), browserVersion: internString(strings, '123'), engine: internString(strings, 'chromium-cdp'), extractedAt: Date.now(), }; return { sceneId: internString(strings, 'scene'), snapshotId: internString(strings, 'snap'), env: emptyEnv, source: emptySource, strings, subjects: { ids: new Uint32Array(0), domNodeId: new Uint32Array(0), subjectKind: new Uint16Array(0), primaryBoxId: new Uint32Array(0), firstFragmentId: new Uint32Array(0), fragmentCount: new Uint16Array(0), firstTextRunId: new Uint32Array(0), textRunCount: new Uint16Array(0), }, dom: { nodeId: new Uint32Array(0), backendNodeId: new Uint32Array(0), parentNodeId: new Uint32Array(0), firstChildIndex: new Uint32Array(0), childCount: new Uint16Array(0), shadowRootKind: new Uint8Array(0), tagNameStringId: new Uint32Array(0), classNameStringIds: [], roleStringId: new Uint32Array(0), ariaNameStringId: new Uint32Array(0), }, frames: { frameId: new Uint32Array(0), frameKind: new Uint16Array(0), ownerSubjectId: new Uint32Array(0), parentFrameId: new Uint32Array(0), originX: new Float64Array(0), originY: new Float64Array(0), axisMatrixStart: new Int32Array(0), clipRectId: new Uint32Array(0), scrollContainerId: new Uint32Array(0), writingMode: new Uint16Array(0), }, matrices: { values: new Float64Array(0) }, rects: { rectId: new Uint32Array(0), left: new Float64Array(0), top: new Float64Array(0), right: new Float64Array(0), bottom: new Float64Array(0) }, boxes: { boxId, subjectId, frameId, borderLeft, borderTop, borderRight, borderBottom, paddingLeft, paddingTop, paddingRight, paddingBottom, contentLeft, contentTop, contentRight, contentBottom, }, fragments: { fragmentId: new Uint32Array(0), subjectId: new Uint32Array(0), fragmentKind: new Uint16Array(0), boxLeft: new Float64Array(0), boxTop: new Float64Array(0), boxRight: new Float64Array(0), boxBottom: new Float64Array(0), lineIndex: new Int32Array(0), flowIndex: new Int32Array(0), parentFragmentId: new Uint32Array(0), }, transforms: { transformId: new Uint32Array(0), subjectId: new Uint32Array(0), matrixStart: new Int32Array(0), matrixLength: new Uint16Array(0), originX: new Float64Array(0), originY: new Float64Array(0), }, styles: { subjectId: new Uint32Array(0), display: new Uint16Array(0), position: new Uint16Array(0), zIndexKind: new Uint8Array(0), zIndexValue: new Int32Array(0), overflowX: new Uint16Array(0), overflowY: new Uint16Array(0), opacity: new Float32Array(0), visibility: new Uint16Array(0), containFlags: new Uint32Array(0), pointerEvents: new Uint16Array(0), lineHeight: new Float64Array(0), fontFamilyStringId: new Uint32Array(0), fontSize: new Float64Array(0), fontWeight: new Uint16Array(0), }, text: { runId: new Uint32Array(0), subjectId: new Uint32Array(0), contentStringId: new Uint32Array(0), lineBoxId: new Uint32Array(0), inkLeft: new Float64Array(0), inkTop: new Float64Array(0), inkRight: new Float64Array(0), inkBottom: new Float64Array(0), baselineY: new Float64Array(0), capHeight: new Float64Array(0), computedLineHeight: new Float64Array(0), }, topology: { containingBlockOf: new Uint32Array(0), nearestPositionedAncestorOf: new Uint32Array(0), scrollContainerOf: new Uint32Array(0), stackingContextOf: new Uint32Array(0), formattingContextOf: new Uint32Array(0), clippingRootOf: new Uint32Array(0), paintOrderBucket: new Uint16Array(0), paintOrderIndex: new Uint32Array(0), }, scroll: { containerId: new Uint32Array(0), scrollLeft: new Float64Array(0), scrollTop: new Float64Array(0), scrollWidth: new Float64Array(0), scrollHeight: new Float64Array(0), clientWidth: new Float64Array(0), clientHeight: new Float64Array(0), }, clipping: { clipNodeId: new Uint32Array(0), subjectId: new Uint32Array(0), clipKind: new Uint16Array(0), clipLeft: new Float64Array(0), clipTop: new Float64Array(0), clipRight: new Float64Array(0), clipBottom: new Float64Array(0), parentClipNodeId: new Uint32Array(0), }, paint: { paintNodeId: new Uint32Array(0), subjectId: new Uint32Array(0), stackingContextId: new Uint32Array(0), bucket: new Uint16Array(0), localPaintIndex: new Uint32Array(0), }, visibility: { subjectId: new Uint32Array(0), isRendered: new Uint8Array(0), isVisible: new Uint8Array(0), visibleArea: new Float64Array(0), clippedArea: new Float64Array(0), }, provenance: { factId: new Uint32Array(0), extractionStepId: new Uint32Array(0), sourceKind: new Uint16Array(0), sourceRef: new Uint32Array(0), }, confidence: { factId: new Uint32Array(0), confidence: new Float32Array(0), reasonCode: new Uint16Array(0), }, selectorIndex: new Map(), ancestorIndex: new Map(), lineBoxIndex: new Map(), textRunIndex: new Map(), }; } function assertTruth(result: PredicateTruth, expected: PredicateTruth['kind']) { assert.equal(result.kind, expected, `expected ${expected}, got ${result.kind}`); } // --------------------------------------------------------------------------- // Spatial predicate tests // --------------------------------------------------------------------------- describe('above', () => { it('returns true when a is above b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 100, bottom: 50 }, { subjectId: 2, left: 0, top: 60, right: 100, bottom: 100 }, ]); assertTruth(above(1, 2, world), 'true'); }); it('returns false when a is not above b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 60, right: 100, bottom: 100 }, { subjectId: 2, left: 0, top: 0, right: 100, bottom: 50 }, ]); assertTruth(above(1, 2, world), 'false'); }); it('returns indeterminate for missing box', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 100, bottom: 50 }, ]); const result = above(1, 99, world); assert.equal(result.kind, 'indeterminate'); if (result.kind === 'indeterminate') { assert.equal(result.reason, 'missing box geometry'); } }); }); describe('below', () => { it('returns true when a is below b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 60, right: 100, bottom: 100 }, { subjectId: 2, left: 0, top: 0, right: 100, bottom: 50 }, ]); assertTruth(below(1, 2, world), 'true'); }); it('returns false when a is not below b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 100, bottom: 50 }, { subjectId: 2, left: 0, top: 60, right: 100, bottom: 100 }, ]); assertTruth(below(1, 2, world), 'false'); }); }); describe('leftOf', () => { it('returns true when a is left of b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 50, bottom: 100 }, { subjectId: 2, left: 60, top: 0, right: 100, bottom: 100 }, ]); assertTruth(leftOf(1, 2, world), 'true'); }); it('returns false when a is not left of b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 60, top: 0, right: 100, bottom: 100 }, { subjectId: 2, left: 0, top: 0, right: 50, bottom: 100 }, ]); assertTruth(leftOf(1, 2, world), 'false'); }); }); describe('rightOf', () => { it('returns true when a is right of b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 60, top: 0, right: 100, bottom: 100 }, { subjectId: 2, left: 0, top: 0, right: 50, bottom: 100 }, ]); assertTruth(rightOf(1, 2, world), 'true'); }); it('returns false when a is not right of b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 50, bottom: 100 }, { subjectId: 2, left: 60, top: 0, right: 100, bottom: 100 }, ]); assertTruth(rightOf(1, 2, world), 'false'); }); }); describe('inside', () => { it('returns true when a is fully inside b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 10, top: 10, right: 90, bottom: 90 }, { subjectId: 2, left: 0, top: 0, right: 100, bottom: 100 }, ]); assertTruth(inside(1, 2, world), 'true'); }); it('returns false when a is partially outside b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 50, top: 50, right: 150, bottom: 150 }, { subjectId: 2, left: 0, top: 0, right: 100, bottom: 100 }, ]); assertTruth(inside(1, 2, world), 'false'); }); it('returns false when a is completely outside b', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 200, top: 200, right: 300, bottom: 300 }, { subjectId: 2, left: 0, top: 0, right: 100, bottom: 100 }, ]); assertTruth(inside(1, 2, world), 'false'); }); }); describe('overlaps', () => { it('returns true when boxes overlap', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 60, bottom: 60 }, { subjectId: 2, left: 40, top: 40, right: 100, bottom: 100 }, ]); assertTruth(overlaps(1, 2, world), 'true'); }); it('returns false when boxes do not overlap', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 40, bottom: 40 }, { subjectId: 2, left: 50, top: 50, right: 100, bottom: 100 }, ]); assertTruth(overlaps(1, 2, world), 'false'); }); it('returns false for edge-touching boxes (zero-area intersection)', () => { const world = makeMinimalWorldWithBoxes([ { subjectId: 1, left: 0, top: 0, right: 50, bottom: 50 }, { subjectId: 2, left: 50, top: 50, right: 100, bottom: 100 }, ]); assertTruth(overlaps(1, 2, world), 'false'); }); });