130 lines
4.8 KiB
TypeScript
130 lines
4.8 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)
|
|
})
|
|
|
|
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 indigo marker color 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)
|
|
|
|
const flat = flattenFloorplan(geometry)
|
|
expect(flat.some((entry) => entry.kind === 'polygon' && entry.fill === '#818cf8')).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))
|
|
}
|