714 lines
22 KiB
TypeScript
714 lines
22 KiB
TypeScript
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)
|
||
})
|
||
})
|