// Unit tests for imhotep-topology engine. // Uses Node.js built-in test runner. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createTopologyGraph, INVALID_ID, ConfidenceLevel, Position, Overflow, Display, ZIndexKind, PaintBucket, ClipKind, ScrollKind, } from './graph.js'; import { isPositioned, resolveContainingBlock, walkUpForPositionedAncestor, buildAncestorTables, computeAncestors, } from './ancestors.js'; import { isScrollContainer, introducesClipping, buildScrollChain, buildClipChain, buildChains, } from './chains.js'; import { establishesStackingContext, classifyPaintBucket, assignStackingContexts, } from './stacking.js'; import { establishesFormattingContext, assignFormattingContexts, } from './formatting.js'; import { inStackingContext, clippedBy, attachedToScrollContainer, comparePaintOrder, isAbove, isBelow, } from './queries.js'; // --------------------------------------------------------------------------- // Helpers to build minimal DOM + style fixtures // --------------------------------------------------------------------------- function makeDom(parents: number[]) { return { parentNodeId: new Uint32Array(parents), nodeCount: parents.length, }; } function makeStyles(nodeCount: number, overrides: Record = {}) { const display = new Uint16Array(nodeCount).fill(Display.BLOCK); const position = new Uint16Array(nodeCount).fill(Position.STATIC); const zIndexKind = new Uint8Array(nodeCount).fill(ZIndexKind.AUTO); const zIndexValue = new Int32Array(nodeCount).fill(0); const overflowX = new Uint16Array(nodeCount).fill(Overflow.VISIBLE); const overflowY = new Uint16Array(nodeCount).fill(Overflow.VISIBLE); const opacity = new Float32Array(nodeCount).fill(1.0); const containFlags = new Uint32Array(nodeCount).fill(0); // Apply overrides by property for (const [prop, indices] of Object.entries(overrides)) { for (const idx of indices as number[]) { switch (prop) { case 'flex': display[idx] = Display.FLEX; break; case 'grid': display[idx] = Display.GRID; break; case 'inline': display[idx] = Display.INLINE; break; case 'inlineBlock': display[idx] = Display.INLINE_BLOCK; break; case 'relative': position[idx] = Position.RELATIVE; break; case 'absolute': position[idx] = Position.ABSOLUTE; break; case 'fixed': position[idx] = Position.FIXED; break; case 'sticky': position[idx] = Position.STICKY; break; case 'zIndex': zIndexKind[idx] = ZIndexKind.INTEGER; break; case 'overflowHidden': overflowX[idx] = Overflow.HIDDEN; overflowY[idx] = Overflow.HIDDEN; break; case 'overflowScroll': overflowX[idx] = Overflow.SCROLL; overflowY[idx] = Overflow.SCROLL; break; case 'opacity': opacity[idx] = 0.5; break; case 'containPaint': containFlags[idx] = 0x02; break; } } } return { display, position, zIndexKind, zIndexValue, overflowX, overflowY, opacity, containFlags }; } // --------------------------------------------------------------------------- // Ancestor resolution tests // --------------------------------------------------------------------------- describe('ancestors', () => { it('isPositioned returns false for static', () => { assert.equal(isPositioned(Position.STATIC), false); assert.equal(isPositioned(Position.RELATIVE), true); assert.equal(isPositioned(Position.ABSOLUTE), true); }); it('walkUpForPositionedAncestor finds nearest positioned ancestor', () => { // Tree: 0(root) <- 1 <- 2 <- 3 // Node 1 is relative. const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { relative: [1] }); assert.equal(walkUpForPositionedAncestor(3, dom, styles), 1); assert.equal(walkUpForPositionedAncestor(2, dom, styles), 1); assert.equal(walkUpForPositionedAncestor(1, dom, styles), INVALID_ID); assert.equal(walkUpForPositionedAncestor(0, dom, styles), INVALID_ID); }); it('resolveContainingBlock for absolute uses nearest positioned ancestor', () => { // Tree: 0 <- 1(relative) <- 2 <- 3(absolute) const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { relative: [1], absolute: [3] }); assert.equal(resolveContainingBlock(3, dom, styles), 1); }); it('resolveContainingBlock for fixed returns INVALID_ID (viewport)', () => { const dom = makeDom([INVALID_ID, 0]); const styles = makeStyles(2, { fixed: [1] }); assert.equal(resolveContainingBlock(1, dom, styles), INVALID_ID); }); it('buildAncestorTables produces correct arrays', () => { // Tree: 0 <- 1(relative) <- 2 <- 3(absolute) const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { relative: [1], absolute: [3] }); const tables = buildAncestorTables(dom, styles); assert.deepEqual( Array.from(tables.nearestPositionedAncestorOf), [INVALID_ID, INVALID_ID, 1, 1], ); assert.deepEqual(Array.from(tables.containingBlockOf), [INVALID_ID, 0, 1, 1]); }); it('computeAncestors writes into graph with confidence', () => { const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { relative: [1], absolute: [3] }); const graph = createTopologyGraph(4); computeAncestors(dom, styles, graph); assert.equal(graph.nearestPositionedAncestorOf[3], 1); assert.equal(graph.containingBlockOf[3], 1); assert.ok(graph.confidence[3] > 0); }); }); // --------------------------------------------------------------------------- // Chain building tests // --------------------------------------------------------------------------- describe('chains', () => { it('isScrollContainer recognizes overflow:scroll', () => { const result = isScrollContainer(Overflow.SCROLL, Overflow.SCROLL, false); assert.equal(result.kind, ScrollKind.SCROLL); assert.equal(result.confidence, ConfidenceLevel.DERIVED); }); it('isScrollContainer returns NONE for visible', () => { const result = isScrollContainer(Overflow.VISIBLE, Overflow.VISIBLE, false); assert.equal(result.kind, ScrollKind.NONE); }); it('introducesClipping returns true for overflow hidden', () => { const result = introducesClipping(Overflow.HIDDEN, Overflow.HIDDEN, 0); assert.equal(result.clips, true); assert.equal(result.kind, ClipKind.OVERFLOW); }); it('buildScrollChain assigns nearest scroll container', () => { // Tree: 0 <- 1(scroll) <- 2 <- 3 const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { overflowScroll: [1] }); const graph = createTopologyGraph(4); buildScrollChain(dom, styles, graph, false); assert.equal(graph.scrollContainerOf[3], 1); assert.equal(graph.scrollContainerOf[2], 1); assert.equal(graph.scrollContainerOf[1], INVALID_ID); assert.equal(graph.scrollContainerOf[0], INVALID_ID); }); it('buildClipChain assigns nearest clipping ancestor', () => { // Tree: 0 <- 1(overflow:hidden) <- 2 <- 3 const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { overflowHidden: [1] }); const graph = createTopologyGraph(4); buildClipChain(dom, styles, graph); assert.equal(graph.clippingRootOf[3], 1); assert.equal(graph.clippingRootOf[2], 1); assert.equal(graph.clippingRootOf[1], INVALID_ID); assert.equal(graph.clippingRootOf[0], INVALID_ID); }); it('buildClipChain lets fixed elements escape ancestor overflow clips', () => { // Tree: 0 <- 1(overflow:hidden) <- 2(fixed) const dom = makeDom([INVALID_ID, 0, 1]); const styles = makeStyles(3, { overflowHidden: [1], fixed: [2] }); const graph = createTopologyGraph(3); buildClipChain(dom, styles, graph); // Fixed element should NOT be clipped by the overflow:hidden ancestor. assert.equal(graph.clippingRootOf[2], INVALID_ID); }); it('buildChains runs both scroll and clip builders', () => { // Tree: 0 <- 1(overflow:hidden + scroll) <- 2 <- 3 const dom = makeDom([INVALID_ID, 0, 1, 2]); const styles = makeStyles(4, { overflowScroll: [1] }); const graph = createTopologyGraph(4); buildChains(dom, styles, graph, false); assert.equal(graph.scrollContainerOf[3], 1); assert.equal(graph.clippingRootOf[3], 1); }); }); // --------------------------------------------------------------------------- // Stacking context tests // --------------------------------------------------------------------------- describe('stacking', () => { it('establishesStackingContext for root is exact', () => { const styles = makeStyles(1); const result = establishesStackingContext(0, true, styles); assert.equal(result.yes, true); assert.equal(result.confidence, ConfidenceLevel.EXACT); }); it('establishesStackingContext for z-index integer', () => { const styles = makeStyles(1, { zIndex: [0] }); const result = establishesStackingContext(0, false, styles); assert.equal(result.yes, true); }); it('establishesStackingContext for opacity < 1', () => { const styles = makeStyles(1, { opacity: [0] }); const result = establishesStackingContext(0, false, styles); assert.equal(result.yes, true); }); it('establishesStackingContext for fixed position', () => { const styles = makeStyles(1, { fixed: [0] }); const result = establishesStackingContext(0, false, styles); assert.equal(result.yes, true); }); it('classifyPaintBucket puts positioned auto into POSITIONED_AUTO', () => { const styles = makeStyles(1, { relative: [0] }); assert.equal(classifyPaintBucket(0, styles), PaintBucket.POSITIONED_AUTO); }); it('classifyPaintBucket puts inline into INLINE_LEVEL', () => { const styles = makeStyles(1, { inline: [0] }); assert.equal(classifyPaintBucket(0, styles), PaintBucket.INLINE_LEVEL); }); it('assignStackingContexts roots every node in a stacking context', () => { // Tree: 0(root) <- 1 <- 2 const dom = makeDom([INVALID_ID, 0, 1]); const styles = makeStyles(3); const graph = createTopologyGraph(3); assignStackingContexts(dom, styles, graph); // Root is the stacking context for everyone. assert.equal(graph.stackingContextOf[0], 0); assert.equal(graph.stackingContextOf[1], 0); assert.equal(graph.stackingContextOf[2], 0); }); it('assignStackingContexts nests when a descendant establishes a new context', () => { // Tree: 0(root) <- 1(z-index) <- 2 const dom = makeDom([INVALID_ID, 0, 1]); const styles = makeStyles(3, { zIndex: [1] }); const graph = createTopologyGraph(3); assignStackingContexts(dom, styles, graph); assert.equal(graph.stackingContextOf[0], 0); assert.equal(graph.stackingContextOf[1], 1); // Node 1 is its own context root assert.equal(graph.stackingContextOf[2], 1); // Node 2 paints inside 1's context }); }); // --------------------------------------------------------------------------- // Formatting context tests // --------------------------------------------------------------------------- describe('formatting', () => { it('establishesFormattingContext for root', () => { const styles = makeStyles(1); const result = establishesFormattingContext(0, true, styles); assert.equal(result.yes, true); }); it('establishesFormattingContext for flex container', () => { const styles = makeStyles(1, { flex: [0] }); const result = establishesFormattingContext(0, false, styles); assert.equal(result.yes, true); }); it('establishesFormattingContext for block with overflow hidden', () => { const styles = makeStyles(1, { overflowHidden: [0] }); const result = establishesFormattingContext(0, false, styles); assert.equal(result.yes, true); }); it('assignFormattingContexts assigns root to all in simple tree', () => { const dom = makeDom([INVALID_ID, 0, 1]); const styles = makeStyles(3); const graph = createTopologyGraph(3); assignFormattingContexts(dom, styles, graph); assert.equal(graph.formattingContextOf[0], 0); assert.equal(graph.formattingContextOf[1], 0); assert.equal(graph.formattingContextOf[2], 0); }); it('assignFormattingContexts isolates inline-block subtree', () => { // Tree: 0 <- 1(inline-block) <- 2 const dom = makeDom([INVALID_ID, 0, 1]); const styles = makeStyles(3, { inlineBlock: [1] }); const graph = createTopologyGraph(3); assignFormattingContexts(dom, styles, graph); assert.equal(graph.formattingContextOf[0], 0); assert.equal(graph.formattingContextOf[1], 1); assert.equal(graph.formattingContextOf[2], 1); }); }); // --------------------------------------------------------------------------- // Query API tests // --------------------------------------------------------------------------- describe('queries', () => { it('inStackingContext returns true when context matches', () => { const graph = createTopologyGraph(3); graph.stackingContextOf[0] = 0; graph.stackingContextOf[1] = 0; graph.stackingContextOf[2] = 1; const r = inStackingContext(graph, 1, 0); assert.equal(r.value, true); }); it('inStackingContext returns false when context mismatches', () => { const graph = createTopologyGraph(3); graph.stackingContextOf[0] = 0; graph.stackingContextOf[1] = 0; const r = inStackingContext(graph, 1, 2); assert.equal(r.value, false); }); it('clippedBy returns true when clipped', () => { const graph = createTopologyGraph(2); graph.clippingRootOf[1] = 0; const r = clippedBy(graph, 1); assert.equal(r.value, true); }); it('clippedBy returns false when not clipped', () => { const graph = createTopologyGraph(1); graph.clippingRootOf[0] = INVALID_ID; const r = clippedBy(graph, 0); assert.equal(r.value, false); }); it('attachedToScrollContainer returns true when attached', () => { const graph = createTopologyGraph(2); graph.scrollContainerOf[1] = 0; const r = attachedToScrollContainer(graph, 1); assert.equal(r.value, true); }); it('comparePaintOrder orders by bucket', () => { const graph = createTopologyGraph(2); graph.stackingContextOf[0] = 0; graph.stackingContextOf[1] = 0; graph.paintOrderBucket[0] = PaintBucket.BLOCK_LEVEL; graph.paintOrderBucket[1] = PaintBucket.POSITIONED_AUTO; assert.equal(comparePaintOrder(graph, 0, 1), 'before'); assert.equal(comparePaintOrder(graph, 1, 0), 'after'); }); it('isAbove returns true when subject paints after reference', () => { const graph = createTopologyGraph(2); graph.stackingContextOf[0] = 0; graph.stackingContextOf[1] = 0; graph.paintOrderBucket[0] = PaintBucket.BLOCK_LEVEL; graph.paintOrderBucket[1] = PaintBucket.POSITIONED_AUTO; const r = isAbove(graph, 1, 0); assert.equal(r.value, true); }); it('isBelow returns true when subject paints before reference', () => { const graph = createTopologyGraph(2); graph.stackingContextOf[0] = 0; graph.stackingContextOf[1] = 0; graph.paintOrderBucket[0] = PaintBucket.BLOCK_LEVEL; graph.paintOrderBucket[1] = PaintBucket.POSITIONED_AUTO; const r = isBelow(graph, 0, 1); assert.equal(r.value, true); }); });