v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fc from 'fast-check';
|
||||
|
||||
import {
|
||||
GeometryWorld,
|
||||
StringTable,
|
||||
FrameKind,
|
||||
buildWorldIndex,
|
||||
internString,
|
||||
} from './world.js';
|
||||
|
||||
import {
|
||||
multiply4x4,
|
||||
translateMatrix,
|
||||
scaleMatrix,
|
||||
rotateMatrix,
|
||||
transformPoint,
|
||||
invert4x4,
|
||||
buildTransformChain,
|
||||
IDENTITY_4X4,
|
||||
} from './transforms.js';
|
||||
|
||||
import {
|
||||
convertPoint,
|
||||
} from './frames.js';
|
||||
|
||||
import {
|
||||
rectIntersection,
|
||||
rectsIntersect,
|
||||
} from './boxes.js';
|
||||
|
||||
// ---- Arbitraries ----
|
||||
|
||||
const finiteFloatArb = fc.integer({ min: -1000, max: 1000 });
|
||||
const positiveFloatArb = fc.float({ noNaN: true, noDefaultInfinity: true, min: Math.fround(0.1), max: Math.fround(100) });
|
||||
|
||||
const matrixArb = fc.record({
|
||||
tx: finiteFloatArb,
|
||||
ty: finiteFloatArb,
|
||||
sx: positiveFloatArb,
|
||||
sy: positiveFloatArb,
|
||||
angle: fc.float({ noNaN: true, noDefaultInfinity: true, min: Math.fround(0), max: Math.fround(Math.PI * 2) }),
|
||||
}).map(({ tx, ty, sx, sy, angle }) => {
|
||||
const t = translateMatrix(tx, ty);
|
||||
const r = rotateMatrix(angle);
|
||||
const s = scaleMatrix(sx, sy);
|
||||
const tr = multiply4x4(t, r);
|
||||
return multiply4x4(tr, s);
|
||||
});
|
||||
|
||||
const rectArb = fc.tuple(finiteFloatArb, finiteFloatArb, finiteFloatArb, finiteFloatArb)
|
||||
.filter(([l, t, r, b]) => l < r && t < b)
|
||||
.map(([l, t, r, b]) => ({ left: l, top: t, right: r, bottom: b }));
|
||||
|
||||
const stringArb = fc.string({ minLength: 1, maxLength: 50 });
|
||||
|
||||
// ---- Property Tests ----
|
||||
|
||||
describe('geometry property tests', () => {
|
||||
it('property: frame transform composition is associative', () => {
|
||||
fc.assert(
|
||||
fc.property(matrixArb, matrixArb, matrixArb, (a, b, c) => {
|
||||
const ab = multiply4x4(a, b);
|
||||
const abc1 = multiply4x4(ab, c);
|
||||
|
||||
const bc = multiply4x4(b, c);
|
||||
const abc2 = multiply4x4(a, bc);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
assert.ok(
|
||||
Math.abs(abc1[i] - abc2[i]) < 1e-5,
|
||||
`Mismatch at index ${i}: ${abc1[i]} vs ${abc2[i]}`
|
||||
);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 200, seed: 42 }
|
||||
);
|
||||
});
|
||||
|
||||
it('property: inverse transform recovers original coordinates', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.record({ tx: finiteFloatArb, ty: finiteFloatArb, sx: positiveFloatArb, sy: positiveFloatArb }),
|
||||
fc.tuple(finiteFloatArb, finiteFloatArb),
|
||||
({ tx, ty, sx, sy }, [x, y]) => {
|
||||
const m = multiply4x4(
|
||||
translateMatrix(tx, ty),
|
||||
scaleMatrix(sx, sy)
|
||||
);
|
||||
const inv = invert4x4(m);
|
||||
assert.ok(inv, 'Matrix should be invertible');
|
||||
|
||||
const transformed = transformPoint(m, x, y);
|
||||
const recovered = transformPoint(inv!, transformed[0], transformed[1]);
|
||||
|
||||
assert.ok(Math.abs(recovered[0] - x) < 1e-4, `x mismatch: ${recovered[0]} vs ${x}`);
|
||||
assert.ok(Math.abs(recovered[1] - y) < 1e-4, `y mismatch: ${recovered[1]} vs ${y}`);
|
||||
}
|
||||
),
|
||||
{ numRuns: 200, seed: 123 }
|
||||
);
|
||||
});
|
||||
|
||||
it('property: coordinate conversion is consistent (A->B->A = identity)', () => {
|
||||
// Build a minimal world with two frames: viewport (id=1) and child (id=2)
|
||||
const makeWorld = (originX: number, originY: number): GeometryWorld => {
|
||||
// String table indices: 0=scene_1, 1=snap_1, 2=en-US, 3=horizontal-tb
|
||||
const strings: StringTable = {
|
||||
values: ['scene_1', 'snap_1', 'en-US', 'horizontal-tb', 'div']
|
||||
};
|
||||
return {
|
||||
sceneId: 0, // index into strings
|
||||
snapshotId: 1,
|
||||
env: {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
deviceScaleFactor: 1,
|
||||
colorScheme: 0,
|
||||
pointer: 0,
|
||||
hover: 0,
|
||||
reducedMotion: false,
|
||||
locale: 2,
|
||||
writingMode: 3,
|
||||
},
|
||||
source: {
|
||||
url: 0,
|
||||
browserName: 0,
|
||||
browserVersion: 0,
|
||||
engine: 0,
|
||||
extractedAt: Date.now(),
|
||||
},
|
||||
strings,
|
||||
subjects: {
|
||||
ids: new Uint32Array([1]),
|
||||
domNodeId: new Uint32Array([10]),
|
||||
subjectKind: new Uint16Array([1]),
|
||||
primaryBoxId: new Uint32Array([100]),
|
||||
firstFragmentId: new Uint32Array([0]),
|
||||
fragmentCount: new Uint16Array([0]),
|
||||
firstTextRunId: new Uint32Array([0]),
|
||||
textRunCount: new Uint16Array([0]),
|
||||
},
|
||||
dom: {
|
||||
nodeId: new Uint32Array([10]),
|
||||
backendNodeId: new Uint32Array([1000]),
|
||||
parentNodeId: new Uint32Array([0]),
|
||||
firstChildIndex: new Uint32Array([0]),
|
||||
childCount: new Uint16Array([0]),
|
||||
shadowRootKind: new Uint8Array([0]),
|
||||
tagNameStringId: new Uint32Array([4]),
|
||||
classNameStringIds: [],
|
||||
roleStringId: new Uint32Array([0]),
|
||||
ariaNameStringId: new Uint32Array([0]),
|
||||
},
|
||||
boxes: {
|
||||
boxId: new Uint32Array([100]),
|
||||
subjectId: new Uint32Array([1]),
|
||||
frameId: new Uint32Array([2]),
|
||||
borderLeft: new Float64Array([0]),
|
||||
borderTop: new Float64Array([0]),
|
||||
borderRight: new Float64Array([100]),
|
||||
borderBottom: new Float64Array([100]),
|
||||
paddingLeft: new Float64Array([0]),
|
||||
paddingTop: new Float64Array([0]),
|
||||
paddingRight: new Float64Array([100]),
|
||||
paddingBottom: new Float64Array([100]),
|
||||
contentLeft: new Float64Array([0]),
|
||||
contentTop: new Float64Array([0]),
|
||||
contentRight: new Float64Array([100]),
|
||||
contentBottom: new Float64Array([100]),
|
||||
},
|
||||
rects: {
|
||||
rectId: new Uint32Array([1]),
|
||||
left: new Float64Array([0]),
|
||||
top: new Float64Array([0]),
|
||||
right: new Float64Array([1440]),
|
||||
bottom: new Float64Array([900]),
|
||||
},
|
||||
frames: {
|
||||
frameId: new Uint32Array([1, 2]),
|
||||
frameKind: new Uint16Array([FrameKind.Viewport, FrameKind.ContainingBlock]),
|
||||
parentFrameId: new Uint32Array([0, 1]),
|
||||
ownerSubjectId: new Uint32Array([0, 1]),
|
||||
originX: new Float64Array([0, originX]),
|
||||
originY: new Float64Array([0, originY]),
|
||||
clipRectId: new Uint32Array([0, 0]),
|
||||
scrollContainerId: new Uint32Array([0, 0]),
|
||||
writingMode: new Uint16Array([3, 3]),
|
||||
axisMatrixStart: new Int32Array([-1, -1]),
|
||||
},
|
||||
matrices: { values: new Float64Array(0) },
|
||||
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([1]),
|
||||
display: new Uint16Array([1]),
|
||||
position: new Uint16Array([1]),
|
||||
zIndexKind: new Uint8Array([0]),
|
||||
zIndexValue: new Int32Array([0]),
|
||||
overflowX: new Uint16Array([1]),
|
||||
overflowY: new Uint16Array([1]),
|
||||
opacity: new Float32Array([1]),
|
||||
visibility: new Uint16Array([1]),
|
||||
containFlags: new Uint32Array([0]),
|
||||
pointerEvents: new Uint16Array([1]),
|
||||
lineHeight: new Float64Array([1.5]),
|
||||
fontFamilyStringId: new Uint32Array([0]),
|
||||
fontSize: new Float64Array([16]),
|
||||
fontWeight: new Uint16Array([400]),
|
||||
},
|
||||
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, 0]),
|
||||
nearestPositionedAncestorOf: new Uint32Array([0, 0]),
|
||||
scrollContainerOf: new Uint32Array([0, 0]),
|
||||
stackingContextOf: new Uint32Array([0, 0]),
|
||||
formattingContextOf: new Uint32Array([0, 0]),
|
||||
clippingRootOf: new Uint32Array([0, 0]),
|
||||
paintOrderBucket: new Uint16Array([0, 0]),
|
||||
paintOrderIndex: new Uint32Array([0, 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([1]),
|
||||
isRendered: new Uint8Array([1]),
|
||||
isVisible: new Uint8Array([1]),
|
||||
visibleArea: new Float64Array([10000]),
|
||||
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(),
|
||||
};
|
||||
};
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.tuple(finiteFloatArb, finiteFloatArb),
|
||||
fc.tuple(finiteFloatArb, finiteFloatArb),
|
||||
([ox, oy], [px, py]) => {
|
||||
const world = makeWorld(ox, oy);
|
||||
const index = buildWorldIndex(world);
|
||||
|
||||
// Convert point from frame 1 (viewport index) to frame 2 (child index) and back
|
||||
const p1 = convertPoint(world, 0, 1, px, py, index);
|
||||
if (!p1) return; // Skip singular transforms
|
||||
|
||||
const p0 = convertPoint(world, 1, 0, p1.x, p1.y, index);
|
||||
assert.ok(p0, 'Reverse conversion should succeed');
|
||||
|
||||
assert.ok(Math.abs(p0!.x - px) < 1e-4, `x round-trip mismatch: ${p0!.x} vs ${px}`);
|
||||
assert.ok(Math.abs(p0!.y - py) < 1e-4, `y round-trip mismatch: ${p0!.y} vs ${py}`);
|
||||
}
|
||||
),
|
||||
{ numRuns: 200, seed: 77 }
|
||||
);
|
||||
});
|
||||
|
||||
it('property: box intersection is commutative', () => {
|
||||
fc.assert(
|
||||
fc.property(rectArb, rectArb, (a, b) => {
|
||||
const ab = rectIntersection(a, b);
|
||||
const ba = rectIntersection(b, a);
|
||||
|
||||
if (ab === null) {
|
||||
assert.strictEqual(ba, null, 'Intersection should be null both ways');
|
||||
} else {
|
||||
assert.ok(ba !== null, 'Intersection should not be null');
|
||||
assert.strictEqual(ab.left, ba.left);
|
||||
assert.strictEqual(ab.top, ba.top);
|
||||
assert.strictEqual(ab.right, ba.right);
|
||||
assert.strictEqual(ab.bottom, ba.bottom);
|
||||
}
|
||||
|
||||
assert.strictEqual(rectsIntersect(a, b), rectsIntersect(b, a));
|
||||
}),
|
||||
{ numRuns: 300, seed: 88 }
|
||||
);
|
||||
});
|
||||
|
||||
it('property: string table deduplication never duplicates', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(stringArb, { minLength: 1, maxLength: 50 }),
|
||||
(strings) => {
|
||||
const table: StringTable = { values: [] };
|
||||
const indices: number[] = [];
|
||||
|
||||
for (const s of strings) {
|
||||
indices.push(internString(table, s));
|
||||
}
|
||||
|
||||
// Check that identical strings map to identical indices
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
for (let j = i + 1; j < strings.length; j++) {
|
||||
if (strings[i] === strings[j]) {
|
||||
assert.strictEqual(
|
||||
indices[i],
|
||||
indices[j],
|
||||
`Duplicate string "${strings[i]}" should have same index`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the number of unique values matches unique indices
|
||||
const uniqueIndices = new Set(indices);
|
||||
assert.ok(uniqueIndices.size <= table.values.length, 'Unique indices should not exceed table size');
|
||||
|
||||
// Verify all strings are retrievable
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
assert.strictEqual(table.values[indices[i]], strings[i]);
|
||||
}
|
||||
}
|
||||
),
|
||||
{ numRuns: 200, seed: 55 }
|
||||
);
|
||||
});
|
||||
|
||||
it('property: buildTransformChain with identity matrices is identity', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(fc.constant(new Float64Array(IDENTITY_4X4)), { minLength: 1, maxLength: 10 }),
|
||||
(matrices) => {
|
||||
const result = buildTransformChain(matrices);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
assert.ok(
|
||||
Math.abs(result[i] - IDENTITY_4X4[i]) < 1e-10,
|
||||
`Identity chain should yield identity at index ${i}`
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
{ numRuns: 100, seed: 11 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user