v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user