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