diff --git a/tests/engine/io/fig/roundtrip/exhaustive.test.ts b/tests/engine/io/fig/roundtrip/exhaustive.test.ts index 84d294e2a..e44dea006 100644 --- a/tests/engine/io/fig/roundtrip/exhaustive.test.ts +++ b/tests/engine/io/fig/roundtrip/exhaustive.test.ts @@ -588,7 +588,6 @@ function verifyFixture(spec: FixtureSpec): void { test.todo( 'BUG: corner radius 999 sentinel lost for non-pill nodes on scene import (40 nodes, raw data preserved)' ) - test.todo('BUG: componentPropDefs verifier rejects 9 instances (verifier logic gap)') test.todo( 'BUG: derivedTextData baseline precision differs from raw (14 instances, font metrics)' ) diff --git a/tests/engine/io/fig/roundtrip/helpers.ts b/tests/engine/io/fig/roundtrip/helpers.ts index 1be2db261..e2692c512 100644 --- a/tests/engine/io/fig/roundtrip/helpers.ts +++ b/tests/engine/io/fig/roundtrip/helpers.ts @@ -1,5 +1,7 @@ import type { SceneGraph, SceneNode } from '@open-pencil/core' +import { verifyComponentPropDefs, verifyDerivedTextData } from './raw-verifiers/helpers' + export interface Mismatch { path: string key: string @@ -57,89 +59,6 @@ export function isColorObj(v: unknown): v is Record { ) } -function verifyFontDigest( - amDigest: unknown, - bmDigest: unknown, - i: number, - ctx: VerifierContext -): void { - if (amDigest && bmDigest) { - const amHex = - typeof amDigest === 'string' ? amDigest : Buffer.from(amDigest as Uint8Array).toString('hex') - const bmHex = - typeof bmDigest === 'string' ? bmDigest : Buffer.from(bmDigest as Uint8Array).toString('hex') - if (amHex !== bmHex) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].fontDigest`, - message: `mismatch` - }) - } - } -} - -function verifyFontLineHeight(amLH: unknown, bmLH: unknown, i: number, ctx: VerifierContext): void { - const amLineHeight = typeof amLH === 'number' ? amLH : 1.2 - const bmLineHeight = typeof bmLH === 'number' ? bmLH : 1.2 - if (bmLineHeight !== 1.2 && Math.abs(amLineHeight - bmLineHeight) > 0.05) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].fontLineHeight`, - message: `${amLineHeight} vs ${bmLineHeight}` - }) - } -} - -function verifySingleFontMetadata( - am: Record, - bm: Record, - i: number, - ctx: VerifierContext -): void { - const amKey = am.key as Record | undefined - const bmKey = bm.key as Record | undefined - if (amKey?.family !== bmKey?.family) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].key.family`, - message: `${String(amKey?.family)} vs ${String(bmKey?.family)}` - }) - } - if (amKey?.style !== bmKey?.style) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].key.style`, - message: `${String(amKey?.style)} vs ${String(bmKey?.style)}` - }) - } - if (am.fontWeight !== bm.fontWeight) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].fontWeight`, - message: `${String(am.fontWeight)} vs ${String(bm.fontWeight)}` - }) - } - if (am.fontStyle !== bm.fontStyle) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData[${i}].fontStyle`, - message: `${String(am.fontStyle)} vs ${String(bm.fontStyle)}` - }) - } - verifyFontLineHeight(am.fontLineHeight, bm.fontLineHeight, i, ctx) - verifyFontDigest(am.fontDigest, bm.fontDigest, i, ctx) -} - -function verifyFontMetadata( - aMeta: Record[], - bMeta: Record[], - ctx: VerifierContext -): void { - for (let i = 0; i < aMeta.length; i++) { - verifySingleFontMetadata(aMeta[i], bMeta[i], i, ctx) - } -} - export const SCENE_VERIFIERS = new Map([ [ 'pluginData', @@ -380,36 +299,14 @@ export const RAW_VERIFIERS = new Map([ 'componentPropDefs', (ctx) => { if (isIdempotent(ctx)) return JSON.stringify(ctx.a) === JSON.stringify(ctx.b) - const aVal = ctx.a as Record[] | undefined - const bVal = ctx.b as Record[] | undefined - if (!aVal && !bVal) return true - if (!aVal || !bVal) return false - if (aVal.length !== bVal.length) return false - return true + return verifyComponentPropDefs(ctx.a, ctx.b) } ], [ 'derivedTextData', (ctx) => { if (isIdempotent(ctx)) return JSON.stringify(ctx.a) === JSON.stringify(ctx.b) - const aVal = ctx.a as Record | undefined - const bVal = ctx.b as Record | undefined - if (!aVal && !bVal) return true - if (!aVal || !bVal) return true - - const aMeta = (aVal.fontMetaData as Record[]) ?? [] - const bMeta = (bVal.fontMetaData as Record[]) ?? [] - if (aMeta.length !== bMeta.length) { - ctx.errors.push({ - path: ctx.path, - key: `${ctx.key}.fontMetaData`, - message: `length mismatch: ${aMeta.length} vs ${bMeta.length}` - }) - } else { - verifyFontMetadata(aMeta, bMeta, ctx) - } - - return true + return verifyDerivedTextData(ctx) } ], ['styleId', defaultEqual(0)], diff --git a/tests/engine/io/fig/roundtrip/raw-verifiers/helpers.ts b/tests/engine/io/fig/roundtrip/raw-verifiers/helpers.ts new file mode 100644 index 000000000..c0c610008 --- /dev/null +++ b/tests/engine/io/fig/roundtrip/raw-verifiers/helpers.ts @@ -0,0 +1,217 @@ +import type { VerifierContext } from '../helpers' + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : undefined +} + +function verifyFontDigest( + amDigest: unknown, + bmDigest: unknown, + i: number, + ctx: VerifierContext +): void { + if (amDigest && bmDigest) { + const amHex = + typeof amDigest === 'string' ? amDigest : Buffer.from(amDigest as Uint8Array).toString('hex') + const bmHex = + typeof bmDigest === 'string' ? bmDigest : Buffer.from(bmDigest as Uint8Array).toString('hex') + if (amHex !== bmHex) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].fontDigest`, + message: `mismatch` + }) + } + } +} + +function verifyFontLineHeight(amLH: unknown, bmLH: unknown, i: number, ctx: VerifierContext): void { + const amLineHeight = typeof amLH === 'number' ? amLH : 1.2 + const bmLineHeight = typeof bmLH === 'number' ? bmLH : 1.2 + if (bmLineHeight !== 1.2 && Math.abs(amLineHeight - bmLineHeight) > 0.05) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].fontLineHeight`, + message: `${amLineHeight} vs ${bmLineHeight}` + }) + } +} + +function verifySingleFontMetadata( + am: Record, + bm: Record, + i: number, + ctx: VerifierContext +): void { + const amKey = am.key as Record | undefined + const bmKey = bm.key as Record | undefined + if (amKey?.family !== bmKey?.family) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].key.family`, + message: `${String(amKey?.family)} vs ${String(bmKey?.family)}` + }) + } + if (amKey?.style !== bmKey?.style) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].key.style`, + message: `${String(amKey?.style)} vs ${String(bmKey?.style)}` + }) + } + if (am.fontWeight !== bm.fontWeight) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].fontWeight`, + message: `${String(am.fontWeight)} vs ${String(bm.fontWeight)}` + }) + } + if (am.fontStyle !== bm.fontStyle) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData[${i}].fontStyle`, + message: `${String(am.fontStyle)} vs ${String(bm.fontStyle)}` + }) + } + verifyFontLineHeight(am.fontLineHeight, bm.fontLineHeight, i, ctx) + verifyFontDigest(am.fontDigest, bm.fontDigest, i, ctx) +} + +function verifyFontMetadata( + aMeta: Record[], + bMeta: Record[], + ctx: VerifierContext +): void { + for (let i = 0; i < aMeta.length; i++) { + verifySingleFontMetadata(aMeta[i], bMeta[i], i, ctx) + } +} + +function verifyVectorSize( + aSize: unknown, + bSize: unknown, + key: string, + ctx: VerifierContext, + tolerance = 1 +): void { + const aVec = asRecord(aSize) + const bVec = asRecord(bSize) + for (const axis of ['x', 'y']) { + const aValue = aVec?.[axis] + const bValue = bVec?.[axis] + if (typeof aValue !== 'number' || typeof bValue !== 'number') continue + if (Math.abs(aValue - bValue) > tolerance) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.${key}.${axis}`, + message: `${aValue} vs ${bValue}` + }) + } + } +} + +export function verifyDerivedTextData(ctx: VerifierContext): boolean { + const aVal = asRecord(ctx.a) + const bVal = asRecord(ctx.b) + if (!aVal && !bVal) return true + if (!aVal || !bVal) return false + + verifyVectorSize(aVal.layoutSize, bVal.layoutSize, 'layoutSize', ctx) + + for (const key of ['truncationStartIndex', 'truncatedHeight']) { + if (aVal[key] !== bVal[key]) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.${key}`, + message: `${String(aVal[key])} vs ${String(bVal[key])}` + }) + } + } + + const aGlyphs = (aVal.glyphs as unknown[] | undefined) ?? [] + const bGlyphs = (bVal.glyphs as unknown[] | undefined) ?? [] + if (aGlyphs.length !== bGlyphs.length) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.glyphs`, + message: `length mismatch: ${aGlyphs.length} vs ${bGlyphs.length}` + }) + } + + const aMeta = (aVal.fontMetaData as Record[]) ?? [] + const bMeta = (bVal.fontMetaData as Record[]) ?? [] + if (aMeta.length !== bMeta.length) { + ctx.errors.push({ + path: ctx.path, + key: `${ctx.key}.fontMetaData`, + message: `length mismatch: ${aMeta.length} vs ${bMeta.length}` + }) + } else { + verifyFontMetadata(aMeta, bMeta, ctx) + } + + return true +} + +function guidToComponentValue(value: unknown): string | undefined { + const guid = asRecord(value) + const sessionID = guid?.sessionID + const localID = guid?.localID + if (typeof sessionID !== 'number' || typeof localID !== 'number') return undefined + return `${sessionID}:${localID}` +} + +function textValueCharacters(value: unknown): string | undefined { + const textValue = asRecord(value) + const characters = textValue?.characters + return typeof characters === 'string' ? characters : undefined +} + +function componentPropInitialValue(def: Record): string | undefined { + const initialValue = asRecord(def.initialValue) + const textValue = textValueCharacters(initialValue?.textValue) + if (textValue !== undefined) return textValue + if (typeof initialValue?.boolValue === 'boolean') return String(initialValue.boolValue) + + const guidValue = guidToComponentValue(initialValue?.guidValue) + if (guidValue !== undefined) return guidValue + + const varValue = asRecord(def.varValue) + const value = asRecord(varValue?.value) + const symbolValue = asRecord(value?.symbolIdValue) + const symbolGuid = guidToComponentValue(symbolValue?.guid) + if (symbolGuid !== undefined) return symbolGuid + + const varTextValue = value?.textValue + if (typeof varTextValue === 'string') return varTextValue + if (typeof value?.boolValue === 'boolean') return String(value.boolValue) + return undefined +} + +function isComponentPropTypeEquivalent(aType: unknown, bType: unknown): boolean { + return aType === bType || (aType === 'VARIANT' && bType === 'TEXT') +} + +function verifySingleComponentPropDef( + aDef: Record, + bDef: Record +): boolean { + if (JSON.stringify(aDef.id) !== JSON.stringify(bDef.id)) return false + if (aDef.name !== bDef.name) return false + if (!isComponentPropTypeEquivalent(aDef.type, bDef.type)) return false + + const aInitialValue = componentPropInitialValue(aDef) + const bInitialValue = componentPropInitialValue(bDef) + return aInitialValue === bInitialValue +} + +export function verifyComponentPropDefs(a: unknown, b: unknown): boolean { + const aDefs = a as Record[] | undefined + const bDefs = b as Record[] | undefined + if (!aDefs && !bDefs) return true + if (!aDefs || !bDefs) return false + if (aDefs.length !== bDefs.length) return false + return aDefs.every((aDef, index) => verifySingleComponentPropDef(aDef, bDefs[index])) +}