2026-06-10 14:11:50 -04:00

139 lines
5.2 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
type FloorplanGeometry,
type GeometryContext,
SpawnNode as SpawnSchemaFromCore,
} from '@pascal-app/core'
import { spawnDefinition } from '../definition'
import { buildSpawnFloorplan } from '../floorplan'
import { SpawnNode } from '../schema'
/**
* Structural parity for the spawn registry definition.
*
* The new renderer is a near-line-by-line port of the legacy
* `@pascal-app/viewer/components/renderers/spawn/spawn-renderer.tsx` —
* same mesh count and primitives. The "parity" assertion
* for the spike is structural (definition is well-formed, both lazy
* modules resolve to React components) plus a manual visual eyeball check
* documented in the plan. Pixel-level Playwright parity lands in Phase 4
* when more nodes are migrated.
*/
describe('spawn definition', () => {
test('schema matches the core schema export', () => {
// Both imports must point to the same Zod schema — the registry
// definition re-exports from core.
expect(SpawnNode).toBe(SpawnSchemaFromCore)
})
test('definition has the expected shape', () => {
expect(spawnDefinition.kind).toBe('spawn')
expect(spawnDefinition.schemaVersion).toBe(1)
expect(spawnDefinition.category).toBe('site')
expect(spawnDefinition.schema).toBe(SpawnNode)
expect(typeof spawnDefinition.floorplanMoveTarget).toBe('function')
})
test('defaults() returns a value that the schema accepts', () => {
const defaults = spawnDefinition.defaults()
const parsed = SpawnNode.safeParse({ ...defaults, id: 'spawn_test1234567890ab' })
expect(parsed.success).toBe(true)
})
test('presentation declares a url palette icon', () => {
expect(spawnDefinition.presentation?.label).toBe('Spawn Point')
expect(spawnDefinition.presentation?.icon.kind).toBe('url')
expect(spawnDefinition.presentation?.paletteSection).toBe('structure')
})
test("movable capability restricts to X/Z (matches today's placement behavior)", () => {
expect(spawnDefinition.capabilities.movable?.axes).toEqual(['x', 'z'])
expect(spawnDefinition.capabilities.movable?.gridSnap).toBe(true)
})
test('rotatable capability declares yaw-only with diagonal-friendly snap angles', () => {
expect(spawnDefinition.capabilities.rotatable?.axes).toEqual(['y'])
const angles = spawnDefinition.capabilities.rotatable?.snapAngles ?? []
expect(angles.length).toBeGreaterThanOrEqual(3)
expect(angles).toContain(0)
})
test('handles expose rotation and move controls', () => {
expect(Array.isArray(spawnDefinition.handles)).toBe(true)
if (!Array.isArray(spawnDefinition.handles)) return
expect(spawnDefinition.handles.map((handle) => handle.kind)).toEqual([
'arc-resize',
'translate',
])
})
test('floorplan uses footprint marker oriented to the spawn view and selected rotation affordance', () => {
const spawn = SpawnNode.parse({
id: 'spawn_test1234567890ab',
position: [1, 0, 2],
rotation: Math.PI / 4,
})
const geometry = buildSpawnFloorplan(spawn, {
resolve: () => undefined,
children: [],
siblings: [],
parent: null,
viewState: {
selected: true,
highlighted: false,
hovered: false,
moving: false,
palette: {
selectedStroke: '#60a5fa',
selectedFill: '#dbeafe',
selectedHatch: '#60a5fa',
wallHoverStroke: '#60a5fa',
endpointHandleFill: '#fed7aa',
endpointHandleStroke: '#f97316',
endpointHandleHoverStroke: '#fb923c',
endpointHandleActiveFill: '#fdba74',
endpointHandleActiveStroke: '#ea580c',
curveHandleFill: '#99f6e4',
curveHandleStroke: '#14b8a6',
curveHandleHoverStroke: '#2dd4bf',
measurementStroke: '#6366f1',
measurementLabelBackground: '#ffffff',
measurementLabelText: '#111827',
},
},
} satisfies GeometryContext)
expect(geometry.kind).toBe('group')
const marker = geometry.kind === 'group' ? geometry.children[0] : null
expect(marker?.kind).toBe('group')
if (marker?.kind === 'group') {
expect(marker.transform?.rotate).toBe(-spawn.rotation)
}
const flat = flattenFloorplan(geometry)
expect(flat.some((entry) => entry.kind === 'path' && entry.stroke === '#818cf8')).toBe(true)
expect(flat.some((entry) => entry.kind === 'move-handle')).toBe(true)
expect(flat.some((entry) => entry.kind === 'rotate-arrow')).toBe(true)
})
test('renderer is a parametric lazy module reference', () => {
expect(spawnDefinition.renderer.kind).toBe('parametric')
if (spawnDefinition.renderer.kind !== 'parametric') return
expect(typeof spawnDefinition.renderer.module).toBe('function')
})
test('tool is a lazy module reference', () => {
expect(typeof spawnDefinition.tool).toBe('function')
})
test('mcp description is set so AI surfaces describe the kind', () => {
expect(spawnDefinition.mcp?.description).toBeDefined()
expect(spawnDefinition.mcp?.description?.length).toBeGreaterThan(0)
})
})
function flattenFloorplan(geometry: FloorplanGeometry): FloorplanGeometry[] {
if (geometry.kind !== 'group') return [geometry]
return geometry.children.flatMap((child) => flattenFloorplan(child))
}