Merge pull request #605 from phoenixweiss/codex/fig-component-property-guid-stability

test(fig): compare instance swap targets semantically
This commit is contained in:
Danila Poyarkov 2026-08-31 20:37:15 +03:00 committed by GitHub
commit bdccf2dfe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 134 additions and 1 deletions

View file

@ -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,

View file

@ -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<string, SceneNode>
aGraph: SceneGraph
bGraph: SceneGraph
aNodePaths: ReadonlyMap<string, string>
bNodePaths: ReadonlyMap<string, string>
aComponentPropertyDefinitions: ReadonlyMap<string, ComponentPropertyDefinition>
bComponentPropertyDefinitions: ReadonlyMap<string, ComponentPropertyDefinition>
errors: Mismatch[]
fixture: FixtureSpec
label: string
@ -60,6 +65,122 @@ export function isColorObj(v: unknown): v is Record<string, number> {
)
}
export function buildNodePathIndex(nodes: ReadonlyMap<string, SceneNode>): Map<string, string> {
return new Map([...nodes].map(([path, node]) => [node.id, path]))
}
export function buildComponentPropertyDefinitionIndex(
graph: SceneGraph
): Map<string, ComponentPropertyDefinition> {
const definitions = new Map<string, ComponentPropertyDefinition>()
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<ComponentPropertyDefinition>
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<string, Verifier>([
[
'pluginData',
@ -93,7 +214,9 @@ export const SCENE_VERIFIERS = new Map<string, Verifier>([
if (!ctx.key.includes('componentPropertyDefinitions')) return false
return ctx.a === 'VARIANT' && ctx.b === 'TEXT'
}
]
],
['componentPropertyDefinitions', verifyComponentPropertyDefinitions],
['componentPropertyAssignments', verifyComponentPropertyAssignments]
])
function verifyAEntries(