This commit is contained in:
2026-04-08 21:26:18 +08:00
commit 8fdc7ac0c3
401 changed files with 53093 additions and 0 deletions

View 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)
})
})