diff --git a/tests/engine/io/fig/roundtrip/exhaustive.test.ts b/tests/engine/io/fig/roundtrip/exhaustive.test.ts index 02cb99738..714068fe1 100644 --- a/tests/engine/io/fig/roundtrip/exhaustive.test.ts +++ b/tests/engine/io/fig/roundtrip/exhaustive.test.ts @@ -18,6 +18,8 @@ import { type FixtureSpec, type CompareOptions, type Verifier, + buildComponentPropertyDefinitionIndex, + buildNodePathIndex, isColorObj, SCENE_VERIFIERS, RAW_VERIFIERS @@ -230,6 +232,10 @@ function compareSceneProps( bNodes, aGraph, bGraph, + aNodePaths: buildNodePathIndex(aNodes), + bNodePaths: buildNodePathIndex(bNodes), + aComponentPropertyDefinitions: buildComponentPropertyDefinitionIndex(aGraph), + bComponentPropertyDefinitions: buildComponentPropertyDefinitionIndex(bGraph), errors, fixture, verifiers, @@ -265,6 +271,10 @@ function compareRawNodeFields( bNodes, aGraph, bGraph, + aNodePaths: buildNodePathIndex(aNodes), + bNodePaths: buildNodePathIndex(bNodes), + aComponentPropertyDefinitions: buildComponentPropertyDefinitionIndex(aGraph), + bComponentPropertyDefinitions: buildComponentPropertyDefinitionIndex(bGraph), errors, fixture, verifiers, diff --git a/tests/engine/io/fig/roundtrip/helpers.ts b/tests/engine/io/fig/roundtrip/helpers.ts index 23bfea462..e3e60921c 100644 --- a/tests/engine/io/fig/roundtrip/helpers.ts +++ b/tests/engine/io/fig/roundtrip/helpers.ts @@ -1,4 +1,5 @@ import type { SceneGraph, SceneNode } from '@open-pencil/core' +import type { ComponentPropertyDefinition } from '@open-pencil/scene-graph' import type { JSONObject } from '@open-pencil/scene-graph/primitives' import { verifyComponentPropDefs, verifyDerivedTextData } from './raw-verifiers/helpers' @@ -33,6 +34,10 @@ export interface VerifierContext { bNodes: Map aGraph: SceneGraph bGraph: SceneGraph + aNodePaths: ReadonlyMap + bNodePaths: ReadonlyMap + aComponentPropertyDefinitions: ReadonlyMap + bComponentPropertyDefinitions: ReadonlyMap errors: Mismatch[] fixture: FixtureSpec label: string @@ -60,6 +65,122 @@ export function isColorObj(v: unknown): v is Record { ) } +export function buildNodePathIndex(nodes: ReadonlyMap): Map { + return new Map([...nodes].map(([path, node]) => [node.id, path])) +} + +export function buildComponentPropertyDefinitionIndex( + graph: SceneGraph +): Map { + const definitions = new Map() + for (const node of graph.getAllNodes()) { + for (const definition of node.componentPropertyDefinitions) { + if (!definitions.has(definition.id)) definitions.set(definition.id, definition) + } + } + return definitions +} + +function sameNodeReference(ctx: VerifierContext, a: string, b: string): boolean { + const aPath = ctx.aNodePaths.get(a) + const bPath = ctx.bNodePaths.get(b) + return aPath !== undefined && aPath === bPath +} + +function sameNodeReferences( + ctx: VerifierContext, + a: string[] | undefined, + b: string[] | undefined +): boolean { + if (a === undefined || b === undefined) return a === b + return ( + a.length === b.length && + a.every((value, index) => { + const other = b[index] + return other !== undefined && sameNodeReference(ctx, value, other) + }) + ) +} +function isComponentPropertyDefinitions(value: unknown): value is ComponentPropertyDefinition[] { + if (!Array.isArray(value)) return false + return value.every(isComponentPropertyDefinition) +} + +function isComponentPropertyDefinition(value: unknown): value is ComponentPropertyDefinition { + if (!value || typeof value !== 'object') return false + const candidate = value as Partial + return ( + typeof candidate.id === 'string' && + typeof candidate.name === 'string' && + (candidate.type === 'VARIANT' || + candidate.type === 'TEXT' || + candidate.type === 'BOOLEAN' || + candidate.type === 'INSTANCE_SWAP') && + typeof candidate.defaultValue === 'string' + ) +} + +function verifyComponentPropertyDefinitions(ctx: VerifierContext): boolean { + if (!isComponentPropertyDefinitions(ctx.a) || !isComponentPropertyDefinitions(ctx.b)) { + return false + } + const aDefinitions = ctx.a + const bDefinitions = ctx.b + if (aDefinitions.length !== bDefinitions.length) return false + + return aDefinitions.every((value, index) => { + const definition = value + const other = bDefinitions[index] + if (!other || definition.type !== other.type || definition.name !== other.name) { + return false + } + + const { defaultValue, preferredValues, ...rest } = definition + const { + defaultValue: otherDefaultValue, + preferredValues: otherPreferredValues, + ...otherRest + } = other + if (JSON.stringify(rest) !== JSON.stringify(otherRest)) return false + + if (definition.type !== 'INSTANCE_SWAP') return defaultValue === otherDefaultValue + return ( + typeof defaultValue === 'string' && + typeof otherDefaultValue === 'string' && + sameNodeReference(ctx, defaultValue, otherDefaultValue) && + sameNodeReferences(ctx, preferredValues, otherPreferredValues) + ) + }) +} + +function isComponentPropertyAssignments(value: unknown): value is ComponentPropertyAssignments { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false + return Object.values(value).every((entry) => typeof entry === 'string') +} + +function verifyComponentPropertyAssignments(ctx: VerifierContext): boolean { + if (!isComponentPropertyAssignments(ctx.a) || !isComponentPropertyAssignments(ctx.b)) { + return false + } + + const aAssignments = ctx.a + const bAssignments = ctx.b + const propertyIds = new Set([...Object.keys(aAssignments), ...Object.keys(bAssignments)]) + for (const propertyId of propertyIds) { + const a = aAssignments[propertyId] + const b = bAssignments[propertyId] + if (a === b) continue + if (typeof a !== 'string' || typeof b !== 'string') return false + const aDefinition = ctx.aComponentPropertyDefinitions.get(propertyId) + const bDefinition = ctx.bComponentPropertyDefinitions.get(propertyId) + if (aDefinition?.type !== 'INSTANCE_SWAP' || bDefinition?.type !== 'INSTANCE_SWAP') { + return false + } + if (!sameNodeReference(ctx, a, b)) return false + } + return true +} + export const SCENE_VERIFIERS = new Map([ [ 'pluginData', @@ -93,7 +214,9 @@ export const SCENE_VERIFIERS = new Map([ if (!ctx.key.includes('componentPropertyDefinitions')) return false return ctx.a === 'VARIANT' && ctx.b === 'TEXT' } - ] + ], + ['componentPropertyDefinitions', verifyComponentPropertyDefinitions], + ['componentPropertyAssignments', verifyComponentPropertyAssignments] ]) function verifyAEntries(