From de12e93cf8501d3ea5a65afe0ea71dba577a7d8b Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Thu, 21 May 2026 14:44:26 -0700 Subject: [PATCH] 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') --- .../imhotep-fixtures/src/e2e-edge.test.ts | 49 ++++++- .../imhotep-solver/src/predicates.test.ts | 121 ++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/packages/imhotep-fixtures/src/e2e-edge.test.ts b/packages/imhotep-fixtures/src/e2e-edge.test.ts index 21ae42c..d0dffcd 100644 --- a/packages/imhotep-fixtures/src/e2e-edge.test.ts +++ b/packages/imhotep-fixtures/src/e2e-edge.test.ts @@ -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(` + + + + + + +
+
+
+ + + `) + 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) }) // ─────────────────────────────────────────────── diff --git a/packages/imhotep-solver/src/predicates.test.ts b/packages/imhotep-solver/src/predicates.test.ts index e07f6f5..a9fc35a 100644 --- a/packages/imhotep-solver/src/predicates.test.ts +++ b/packages/imhotep-solver/src/predicates.test.ts @@ -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 + }); + }); });