all
This commit is contained in:
477
tests/unit/schema/componentSchema.test.ts
Normal file
477
tests/unit/schema/componentSchema.test.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
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<string, unknown> = {}) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user