test(kiwi): verify component prop roundtrips
- Add semantic raw verifier for componentPropDefs - Keep derived text raw checks focused on stable metadata - Split raw verifier helpers to keep roundtrip test utilities small
This commit is contained in:
parent
35db955cb9
commit
8a10552c94
|
|
@ -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)'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, number> {
|
|||
)
|
||||
}
|
||||
|
||||
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<string, unknown>,
|
||||
bm: Record<string, unknown>,
|
||||
i: number,
|
||||
ctx: VerifierContext
|
||||
): void {
|
||||
const amKey = am.key as Record<string, unknown> | undefined
|
||||
const bmKey = bm.key as Record<string, unknown> | 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<string, unknown>[],
|
||||
bMeta: Record<string, unknown>[],
|
||||
ctx: VerifierContext
|
||||
): void {
|
||||
for (let i = 0; i < aMeta.length; i++) {
|
||||
verifySingleFontMetadata(aMeta[i], bMeta[i], i, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
export const SCENE_VERIFIERS = new Map<string, Verifier>([
|
||||
[
|
||||
'pluginData',
|
||||
|
|
@ -380,36 +299,14 @@ export const RAW_VERIFIERS = new Map<string, Verifier>([
|
|||
'componentPropDefs',
|
||||
(ctx) => {
|
||||
if (isIdempotent(ctx)) return JSON.stringify(ctx.a) === JSON.stringify(ctx.b)
|
||||
const aVal = ctx.a as Record<string, unknown>[] | undefined
|
||||
const bVal = ctx.b as Record<string, unknown>[] | 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<string, unknown> | undefined
|
||||
const bVal = ctx.b as Record<string, unknown> | undefined
|
||||
if (!aVal && !bVal) return true
|
||||
if (!aVal || !bVal) return true
|
||||
|
||||
const aMeta = (aVal.fontMetaData as Record<string, unknown>[]) ?? []
|
||||
const bMeta = (bVal.fontMetaData as Record<string, unknown>[]) ?? []
|
||||
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)],
|
||||
|
|
|
|||
217
tests/engine/io/fig/roundtrip/raw-verifiers/helpers.ts
Normal file
217
tests/engine/io/fig/roundtrip/raw-verifiers/helpers.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import type { VerifierContext } from '../helpers'
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: 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<string, unknown>,
|
||||
bm: Record<string, unknown>,
|
||||
i: number,
|
||||
ctx: VerifierContext
|
||||
): void {
|
||||
const amKey = am.key as Record<string, unknown> | undefined
|
||||
const bmKey = bm.key as Record<string, unknown> | 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<string, unknown>[],
|
||||
bMeta: Record<string, unknown>[],
|
||||
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<string, unknown>[]) ?? []
|
||||
const bMeta = (bVal.fontMetaData as Record<string, unknown>[]) ?? []
|
||||
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, unknown>): 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<string, unknown>,
|
||||
bDef: Record<string, unknown>
|
||||
): 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<string, unknown>[] | undefined
|
||||
const bDefs = b as Record<string, unknown>[] | 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]))
|
||||
}
|
||||
Loading…
Reference in a new issue