import { describe, it } from 'node:test' import assert from 'node:assert' import { GeometryWorld, StringTable, SubjectKind, FrameKind, DisplayValue, PositionValue, buildWorldIndex, WorldIndex, internString, } from './world.js' import { multiply4x4, translateMatrix, scaleMatrix, rotateMatrix, transformPoint, invert4x4, buildTransformChain, IDENTITY_4X4, } from './transforms.js' import { resolveViewportFrame, walkFrameAncestors, buildFrameTransform, buildFrameToFrameTransform, convertPoint, convertRect, } from './frames.js' import { getBorderBox, getPaddingBox, getContentBox, rectSize, rectArea, rectsIntersect, rectIntersection, getPrimaryBoxInViewport, } from './boxes.js' import { materializeWorld, materializeWorldWithIndex, validateWorld, } from './materialize.js' import { RawExtractionResult, RawSubject, RawFrame, RawBox, RawRect, } from './normalize.js' // --------------------------------------------------------------------------- // Helper: build a minimal valid raw extraction result // --------------------------------------------------------------------------- function makeMinimalRaw(): RawExtractionResult { return { sceneId: 'scene_1', snapshotId: 'snap_1', env: { viewportWidth: 1440, viewportHeight: 900, deviceScaleFactor: 1, colorScheme: 'dark', pointer: 'fine', hover: 'none', reducedMotion: false, locale: 'en-US', writingMode: 'horizontal-tb', }, source: { url: 'https://example.test', browserName: 'Chromium', browserVersion: '123', engine: 'chromium-cdp', extractedAt: Date.now(), }, subjects: [ { id: 1, domNodeId: 10, kind: 'element', primaryBoxId: 100, firstFragmentId: 0, fragmentCount: 0, firstTextRunId: 0, textRunCount: 0 }, { id: 2, domNodeId: 20, kind: 'element', primaryBoxId: 200, firstFragmentId: 0, fragmentCount: 0, firstTextRunId: 0, textRunCount: 0 }, ], dom: [ { nodeId: 10, backendNodeId: 1000, parentNodeId: 0, firstChildIndex: 0, childCount: 0, shadowRootKind: '', tagName: 'div', classNames: [], role: '', ariaName: '' }, { nodeId: 20, backendNodeId: 2000, parentNodeId: 10, firstChildIndex: 0, childCount: 0, shadowRootKind: '', tagName: 'span', classNames: [], role: '', ariaName: '' }, ], frames: [ { id: 1, kind: 'viewport', ownerSubjectId: 0, parentFrameId: 0, originX: 0, originY: 0, clipRectId: 0, scrollContainerId: 0, writingMode: 'horizontal-tb' }, { id: 2, kind: 'containingBlock', ownerSubjectId: 1, parentFrameId: 1, originX: 10, originY: 20, clipRectId: 0, scrollContainerId: 0, writingMode: 'horizontal-tb' }, ], rects: [ { id: 1, left: 0, top: 0, right: 1440, bottom: 900 }, ], boxes: [ { id: 100, subjectId: 1, frameId: 2, borderLeft: 10, borderTop: 20, borderRight: 110, borderBottom: 120, paddingLeft: 12, paddingTop: 22, paddingRight: 108, paddingBottom: 118, contentLeft: 14, contentTop: 24, contentRight: 106, contentBottom: 116 }, { id: 200, subjectId: 2, frameId: 2, borderLeft: 30, borderTop: 40, borderRight: 130, borderBottom: 140, paddingLeft: 32, paddingTop: 42, paddingRight: 128, paddingBottom: 138, contentLeft: 34, contentTop: 44, contentRight: 126, contentBottom: 136 }, ], fragments: [], transforms: [], styles: [ { subjectId: 1, display: 'block', position: 'static', zIndex: 'auto', overflowX: 'visible', overflowY: 'visible', opacity: 1, visibility: 'visible', contain: '', pointerEvents: 'auto', lineHeight: 1.5, fontFamily: 'sans-serif', fontSize: 16, fontWeight: 400 }, ], text: [], topology: { containingBlockOf: [0, 1], nearestPositionedAncestorOf: [0, 0], scrollContainerOf: [0, 0], stackingContextOf: [0, 0], formattingContextOf: [0, 0], clippingRootOf: [0, 0], paintOrderBucket: [0, 0], paintOrderIndex: [0, 0], }, scroll: [], clipping: [], paint: [], visibility: [ { subjectId: 1, isRendered: true, isVisible: true, visibleArea: 10000, clippedArea: 0 }, { subjectId: 2, isRendered: true, isVisible: true, visibleArea: 10000, clippedArea: 0 }, ], provenance: [], confidence: [], } } // --------------------------------------------------------------------------- // World construction tests // --------------------------------------------------------------------------- describe('world construction', () => { it('materializes a minimal world', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) assert.strictEqual(world.subjects.ids.length, 2) assert.strictEqual(world.boxes.boxId.length, 2) assert.strictEqual(world.frames.frameId.length, 2) assert.strictEqual(world.strings.values.length > 0, true) }) it('builds a world index', () => { const raw = makeMinimalRaw() const { world, index } = materializeWorldWithIndex(raw) assert.strictEqual(index.subjectById.get(1), 0) assert.strictEqual(index.subjectById.get(2), 1) assert.strictEqual(index.boxById.get(100), 0) assert.strictEqual(index.frameById.get(1), 0) assert.strictEqual(index.frameById.get(2), 1) }) it('interns strings into a deduplicated table', () => { const table: StringTable = { values: [] } const idx1 = internString(table, 'hello') const idx2 = internString(table, 'hello') const idx3 = internString(table, 'world') assert.strictEqual(idx1, idx2) assert.notStrictEqual(idx1, idx3) assert.strictEqual(table.values.length, 2) }) it('validates a correct world with no errors', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const errors = validateWorld(world) assert.strictEqual(errors.length, 0) }) it('detects invalid box subject references', () => { const raw = makeMinimalRaw() raw.boxes.push({ id: 300, subjectId: 999, // nonexistent frameId: 1, borderLeft: 0, borderTop: 0, borderRight: 10, borderBottom: 10, paddingLeft: 0, paddingTop: 0, paddingRight: 10, paddingBottom: 10, contentLeft: 0, contentTop: 0, contentRight: 10, contentBottom: 10, }) const world = materializeWorld(raw) const errors = validateWorld(world) const invalidSubject = errors.find((e) => e.code === 'INVALID_SUBJECT_REF') assert.ok(invalidSubject) }) it('detects invalid frame parent references', () => { const raw = makeMinimalRaw() raw.frames.push({ id: 3, kind: 'containingBlock', ownerSubjectId: 2, parentFrameId: 999, // nonexistent originX: 0, originY: 0, clipRectId: 0, scrollContainerId: 0, writingMode: 'horizontal-tb', }) const world = materializeWorld(raw) const errors = validateWorld(world) const invalidParent = errors.find((e) => e.code === 'INVALID_PARENT_FRAME') assert.ok(invalidParent) }) }) // --------------------------------------------------------------------------- // Frame resolution tests // --------------------------------------------------------------------------- describe('frame resolution', () => { it('resolves the viewport frame', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const viewportIdx = resolveViewportFrame(world.frames) assert.strictEqual(viewportIdx, 0) assert.strictEqual(world.frames.frameKind[viewportIdx], FrameKind.Viewport) }) it('walks frame ancestors', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const ancestors = walkFrameAncestors(world.frames, 1) // frame 2 (child of viewport) assert.deepStrictEqual(ancestors, [1, 0]) }) it('handles orphaned frames gracefully', () => { const raw = makeMinimalRaw() raw.frames.push({ id: 3, kind: 'synthetic', ownerSubjectId: 0, parentFrameId: 0, originX: 0, originY: 0, clipRectId: 0, scrollContainerId: 0, writingMode: 'horizontal-tb', }) const world = materializeWorld(raw) const ancestors = walkFrameAncestors(world.frames, 2) assert.deepStrictEqual(ancestors, [2]) }) }) // --------------------------------------------------------------------------- // Transform matrix tests // --------------------------------------------------------------------------- describe('transform matrices', () => { it('identity matrix leaves points unchanged', () => { const out = transformPoint(new Float64Array(IDENTITY_4X4), 5, 7) assert.strictEqual(out[0], 5) assert.strictEqual(out[1], 7) }) it('translation matrix moves points', () => { const m = translateMatrix(10, 20) const out = transformPoint(m, 5, 7) assert.strictEqual(out[0], 15) assert.strictEqual(out[1], 27) }) it('scale matrix scales points', () => { const m = scaleMatrix(2, 3) const out = transformPoint(m, 5, 7) assert.strictEqual(out[0], 10) assert.strictEqual(out[1], 21) }) it('rotation matrix rotates points', () => { const m = rotateMatrix(Math.PI / 2) // 90 degrees const out = transformPoint(m, 1, 0) // Should be approximately (0, 1) assert.ok(Math.abs(out[0]) < 1e-10) assert.ok(Math.abs(out[1] - 1) < 1e-10) }) it('matrix multiplication is associative', () => { const a = translateMatrix(1, 0) const b = scaleMatrix(2, 2) const c = rotateMatrix(Math.PI / 4) 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-10, `Mismatch at index ${i}: ${abc1[i]} vs ${abc2[i]}` ) } }) it('inverse of identity is identity', () => { const inv = invert4x4(new Float64Array(IDENTITY_4X4)) assert.ok(inv) for (let i = 0; i < 16; i++) { assert.strictEqual(inv[i], IDENTITY_4X4[i]) } }) it('inverse times original gives identity', () => { const m = translateMatrix(10, 20) const inv = invert4x4(m) assert.ok(inv) const prod = multiply4x4(m, inv) for (let i = 0; i < 16; i++) { const expected = IDENTITY_4X4[i] assert.ok( Math.abs(prod[i] - expected) < 1e-10, `Mismatch at ${i}: ${prod[i]} vs ${expected}` ) } }) it('buildTransformChain composes in order', () => { const t1 = translateMatrix(1, 0) const t2 = translateMatrix(0, 1) const chain = buildTransformChain([t1, t2]) const out = transformPoint(chain, 0, 0) assert.strictEqual(out[0], 1) assert.strictEqual(out[1], 1) }) it('returns identity for empty chain', () => { const chain = buildTransformChain([]) const out = transformPoint(chain, 5, 7) assert.strictEqual(out[0], 5) assert.strictEqual(out[1], 7) }) }) // --------------------------------------------------------------------------- // Coordinate transform tests // --------------------------------------------------------------------------- describe('coordinate transforms', () => { it('converts a point between identical frames', () => { const raw = makeMinimalRaw() const { world, index } = materializeWorldWithIndex(raw) const viewportIdx = resolveViewportFrame(world.frames) // Frame 2 has origin (10, 20) relative to viewport, no transform const result = convertPoint(world, viewportIdx, 1, 5, 5, index) assert.ok(result) assert.strictEqual(result!.x, -5) // 5 - 10 assert.strictEqual(result!.y, -15) // 5 - 20 }) it('converts a rect between frames', () => { const raw = makeMinimalRaw() const { world, index } = materializeWorldWithIndex(raw) const viewportIdx = resolveViewportFrame(world.frames) // Rect (0,0,50,50) in frame 1 (origin 10,20) maps to (10,20,60,70) in viewport const result = convertRect(world, 1, viewportIdx, 0, 0, 50, 50, index) assert.ok(result) assert.strictEqual(result!.left, 10) assert.strictEqual(result!.top, 20) assert.strictEqual(result!.right, 60) assert.strictEqual(result!.bottom, 70) }) it('applies transform matrices in frame chains', () => { const raw = makeMinimalRaw() // Add a scale transform to frame 2 const scaleMat = Array.from(scaleMatrix(2, 2)) raw.frames[1].matrix = scaleMat const { world, index } = materializeWorldWithIndex(raw) const viewportIdx = resolveViewportFrame(world.frames) // Point (10, 20) in frame 2: scaled by 2, then offset by origin (10, 20) // In viewport: (10*2 + 10, 20*2 + 20) = (30, 60) // Inverse: viewport to frame 2 const result = convertPoint(world, viewportIdx, 1, 30, 60, index) assert.ok(result) assert.ok(Math.abs(result!.x - 10) < 1e-10) assert.ok(Math.abs(result!.y - 20) < 1e-10) }) }) // --------------------------------------------------------------------------- // Box geometry tests // --------------------------------------------------------------------------- describe('box geometry', () => { it('reads border box correctly', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const box = getBorderBox(world.boxes, 0) assert.strictEqual(box.left, 10) assert.strictEqual(box.top, 20) assert.strictEqual(box.right, 110) assert.strictEqual(box.bottom, 120) }) it('reads padding box correctly', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const box = getPaddingBox(world.boxes, 0) assert.strictEqual(box.left, 12) assert.strictEqual(box.top, 22) assert.strictEqual(box.right, 108) assert.strictEqual(box.bottom, 118) }) it('reads content box correctly', () => { const raw = makeMinimalRaw() const world = materializeWorld(raw) const box = getContentBox(world.boxes, 0) assert.strictEqual(box.left, 14) assert.strictEqual(box.top, 24) assert.strictEqual(box.right, 106) assert.strictEqual(box.bottom, 116) }) it('computes rect size', () => { const size = rectSize({ left: 10, top: 20, right: 110, bottom: 120 }) assert.strictEqual(size.width, 100) assert.strictEqual(size.height, 100) }) it('computes rect area', () => { const area = rectArea({ left: 0, top: 0, right: 10, bottom: 20 }) assert.strictEqual(area, 200) }) it('detects intersecting rects', () => { const a = { left: 0, top: 0, right: 10, bottom: 10 } const b = { left: 5, top: 5, right: 15, bottom: 15 } assert.strictEqual(rectsIntersect(a, b), true) }) it('detects non-intersecting rects', () => { const a = { left: 0, top: 0, right: 10, bottom: 10 } const b = { left: 10, top: 10, right: 20, bottom: 20 } assert.strictEqual(rectsIntersect(a, b), false) }) it('computes rect intersection', () => { const a = { left: 0, top: 0, right: 10, bottom: 10 } const b = { left: 5, top: 5, right: 15, bottom: 15 } const inter = rectIntersection(a, b) assert.ok(inter) assert.strictEqual(inter!.left, 5) assert.strictEqual(inter!.top, 5) assert.strictEqual(inter!.right, 10) assert.strictEqual(inter!.bottom, 10) }) it('returns null for non-intersecting rects', () => { const a = { left: 0, top: 0, right: 10, bottom: 10 } const b = { left: 20, top: 20, right: 30, bottom: 30 } const inter = rectIntersection(a, b) assert.strictEqual(inter, null) }) it('gets primary box in viewport', () => { const raw = makeMinimalRaw() const { world, index } = materializeWorldWithIndex(raw) const box = getPrimaryBoxInViewport(world, 1, index) assert.ok(box) // Box 100 is in frame 2 with origin (10, 20) // border box: left=10, top=20, right=110, bottom=120 // In viewport: offset by (10, 20) -> left=20, top=40, right=120, bottom=140 assert.strictEqual(box!.left, 20) assert.strictEqual(box!.top, 40) assert.strictEqual(box!.right, 120) assert.strictEqual(box!.bottom, 140) }) }) // --------------------------------------------------------------------------- // String table tests // --------------------------------------------------------------------------- describe('string table', () => { it('deduplicates identical strings', () => { const table: StringTable = { values: [] } const a = internString(table, 'div') const b = internString(table, 'div') const c = internString(table, 'span') assert.strictEqual(a, b) assert.notStrictEqual(a, c) assert.strictEqual(table.values[a], 'div') assert.strictEqual(table.values[c], 'span') }) })