chore: fill CSS contain test gaps + unskip clippedBy e2e test

- Add 4 unit tests in predicates.test.ts for new contain metrics:
  inside hasClippedOverflow (with/without clipping)
  clippedBy clipKind (contain:paint=1, overflow=2)
- Unskip and implement clippedBy e2e test with overflow:hidden container
  in e2e-edge.test.ts (was stale skipped with 'not yet implemented')
This commit is contained in:
John Dvorak
2026-05-21 14:44:26 -07:00
parent 4ff56d61c2
commit de12e93cf8
2 changed files with 165 additions and 5 deletions
+44 -5
View File
@@ -185,11 +185,50 @@ test.describe('Edge Feature Tests', () => {
expect(result.diagnostics.some(d => (d.code as string) === 'IMH_CARDINALITY_ATMOSTN_FAILED')).toBe(true)
})
// ───────────────────────────────────────────────
// Topology: clippedBy
// ───────────────────────────────────────────────
test.skip('clippedBy - documented but not yet implemented', async () => {
// TODO: implement clippedBy topology predicate
// Add clippedBy to dense DSL test page with overflow:hidden container
async function loadClippingTestPage(page: Page) {
await page.setContent(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 20px; }
.clip-container {
width: 200px; height: 200px;
overflow: hidden;
background: #eee;
position: relative;
}
.clipped-child {
width: 300px; height: 50px;
background: blue;
position: absolute; left: -50px; top: 0;
}
</style>
</head>
<body>
<div class="clip-container" data-testid="clip-container">
<div class="clipped-child" data-testid="clipped-child"></div>
</div>
</body>
</html>
`)
await page.waitForTimeout(50)
}
test('clippedBy - child is clipped by overflow:hidden container', async ({ page }) => {
await loadClippingTestPage(page)
const ui = await imhotep(page)
ui.spec(`in viewport:
'[data-testid="clipped-child"]' clippedBy '[data-testid="clip-container"]'`)
const result = await ui.checkAll()
expect(result.passed).toBe(true)
expect(result.clauseResults.length).toBe(1)
expect(result.clauseResults[0].status).toBe('pass')
// clipKind: 1=contain:paint, 2=overflow — container has overflow:hidden
expect(result.clauseResults[0].metrics?.clipKind).toBe(2)
})
// ───────────────────────────────────────────────
@@ -477,4 +477,125 @@ describe('spatial alias predicates', () => {
assert.strictEqual(result.metrics?.max, 540);
});
});
// --- CSS contain metrics tests ------------------------------------------
describe('CSS contain metrics', () => {
beforeEach(() => {
clearPredicateRegistry();
registerDefaultPredicates();
});
it('inside reports hasClippedOverflow when reference clips and subject overflows', () => {
const evaluator = getPredicateEvaluator('inside')!;
// Subject 1 (10,10)-(90,30) is NOT fully inside Reference 2 (0,0)-(50,50)
// overflowLeft=0, overflowTop=0, overflowRight=40, overflowBottom=0
// Reference 2 clips (overflow:hidden → clipKind=1)
const world = makeWorld({
boxes: {
boxId: [100, 200],
subjectId: [1, 2],
frameId: [1, 1],
borderLeft: [10, 0],
borderTop: [10, 0],
borderRight: [90, 50],
borderBottom: [30, 50],
paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0],
contentLeft: [10, 0], contentTop: [10, 0], contentRight: [90, 50], contentBottom: [30, 50],
},
visualBoxes: {
boxId: [100, 200],
subjectId: [1, 2],
frameId: [1, 1],
borderLeft: [10, 0], borderTop: [10, 0], borderRight: [90, 50], borderBottom: [30, 50],
paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0],
contentLeft: [10, 0], contentTop: [10, 0], contentRight: [90, 50], contentBottom: [30, 50],
},
subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0] },
dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0] },
clipping: {
clipNodeId: [0],
subjectId: [2], // reference 2 is a clipping container (overflow:hidden)
clipKind: [1],
clipLeft: [0], clipTop: [0], clipRight: [50], clipBottom: [50],
parentClipNodeId: [0],
},
});
const result = evaluator.evaluateTuple(world, [1, 2]);
assert.strictEqual(result.truth, 'false');
assert.strictEqual(result.metrics?.overflowRight, 40);
assert.strictEqual(result.metrics?.hasClippedOverflow, 1);
});
it('inside does not report hasClippedOverflow when reference does not clip', () => {
const evaluator = getPredicateEvaluator('inside')!;
// Reference 2 does NOT clip (no clipping entry)
const world = makeWorld({
boxes: {
boxId: [100, 200],
subjectId: [1, 2],
frameId: [1, 1],
borderLeft: [10, 0],
borderTop: [10, 0],
borderRight: [90, 50],
borderBottom: [30, 50],
paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0],
contentLeft: [10, 0], contentTop: [10, 0], contentRight: [90, 50], contentBottom: [30, 50],
},
visualBoxes: {
boxId: [100, 200],
subjectId: [1, 2],
frameId: [1, 1],
borderLeft: [10, 0], borderTop: [10, 0], borderRight: [90, 50], borderBottom: [30, 50],
paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0],
contentLeft: [10, 0], contentTop: [10, 0], contentRight: [90, 50], contentBottom: [30, 50],
},
subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0] },
dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0] },
});
const result = evaluator.evaluateTuple(world, [1, 2]);
assert.strictEqual(result.truth, 'false');
assert.strictEqual(result.metrics?.overflowRight, 40);
assert.strictEqual(result.metrics?.hasClippedOverflow, undefined);
});
it('clippedBy reports clipKind=1 for contain:paint clip', () => {
const evaluator = getPredicateEvaluator('clippedBy')!;
// Subject 1 is clipped by Reference 2 (contain:paint → clipKind=5)
const world = makeWorld({
subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0] },
dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0] },
topology: { clippingRootOf: [2], stackingContextOf: [], scrollContainerOf: [], containingBlockOf: [], formattingContextOf: [], nearestPositionedAncestorOf: [], paintOrderBucket: [], paintOrderIndex: [] },
clipping: {
clipNodeId: [0],
subjectId: [2],
clipKind: [5], // Contain=5
clipLeft: [0], clipTop: [0], clipRight: [100], clipBottom: [100],
parentClipNodeId: [0],
},
});
const result = evaluator.evaluateTuple(world, [1, 2]);
assert.strictEqual(result.truth, 'true');
assert.strictEqual(result.metrics?.clipKind, 1); // 1=contain:paint in predicate encoding
});
it('clippedBy reports clipKind=2 for overflow:hidden clip', () => {
const evaluator = getPredicateEvaluator('clippedBy')!;
const world = makeWorld({
subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0] },
dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0] },
topology: { clippingRootOf: [2], stackingContextOf: [], scrollContainerOf: [], containingBlockOf: [], formattingContextOf: [], nearestPositionedAncestorOf: [], paintOrderBucket: [], paintOrderIndex: [] },
clipping: {
clipNodeId: [0],
subjectId: [2],
clipKind: [1], // Overflow=1
clipLeft: [0], clipTop: [0], clipRight: [100], clipBottom: [100],
parentClipNodeId: [0],
},
});
const result = evaluator.evaluateTuple(world, [1, 2]);
assert.strictEqual(result.truth, 'true');
assert.strictEqual(result.metrics?.clipKind, 2); // 2=overflow in predicate encoding
});
});
});