all
This commit is contained in:
713
tests/unit/runtime/expressionEval.test.ts
Normal file
713
tests/unit/runtime/expressionEval.test.ts
Normal file
@@ -0,0 +1,713 @@
|
||||
import type { CanvasRuntimeVariable } from '@/components/editor/canvas/context/runtime'
|
||||
import type { Layer, LayerBindingDefinition } from '@/components/editor/canvas/types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
resolveLayerAppearance,
|
||||
resolveLayerBindingValue,
|
||||
resolveLayerDisplayValue,
|
||||
} from '@/components/editor/components/runtime'
|
||||
|
||||
// ──── 工具函数:构造测试用 Layer ────
|
||||
|
||||
function createLayer(overrides: Partial<Layer> = {}): Layer {
|
||||
return {
|
||||
id: 'layer-1',
|
||||
type: 'text',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 200,
|
||||
height: 40,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createVariableMap(entries: Record<string, { value: unknown, moduleLabel?: string, propLabel?: string }>): Record<string, CanvasRuntimeVariable> {
|
||||
const map: Record<string, CanvasRuntimeVariable> = {}
|
||||
for (const [path, { value, moduleLabel, propLabel }] of Object.entries(entries)) {
|
||||
map[path] = {
|
||||
path,
|
||||
moduleLabel: moduleLabel ?? path.split('.')[0] ?? 'MOD',
|
||||
moduleName: 'Module',
|
||||
propLabel: propLabel ?? path.split('.')[1] ?? path,
|
||||
propName: 'Property',
|
||||
type: 'analog',
|
||||
value,
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// resolveLayerDisplayValue
|
||||
// ============================================
|
||||
|
||||
describe('resolveLayerDisplayValue — 图层显示值解析', () => {
|
||||
const emptyVarMap: Record<string, CanvasRuntimeVariable> = {}
|
||||
|
||||
describe('text 类型图层', () => {
|
||||
it('无绑定时返回 config.content 的值', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
config: { content: '静态文本' },
|
||||
})
|
||||
expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('静态文本')
|
||||
})
|
||||
|
||||
it('无绑定且无 config 时返回空字符串', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('')
|
||||
})
|
||||
|
||||
it('有绑定时优先使用绑定值', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
config: { content: '静态文本' },
|
||||
bindings: { value: 'MOD.temperature' },
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.temperature': { value: '高温告警' },
|
||||
})
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('高温告警')
|
||||
})
|
||||
})
|
||||
|
||||
describe('number 类型图层', () => {
|
||||
it('无绑定时显示默认数值 0 并保留 2 位小数', () => {
|
||||
const layer = createLayer({ type: 'number' })
|
||||
expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('0.00')
|
||||
})
|
||||
|
||||
it('有绑定时显示变量值并格式化', () => {
|
||||
const layer = createLayer({
|
||||
type: 'number',
|
||||
config: { decimals: 1 },
|
||||
bindings: { value: 'MOD.temperature' },
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.temperature': { value: 36.567 },
|
||||
})
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('36.6')
|
||||
})
|
||||
|
||||
it('应正确应用前缀和后缀', () => {
|
||||
const layer = createLayer({
|
||||
type: 'number',
|
||||
config: { decimals: 0, prefix: '温度:', suffix: '℃' },
|
||||
bindings: { value: 'MOD.temp' },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.temp': { value: 25 } })
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('温度:25℃')
|
||||
})
|
||||
|
||||
it('非数值绑定应回退为 0', () => {
|
||||
const layer = createLayer({
|
||||
type: 'number',
|
||||
config: { decimals: 2 },
|
||||
bindings: { value: 'MOD.invalid' },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.invalid': { value: 'not-a-number' } })
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('0.00')
|
||||
})
|
||||
|
||||
it('小数位 decimals 配置为 0 时不显示小数部分', () => {
|
||||
const layer = createLayer({
|
||||
type: 'number',
|
||||
config: { decimals: 0 },
|
||||
bindings: { value: 'MOD.count' },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.count': { value: 42.999 } })
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('43')
|
||||
})
|
||||
})
|
||||
|
||||
describe('rect 类型图层', () => {
|
||||
it('无绑定且无 value 时返回空字符串', () => {
|
||||
const layer = createLayer({ type: 'rect' })
|
||||
expect(resolveLayerDisplayValue(layer, emptyVarMap)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('null / undefined 值处理', () => {
|
||||
it('绑定值为 null 时返回空字符串', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
bindings: { value: 'MOD.nullVar' },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.nullVar': { value: null } })
|
||||
// 变量存在但值为 null → 在 resolveLayerBindingValue 中返回 null
|
||||
// resolveLayerDisplayValue 中 null 走 String 转换前会被拦截返回 ''
|
||||
expect(resolveLayerDisplayValue(layer, varMap)).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// resolveLayerBindingValue
|
||||
// ============================================
|
||||
|
||||
describe('resolveLayerBindingValue — 绑定值解析', () => {
|
||||
describe('variable 类型绑定', () => {
|
||||
it('字符串绑定应解析为变量引用', () => {
|
||||
const layer = createLayer({
|
||||
bindings: { value: 'MOD.pressure' },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.pressure': { value: 101.3 } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(101.3)
|
||||
})
|
||||
|
||||
it('变量不存在时应返回 undefined', () => {
|
||||
const layer = createLayer({
|
||||
bindings: { value: 'MOD.nonexistent' },
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('无绑定定义时应返回 undefined', () => {
|
||||
const layer = createLayer()
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('对象格式的 variable 绑定应正确解析', () => {
|
||||
const binding: LayerBindingDefinition = {
|
||||
id: 'b1',
|
||||
type: 'variable',
|
||||
value: 'MOD.level',
|
||||
priority: 0,
|
||||
}
|
||||
const layer = createLayer({
|
||||
bindings: { value: binding },
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.level': { value: 85 } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(85)
|
||||
})
|
||||
})
|
||||
|
||||
describe('expression 类型绑定', () => {
|
||||
it('简单数学表达式应正确计算', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'e1',
|
||||
type: 'expression',
|
||||
value: 'vars["MOD.a"] + vars["MOD.b"]',
|
||||
variables: ['MOD.a', 'MOD.b'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.a': { value: 10 },
|
||||
'MOD.b': { value: 20 },
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(30)
|
||||
})
|
||||
|
||||
it('三元表达式应正确计算', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'e2',
|
||||
type: 'expression',
|
||||
value: 'vars["MOD.flag"] ? "开启" : "关闭"',
|
||||
variables: ['MOD.flag'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.flag': { value: true } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('开启')
|
||||
})
|
||||
|
||||
it('使用 Math 内置对象的表达式', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'e3',
|
||||
type: 'expression',
|
||||
value: 'Math.round(vars["MOD.val"] * 100) / 100',
|
||||
variables: ['MOD.val'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.val': { value: 3.14159 } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(3.14)
|
||||
})
|
||||
|
||||
it('无效表达式应返回 undefined 且不抛异常', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'e4',
|
||||
type: 'expression',
|
||||
value: '!!!invalid syntax{{{',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('通过模块标签访问变量的表达式', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'e5',
|
||||
type: 'expression',
|
||||
value: 'MOD["temperature"] * 1.8 + 32',
|
||||
variables: ['MOD.temperature'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.temperature': { value: 100, moduleLabel: 'MOD', propLabel: 'temperature' },
|
||||
})
|
||||
// 100 * 1.8 + 32 = 212 (摄氏转华氏)
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(212)
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定优先级选择', () => {
|
||||
it('多条绑定规则取优先级最高的', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: [
|
||||
{
|
||||
id: 'low',
|
||||
type: 'variable',
|
||||
value: 'MOD.low',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
type: 'variable',
|
||||
value: 'MOD.high',
|
||||
priority: 10,
|
||||
},
|
||||
{
|
||||
id: 'mid',
|
||||
type: 'variable',
|
||||
value: 'MOD.mid',
|
||||
priority: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.low': { value: 'low' },
|
||||
'MOD.high': { value: 'high' },
|
||||
'MOD.mid': { value: 'mid' },
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('high')
|
||||
})
|
||||
|
||||
it('优先级相同时取最后出现的', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: [
|
||||
{
|
||||
id: 'a',
|
||||
type: 'variable',
|
||||
value: 'MOD.first',
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
type: 'variable',
|
||||
value: 'MOD.second',
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.first': { value: 'first' },
|
||||
'MOD.second': { value: 'second' },
|
||||
})
|
||||
// 同优先级时,后者 priority 不大于前者,所以前者保持选中
|
||||
// 源码: priority > selectedPriority → 仅严格大于才替换,所以 first 胜出
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('first')
|
||||
})
|
||||
})
|
||||
|
||||
describe('enabled/disabled 绑定过滤', () => {
|
||||
it('enabled: false 的绑定应被跳过', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: [
|
||||
{
|
||||
id: 'disabled',
|
||||
type: 'variable',
|
||||
value: 'MOD.disabled_val',
|
||||
priority: 100,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'enabled',
|
||||
type: 'variable',
|
||||
value: 'MOD.enabled_val',
|
||||
priority: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.disabled_val': { value: '不应出现' },
|
||||
'MOD.enabled_val': { value: '应该出现' },
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe('应该出现')
|
||||
})
|
||||
|
||||
it('所有绑定都 disabled 时应返回 undefined', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: [
|
||||
{
|
||||
id: 'd1',
|
||||
type: 'variable',
|
||||
value: 'MOD.a',
|
||||
priority: 1,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'd2',
|
||||
type: 'variable',
|
||||
value: 'MOD.b',
|
||||
priority: 2,
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('绑定值为空字符串时应被跳过', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: [
|
||||
{
|
||||
id: 'empty',
|
||||
type: 'variable',
|
||||
value: '',
|
||||
priority: 100,
|
||||
},
|
||||
{
|
||||
id: 'valid',
|
||||
type: 'variable',
|
||||
value: 'MOD.val',
|
||||
priority: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.val': { value: 42 } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(42)
|
||||
})
|
||||
|
||||
it('绑定值为纯空格时应被跳过', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'spaces',
|
||||
type: 'variable',
|
||||
value: ' ',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// resolveLayerAppearance
|
||||
// ============================================
|
||||
|
||||
describe('resolveLayerAppearance — 外观样式解析', () => {
|
||||
const emptyVarMap: Record<string, CanvasRuntimeVariable> = {}
|
||||
const autoTextColor = '#111827'
|
||||
|
||||
describe('文字颜色解析', () => {
|
||||
it('无 style 和 config 时使用 autoTextColor', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.textColor).toBe(autoTextColor)
|
||||
})
|
||||
|
||||
it('style.text.color 优先于 autoTextColor', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
style: { text: { color: '#FF0000' } },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.textColor).toBe('#FF0000')
|
||||
})
|
||||
|
||||
it('config.textColor 作为备选', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
config: { textColor: '#00FF00' },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.textColor).toBe('#00FF00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('背景颜色解析', () => {
|
||||
it('rect 类型无配置时使用默认填充色 #D7EBFF', () => {
|
||||
const layer = createLayer({ type: 'rect' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.backgroundColor).not.toBeNull()
|
||||
expect(result.backgroundColor!.startsWith('#D7EBFF')).toBe(true)
|
||||
})
|
||||
|
||||
it('text 类型无配置时背景为 null', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.backgroundColor).toBeNull()
|
||||
})
|
||||
|
||||
it('通过 style.fill.color 设置背景色', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
style: { fill: { color: '#AABBCC' } },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.backgroundColor).not.toBeNull()
|
||||
expect(result.backgroundColor!.startsWith('#AABBCC')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('边框样式解析', () => {
|
||||
it('无配置时边框宽度为 0', () => {
|
||||
const layer = createLayer({ type: 'rect' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.borderWidth).toBe(0)
|
||||
expect(result.borderStyle).toBe('solid')
|
||||
})
|
||||
|
||||
it('应正确读取 style.border 属性', () => {
|
||||
const layer = createLayer({
|
||||
type: 'rect',
|
||||
style: {
|
||||
border: { color: '#333', width: 2, style: 'dashed', radius: 8 },
|
||||
},
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.borderColor).toBe('#333')
|
||||
expect(result.borderWidth).toBe(2)
|
||||
expect(result.borderStyle).toBe('dashed')
|
||||
expect(result.borderRadius).toBe(8)
|
||||
})
|
||||
|
||||
it('负数边框宽度应被 clamp 为 0', () => {
|
||||
const layer = createLayer({
|
||||
type: 'rect',
|
||||
style: { border: { width: -5 } },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.borderWidth).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('字体样式解析', () => {
|
||||
it('text 类型默认字号为 18', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.fontSize).toBe(18)
|
||||
})
|
||||
|
||||
it('number 类型默认字号为 24', () => {
|
||||
const layer = createLayer({ type: 'number' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.fontSize).toBe(24)
|
||||
})
|
||||
|
||||
it('字号最小值为 10', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
style: { text: { fontSize: 5 } },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.fontSize).toBe(10)
|
||||
})
|
||||
|
||||
it('number 类型默认字重为 700', () => {
|
||||
const layer = createLayer({ type: 'number' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.fontWeight).toBe(700)
|
||||
})
|
||||
|
||||
it('text 类型默认字重为 500', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.fontWeight).toBe(500)
|
||||
})
|
||||
|
||||
it('默认对齐方式为 center / middle', () => {
|
||||
const layer = createLayer({ type: 'text' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.textAlign).toBe('center')
|
||||
expect(result.verticalAlign).toBe('middle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('阴影样式解析', () => {
|
||||
it('无阴影配置时 boxShadow 为 none', () => {
|
||||
const layer = createLayer({ type: 'rect' })
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.boxShadow).toBe('none')
|
||||
})
|
||||
|
||||
it('有阴影配置时应生成 CSS box-shadow 值', () => {
|
||||
const layer = createLayer({
|
||||
type: 'rect',
|
||||
style: {
|
||||
shadow: {
|
||||
color: '#000000',
|
||||
blur: 10,
|
||||
offsetX: 2,
|
||||
offsetY: 4,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.boxShadow).toBe('2px 4px 10px #000000')
|
||||
})
|
||||
|
||||
it('阴影 enabled 为 false 时返回 none', () => {
|
||||
const layer = createLayer({
|
||||
type: 'rect',
|
||||
style: {
|
||||
shadow: {
|
||||
color: '#000000',
|
||||
blur: 10,
|
||||
offsetX: 2,
|
||||
offsetY: 4,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.boxShadow).toBe('none')
|
||||
})
|
||||
})
|
||||
|
||||
describe('通过绑定覆盖外观属性', () => {
|
||||
it('绑定值应覆盖静态样式属性', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
style: { text: { color: '#111111' } },
|
||||
bindings: {
|
||||
'style.text.color': 'MOD.textColor',
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.textColor': { value: '#FF5500' },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, varMap)
|
||||
expect(result.textColor).toBe('#FF5500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('displayValue 包含在返回结果中', () => {
|
||||
it('resolveLayerAppearance 结果包含 displayValue', () => {
|
||||
const layer = createLayer({
|
||||
type: 'text',
|
||||
config: { content: '测试内容' },
|
||||
})
|
||||
const result = resolveLayerAppearance(layer, autoTextColor, emptyVarMap)
|
||||
expect(result.displayValue).toBe('测试内容')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// 表达式安全沙盒
|
||||
// ============================================
|
||||
|
||||
describe('表达式安全沙盒', () => {
|
||||
it('应拒绝包含 constructor 的表达式', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'attack1',
|
||||
type: 'expression',
|
||||
value: 'this.constructor.constructor("return globalThis")()',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('应拒绝包含 window 的表达式', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'attack2',
|
||||
type: 'expression',
|
||||
value: 'window.location.href',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('应拒绝包含 __proto__ 的表达式', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'attack3',
|
||||
type: 'expression',
|
||||
value: '({}).__proto__',
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('正常表达式应不受影响', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'safe',
|
||||
type: 'expression',
|
||||
value: 'vars["MOD.a"] + 10',
|
||||
variables: ['MOD.a'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({ 'MOD.a': { value: 5 } })
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(15)
|
||||
})
|
||||
|
||||
it('使用 Math 的表达式应正常工作', () => {
|
||||
const layer = createLayer({
|
||||
bindings: {
|
||||
value: {
|
||||
id: 'math',
|
||||
type: 'expression',
|
||||
value: 'Math.max(vars["MOD.a"], vars["MOD.b"])',
|
||||
variables: ['MOD.a', 'MOD.b'],
|
||||
priority: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const varMap = createVariableMap({
|
||||
'MOD.a': { value: 3 },
|
||||
'MOD.b': { value: 7 },
|
||||
})
|
||||
expect(resolveLayerBindingValue(layer, 'value', varMap)).toBe(7)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user