315 lines
8.9 KiB
TypeScript
315 lines
8.9 KiB
TypeScript
/**
|
|
* Tests for relation-specific proof generation.
|
|
*
|
|
* Verifies that generateProof produces rich, relation-specific failedPredicate
|
|
* details instead of generic left/right metric comparisons.
|
|
*/
|
|
|
|
import { describe, it, beforeEach } from 'node:test'
|
|
import assert from 'node:assert'
|
|
|
|
import {
|
|
generateProof,
|
|
resetProofCounter,
|
|
} from './proofs.js'
|
|
|
|
import type {
|
|
ClauseResult,
|
|
ClauseDescriptor,
|
|
GeometryWorld,
|
|
} from './registry.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock World
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const world: GeometryWorld = {
|
|
sceneId: 'scene_1',
|
|
snapshotId: 'snap_1',
|
|
env: {
|
|
viewportWidth: 1280,
|
|
viewportHeight: 800,
|
|
deviceScaleFactor: 1,
|
|
colorScheme: 'light',
|
|
pointer: 'fine',
|
|
hover: false,
|
|
reducedMotion: false,
|
|
locale: 'en',
|
|
writingMode: 'horizontal-tb',
|
|
},
|
|
strings: { values: [] },
|
|
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],
|
|
},
|
|
boxes: {
|
|
boxId: [100, 200],
|
|
subjectId: [1, 2],
|
|
frameId: [1, 1],
|
|
borderLeft: [0, 110],
|
|
borderTop: [0, 50],
|
|
borderRight: [100, 210],
|
|
borderBottom: [40, 90],
|
|
paddingLeft: [0, 0],
|
|
paddingTop: [0, 0],
|
|
paddingRight: [0, 0],
|
|
paddingBottom: [0, 0],
|
|
contentLeft: [0, 0],
|
|
contentTop: [0, 0],
|
|
contentRight: [0, 0],
|
|
contentBottom: [0, 0],
|
|
},
|
|
visualBoxes: {
|
|
boxId: [], subjectId: [], frameId: [],
|
|
borderLeft: [], borderTop: [], borderRight: [], borderBottom: [],
|
|
paddingLeft: [], paddingTop: [], paddingRight: [], paddingBottom: [],
|
|
contentLeft: [], contentTop: [], contentRight: [], contentBottom: [],
|
|
},
|
|
transforms: {
|
|
transformId: [], subjectId: [], matrixStart: [], matrixLength: [],
|
|
originX: [], originY: [],
|
|
},
|
|
matrices: { values: [] },
|
|
rects: { rectId: [], left: [], top: [], right: [], bottom: [] },
|
|
topology: {
|
|
containingBlockOf: [0, 0],
|
|
nearestPositionedAncestorOf: [0, 0],
|
|
scrollContainerOf: [0, 0],
|
|
stackingContextOf: [0, 0],
|
|
formattingContextOf: [0, 0],
|
|
clippingRootOf: [0, 0],
|
|
paintOrderBucket: [0, 0],
|
|
paintOrderIndex: [0, 0],
|
|
},
|
|
scroll: {
|
|
containerId: [],
|
|
scrollLeft: [],
|
|
scrollTop: [],
|
|
scrollWidth: [],
|
|
scrollHeight: [],
|
|
clientWidth: [],
|
|
clientHeight: [],
|
|
},
|
|
clipping: {
|
|
clipNodeId: [],
|
|
subjectId: [],
|
|
clipKind: [],
|
|
clipLeft: [],
|
|
clipTop: [],
|
|
clipRight: [],
|
|
clipBottom: [],
|
|
parentClipNodeId: [],
|
|
},
|
|
visibility: {
|
|
subjectId: [],
|
|
isRendered: [],
|
|
isVisible: [],
|
|
visibleArea: [],
|
|
clippedArea: [],
|
|
},
|
|
}
|
|
|
|
function makeClause(kind: string): ClauseDescriptor {
|
|
return {
|
|
clauseId: 'clause_1',
|
|
clauseKind: kind,
|
|
version: 1,
|
|
subjectRef: 1,
|
|
referenceRef: 2,
|
|
}
|
|
}
|
|
|
|
function makeResult(status: 'pass' | 'fail' | 'error', metrics?: Record<string, number>): ClauseResult {
|
|
return {
|
|
clauseId: 'clause_1',
|
|
status,
|
|
truth: status === 'error' ? 'indeterminate' : 'determinate',
|
|
metrics,
|
|
witness: { subjectId: 1, referenceId: 2 },
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Setup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
beforeEach(() => {
|
|
resetProofCounter()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Relation-Specific Proof Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('relation-specific proofs', () => {
|
|
it('leftOf proof includes measured gap and expected bounds', () => {
|
|
const clause = makeClause('relation.leftOf')
|
|
const result = makeResult('fail', {
|
|
observedGap: -5,
|
|
minGap: 0,
|
|
maxGap: Infinity,
|
|
subjectLeft: 0,
|
|
subjectTop: 0,
|
|
subjectRight: 100,
|
|
subjectBottom: 40,
|
|
refLeft: 90,
|
|
refTop: 50,
|
|
refRight: 190,
|
|
refBottom: 90,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.outcome, 'fail')
|
|
assert.ok(proof.failedPredicate)
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'leftOf')
|
|
assert.strictEqual(proof.failedPredicate!.measuredGap, -5)
|
|
assert.strictEqual(proof.failedPredicate!.expectedMinGap, 0)
|
|
assert.ok(proof.failedPredicate!.subjectRect)
|
|
assert.strictEqual(proof.failedPredicate!.subjectRect!.right, 100)
|
|
assert.ok(proof.failedPredicate!.referenceRect)
|
|
assert.strictEqual(proof.failedPredicate!.referenceRect!.left, 90)
|
|
})
|
|
|
|
it('above proof includes vertical gap and positions', () => {
|
|
const clause = makeClause('relation.above')
|
|
const result = makeResult('fail', {
|
|
observedGap: -3,
|
|
minGap: 0,
|
|
maxGap: Infinity,
|
|
subjectLeft: 0,
|
|
subjectTop: 0,
|
|
subjectRight: 100,
|
|
subjectBottom: 40,
|
|
refLeft: 0,
|
|
refTop: 35,
|
|
refRight: 100,
|
|
refBottom: 75,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'above')
|
|
assert.strictEqual(proof.failedPredicate!.measuredGap, -3)
|
|
assert.ok(proof.failedPredicate!.subjectRect)
|
|
assert.ok(proof.failedPredicate!.referenceRect)
|
|
})
|
|
|
|
it('inside proof includes overflow edges', () => {
|
|
const clause = makeClause('relation.inside')
|
|
const result = makeResult('fail', {
|
|
overflowLeft: 10,
|
|
overflowTop: 0,
|
|
overflowRight: -5,
|
|
overflowBottom: 0,
|
|
subjectLeft: 10,
|
|
subjectTop: 0,
|
|
subjectRight: 105,
|
|
subjectBottom: 40,
|
|
refLeft: 0,
|
|
refTop: 0,
|
|
refRight: 100,
|
|
refBottom: 40,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'inside')
|
|
assert.ok(proof.failedPredicate!.overflowEdges)
|
|
assert.strictEqual(proof.failedPredicate!.overflowEdges!.left, 10)
|
|
assert.strictEqual(proof.failedPredicate!.overflowEdges!.right, -5)
|
|
assert.ok(proof.failedPredicate!.subjectRect)
|
|
assert.ok(proof.failedPredicate!.referenceRect)
|
|
})
|
|
|
|
it('atLeast proof includes measured vs expected dimensions', () => {
|
|
const clause = makeClause('size.atLeast')
|
|
const result = makeResult('fail', {
|
|
observed: 80,
|
|
min: 100,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'atLeast')
|
|
assert.strictEqual(proof.failedPredicate!.measuredValue, 80)
|
|
assert.strictEqual(proof.failedPredicate!.expectedMin, 100)
|
|
})
|
|
|
|
it('atMost proof includes measured vs expected dimensions', () => {
|
|
const clause = makeClause('size.atMost')
|
|
const result = makeResult('fail', {
|
|
observed: 120,
|
|
max: 100,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'atMost')
|
|
assert.strictEqual(proof.failedPredicate!.measuredValue, 120)
|
|
assert.strictEqual(proof.failedPredicate!.expectedMax, 100)
|
|
})
|
|
|
|
it('alignedWith proof includes delta and tolerance', () => {
|
|
const clause = makeClause('alignment.alignedWith')
|
|
const result = makeResult('fail', {
|
|
delta: 5,
|
|
tolerance: 1,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'alignedWith')
|
|
assert.strictEqual(proof.failedPredicate!.measuredValue, 5)
|
|
assert.strictEqual(proof.failedPredicate!.expectedMax, 1)
|
|
})
|
|
|
|
it('centeredWithin proof includes deltaX, deltaY and tolerance', () => {
|
|
const clause = makeClause('alignment.centeredWithin')
|
|
const result = makeResult('fail', {
|
|
deltaX: 3,
|
|
deltaY: 4,
|
|
tolerance: 2,
|
|
})
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'centeredWithin')
|
|
assert.strictEqual(proof.failedPredicate!.measuredValue, 3)
|
|
assert.strictEqual(proof.failedPredicate!.expectedMax, 2)
|
|
})
|
|
|
|
it('omits failedPredicate on pass', () => {
|
|
const clause = makeClause('relation.leftOf')
|
|
const result = makeResult('pass', { observedGap: 10, minGap: 0 })
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.strictEqual(proof.outcome, 'pass')
|
|
assert.strictEqual(proof.failedPredicate, undefined)
|
|
})
|
|
|
|
it('falls back to generic synthesis for unknown relation kinds', () => {
|
|
const clause = makeClause('relation.unknownRelation')
|
|
const result = makeResult('fail', { foo: 10, bar: 20 })
|
|
|
|
const proof = generateProof(result, clause, world)
|
|
|
|
assert.ok(proof.failedPredicate)
|
|
assert.strictEqual(proof.failedPredicate!.relationKind, 'unknownRelation')
|
|
assert.strictEqual(proof.failedPredicate!.op, '<')
|
|
assert.strictEqual(proof.failedPredicate!.left, 10)
|
|
assert.strictEqual(proof.failedPredicate!.right, 20)
|
|
})
|
|
})
|