import { barComponentSchema, buttonComponentConfigSchema, buttonComponentSchema, componentBaseSchema, componentEventSchema, componentTypeSchema, numberComponentConfigSchema, numberComponentSchema, rectComponentSchema, textComponentConfigSchema, textComponentSchema, } from '@cslab-dcs/schema/canvas/component' import { describe, expect, it } from 'vitest' // ──── 测试辅助函数 ──── function createValidBase(overrides: Record = {}) { return { id: '550e8400-e29b-41d4-a716-446655440000', type: 'rect', name: '测试组件', position: { x: 100, y: 200 }, size: { width: 300, height: 150 }, ...overrides, } } // ============================================ // componentTypeSchema — 组件类型枚举 // ============================================ describe('componentTypeSchema — 组件类型枚举', () => { it('应接受所有合法的组件类型', () => { const validTypes = ['rect', 'number', 'text', 'bar', 'button', 'pidController', 'canvasSwitcher', 'custom'] for (const type of validTypes) { expect(componentTypeSchema.safeParse(type).success).toBe(true) } }) it('应拒绝无效的组件类型', () => { expect(componentTypeSchema.safeParse('invalid').success).toBe(false) expect(componentTypeSchema.safeParse('').success).toBe(false) expect(componentTypeSchema.safeParse(123).success).toBe(false) }) }) // ============================================ // componentBaseSchema — 组件基础属性 // ============================================ describe('componentBaseSchema — 组件基础属性', () => { it('最小合法数据应通过验证', () => { const result = componentBaseSchema.safeParse(createValidBase()) expect(result.success).toBe(true) }) it('id 必须是合法的 UUID', () => { const result = componentBaseSchema.safeParse(createValidBase({ id: 'not-a-uuid' })) expect(result.success).toBe(false) }) it('id 不能为空', () => { const result = componentBaseSchema.safeParse(createValidBase({ id: '' })) expect(result.success).toBe(false) }) it('name 不能为空字符串', () => { const result = componentBaseSchema.safeParse(createValidBase({ name: '' })) expect(result.success).toBe(false) }) it('position 必须包含 x 和 y', () => { const result = componentBaseSchema.safeParse(createValidBase({ position: { x: 10 } })) expect(result.success).toBe(false) }) it('size 的 width 和 height 必须为正数', () => { const negativeWidth = componentBaseSchema.safeParse(createValidBase({ size: { width: -10, height: 100 } })) expect(negativeWidth.success).toBe(false) const zeroHeight = componentBaseSchema.safeParse(createValidBase({ size: { width: 100, height: 0 } })) expect(zeroHeight.success).toBe(false) }) it('默认值:zIndex 应为 0', () => { const result = componentBaseSchema.parse(createValidBase()) expect(result.zIndex).toBe(0) }) it('默认值:opacity 应为 1', () => { const result = componentBaseSchema.parse(createValidBase()) expect(result.opacity).toBe(1) }) it('默认值:visible 应为 true', () => { const result = componentBaseSchema.parse(createValidBase()) expect(result.visible).toBe(true) }) it('opacity 必须在 0-1 范围内', () => { const tooHigh = componentBaseSchema.safeParse(createValidBase({ opacity: 1.5 })) expect(tooHigh.success).toBe(false) const tooLow = componentBaseSchema.safeParse(createValidBase({ opacity: -0.1 })) expect(tooLow.success).toBe(false) const valid = componentBaseSchema.safeParse(createValidBase({ opacity: 0.5 })) expect(valid.success).toBe(true) }) it('zIndex 必须是整数', () => { const floatZIndex = componentBaseSchema.safeParse(createValidBase({ zIndex: 1.5 })) expect(floatZIndex.success).toBe(false) }) it('可选属性 bindings / style / events / metadata 为空时通过验证', () => { const data = createValidBase() const result = componentBaseSchema.safeParse(data) expect(result.success).toBe(true) }) it('events 数组应验证每个事件的格式', () => { const data = createValidBase({ events: [ { id: 'evt-1', trigger: 'click', action: { type: 'callMethod' }, }, ], }) const result = componentBaseSchema.safeParse(data) expect(result.success).toBe(true) }) }) // ============================================ // componentEventSchema — 事件 Schema // ============================================ describe('componentEventSchema — 事件 Schema', () => { it('合法事件应通过验证', () => { const event = { id: 'evt-1', trigger: 'click', action: { type: 'callMethod' }, } expect(componentEventSchema.safeParse(event).success).toBe(true) }) it('trigger 只能是 click / dblclick', () => { const invalid = { id: 'evt-1', trigger: 'hover', action: { type: 'callMethod' }, } expect(componentEventSchema.safeParse(invalid).success).toBe(false) }) it('dblclick 触发器应通过验证', () => { const event = { id: 'evt-1', trigger: 'dblclick', action: { type: 'callMethod' }, } expect(componentEventSchema.safeParse(event).success).toBe(true) }) it('action.type 不能为空字符串', () => { const invalid = { id: 'evt-1', trigger: 'click', action: { type: '' }, } expect(componentEventSchema.safeParse(invalid).success).toBe(false) }) it('可选属性 condition 应通过验证', () => { const event = { id: 'evt-1', trigger: 'click', condition: 'vars.status === "running"', action: { type: 'navigate', payload: { target: 'page2' } }, } const result = componentEventSchema.safeParse(event) expect(result.success).toBe(true) }) }) // ============================================ // rectComponentSchema — 矩形组件 // ============================================ describe('rectComponentSchema — 矩形组件', () => { it('合法矩形组件应通过验证', () => { const data = createValidBase({ type: 'rect' }) expect(rectComponentSchema.safeParse(data).success).toBe(true) }) it('type 必须是 rect', () => { const data = createValidBase({ type: 'text' }) expect(rectComponentSchema.safeParse(data).success).toBe(false) }) it('可选 config.fillColor', () => { const data = createValidBase({ type: 'rect', config: { fillColor: '#FF0000' }, }) const result = rectComponentSchema.safeParse(data) expect(result.success).toBe(true) if (result.success) { expect(result.data.config?.fillColor).toBe('#FF0000') } }) it('无 config 时也应通过', () => { const data = createValidBase({ type: 'rect' }) expect(rectComponentSchema.safeParse(data).success).toBe(true) }) }) // ============================================ // numberComponentSchema / numberComponentConfigSchema — 数字组件 // ============================================ describe('numberComponentConfigSchema — 数字组件配置', () => { it('空对象应通过(全部可选或有默认值)', () => { const result = numberComponentConfigSchema.safeParse({}) expect(result.success).toBe(true) }) it('默认值:decimals 应为 2', () => { const result = numberComponentConfigSchema.parse({}) expect(result.decimals).toBe(2) }) it('默认值:thousandsSeparator 应为 false', () => { const result = numberComponentConfigSchema.parse({}) expect(result.thousandsSeparator).toBe(false) }) it('默认值:showTrend 应为 false', () => { const result = numberComponentConfigSchema.parse({}) expect(result.showTrend).toBe(false) }) it('decimals 必须在 0-10 范围内', () => { expect(numberComponentConfigSchema.safeParse({ decimals: -1 }).success).toBe(false) expect(numberComponentConfigSchema.safeParse({ decimals: 11 }).success).toBe(false) expect(numberComponentConfigSchema.safeParse({ decimals: 5 }).success).toBe(true) }) it('decimals 必须是整数', () => { expect(numberComponentConfigSchema.safeParse({ decimals: 1.5 }).success).toBe(false) }) it('prefix 和 suffix 应为字符串', () => { const result = numberComponentConfigSchema.safeParse({ prefix: 'T=', suffix: '℃', }) expect(result.success).toBe(true) }) }) describe('numberComponentSchema — 数字组件', () => { it('合法数字组件应通过验证', () => { const data = createValidBase({ type: 'number', config: { decimals: 3, suffix: '℃' }, }) expect(numberComponentSchema.safeParse(data).success).toBe(true) }) it('type 必须是 number', () => { const data = createValidBase({ type: 'rect' }) expect(numberComponentSchema.safeParse(data).success).toBe(false) }) }) // ============================================ // textComponentSchema / textComponentConfigSchema — 文本组件 // ============================================ describe('textComponentConfigSchema — 文本组件配置', () => { it('空对象应通过', () => { expect(textComponentConfigSchema.safeParse({}).success).toBe(true) }) it('默认值:content 应为空字符串', () => { const result = textComponentConfigSchema.parse({}) expect(result.content).toBe('') }) it('默认值:isDynamic 应为 false', () => { const result = textComponentConfigSchema.parse({}) expect(result.isDynamic).toBe(false) }) it('包含所有字段时应通过验证', () => { const config = { fillColor: '#FFFFFF', textColor: '#000000', content: '测试内容', isDynamic: true, expression: 'vars["MOD.val"]', } const result = textComponentConfigSchema.safeParse(config) expect(result.success).toBe(true) }) }) describe('textComponentSchema — 文本组件', () => { it('合法文本组件应通过验证', () => { const data = createValidBase({ type: 'text', config: { content: 'Hello' }, }) expect(textComponentSchema.safeParse(data).success).toBe(true) }) }) // ============================================ // barComponentSchema — 棒图组件 // ============================================ describe('barComponentSchema — 棒图组件', () => { it('合法棒图组件应通过验证', () => { const data = createValidBase({ type: 'bar', config: { min: 0, max: 100, direction: 'vertical' }, }) expect(barComponentSchema.safeParse(data).success).toBe(true) }) it('config 可以包含分段颜色', () => { const data = createValidBase({ type: 'bar', config: { min: 0, max: 100, colors: [ { threshold: 30, color: '#00FF00' }, { threshold: 70, color: '#FFFF00' }, { threshold: 100, color: '#FF0000' }, ], }, }) const result = barComponentSchema.safeParse(data) expect(result.success).toBe(true) }) it('direction 只能是 horizontal 或 vertical', () => { const data = createValidBase({ type: 'bar', config: { direction: 'diagonal' }, }) const result = barComponentSchema.safeParse(data) expect(result.success).toBe(false) }) }) // ============================================ // buttonComponentSchema / buttonComponentConfigSchema — 按钮组件 // ============================================ describe('buttonComponentConfigSchema — 按钮组件配置', () => { it('空对象应通过(使用默认值)', () => { expect(buttonComponentConfigSchema.safeParse({}).success).toBe(true) }) it('默认值:label 应为 "按钮"', () => { const result = buttonComponentConfigSchema.parse({}) expect(result.label).toBe('按钮') }) it('默认值:buttonType 应为 trigger', () => { const result = buttonComponentConfigSchema.parse({}) expect(result.buttonType).toBe('trigger') }) it('默认值:confirmRequired 应为 false', () => { const result = buttonComponentConfigSchema.parse({}) expect(result.confirmRequired).toBe(false) }) it('buttonType 只能是 trigger / toggle / navigate', () => { expect(buttonComponentConfigSchema.safeParse({ buttonType: 'trigger' }).success).toBe(true) expect(buttonComponentConfigSchema.safeParse({ buttonType: 'toggle' }).success).toBe(true) expect(buttonComponentConfigSchema.safeParse({ buttonType: 'navigate' }).success).toBe(true) expect(buttonComponentConfigSchema.safeParse({ buttonType: 'invalid' }).success).toBe(false) }) }) describe('buttonComponentSchema — 按钮组件', () => { it('合法按钮组件应通过验证', () => { const data = createValidBase({ type: 'button', config: { label: '启动', buttonType: 'trigger', targetMethod: 'startPump', confirmRequired: true, confirmMessage: '确认启动泵?', }, }) expect(buttonComponentSchema.safeParse(data).success).toBe(true) }) }) // ============================================ // 完整组件数据验证 // ============================================ describe('完整组件数据验证', () => { it('包含所有可选字段的完整组件应通过验证', () => { const fullComponent = { id: '550e8400-e29b-41d4-a716-446655440000', type: 'text', name: '温度显示', position: { x: 50, y: 100 }, size: { width: 200, height: 40 }, zIndex: 5, opacity: 0.8, visible: true, style: { fill: { color: '#FFFFFF', opacity: 0.5 }, border: { color: '#333333', width: 1, style: 'solid' as const, radius: 4 }, text: { fontSize: 16, color: '#000000', align: 'center' as const }, shadow: { color: '#000000', blur: 4, offsetX: 1, offsetY: 2, enabled: true }, }, events: [ { id: 'evt-1', trigger: 'click', action: { type: 'callMethod', payload: { method: 'reset' } }, }, ], metadata: { groupId: 'g1', groupName: '温度组' }, } const result = textComponentSchema.safeParse(fullComponent) expect(result.success).toBe(true) }) it('缺少必填字段 name 时应失败', () => { const data = { id: '550e8400-e29b-41d4-a716-446655440000', type: 'rect', position: { x: 0, y: 0 }, size: { width: 100, height: 100 }, } expect(componentBaseSchema.safeParse(data).success).toBe(false) }) it('缺少必填字段 position 时应失败', () => { const data = { id: '550e8400-e29b-41d4-a716-446655440000', type: 'rect', name: '测试', size: { width: 100, height: 100 }, } expect(componentBaseSchema.safeParse(data).success).toBe(false) }) it('缺少必填字段 size 时应失败', () => { const data = { id: '550e8400-e29b-41d4-a716-446655440000', type: 'rect', name: '测试', position: { x: 0, y: 0 }, } expect(componentBaseSchema.safeParse(data).success).toBe(false) }) })