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 } ); }); });