Files
test/tests/unit/schema/componentSchema.test.ts
2026-04-08 21:26:18 +08:00

478 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})