dsh-muv-table 0.2.12 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -1
- package/lib/client.js +784 -60
- package/lib/index.js +54 -8
- package/lib/muv-parser.js +70 -3
- package/lib/panel.html +7 -5
- package/package.json +6 -3
- package/test-card-ops.mjs +152 -0
- package/test-muv-parser.mjs +121 -0
- package/test-preset-resolve.mjs +89 -4
- package/test-var-section.mjs +272 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// 运行时变量折叠区回归(P1:自 dsh-tavern-v2「🔢 变量面板」迁入 MUV 表格面板)。
|
|
2
|
+
//
|
|
3
|
+
// Run: node test-var-section.mjs
|
|
4
|
+
//
|
|
5
|
+
// 为什么测这些:这条链路的"协议"是 muv-engine 的 HTTP 形状 —— 保存请求体必须是
|
|
6
|
+
// 精确的 { sessionId, merge:true, data }(merge 语义只加不改不删,null/0/false/''
|
|
7
|
+
// 这些"假值叶子"一旦在提交前被 coerce 就会被静默吃掉);以及三块纯行为契约:
|
|
8
|
+
// 失败保留本地 edits、已覆盖三态、快照入口探测失败必须隐藏。
|
|
9
|
+
//
|
|
10
|
+
// 本用例**逐字提取** lib/client.js 里两端标记之间的那段纯函数执行 —— 不是复制一份到
|
|
11
|
+
// 测试里,所以被测代码改了它就会跟着变(结构断言会因此变红)。
|
|
12
|
+
// 因此那段代码有硬约束:不许引用 React / DOM / 外层闭包变量,只用入参 + 全局 fetch。
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
let pass = 0, fail = 0
|
|
19
|
+
function check(name, cond, detail) {
|
|
20
|
+
if (cond) { pass++; console.log(' OK ' + name) }
|
|
21
|
+
else { fail++; console.log(' FAIL ' + name + (detail ? ' -> ' + detail : '')) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const CLIENT = path.join(here, 'lib', 'client.js')
|
|
26
|
+
const src = fs.readFileSync(CLIENT, 'utf8')
|
|
27
|
+
|
|
28
|
+
// ── 逐字提取被标记的纯函数段 ─────────────────────────────────────────
|
|
29
|
+
const BEGIN = '// [[var-section-core-start]]'
|
|
30
|
+
const END = '// [[var-section-core-end]]'
|
|
31
|
+
const i0 = src.indexOf(BEGIN)
|
|
32
|
+
const i1 = src.indexOf(END)
|
|
33
|
+
if (i0 < 0 || i1 < 0 || i1 < i0) {
|
|
34
|
+
console.log('FAIL 在 lib/client.js 里找不到 [[var-section-core-start/end]] 标记')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
const extracted = src.slice(i0 + BEGIN.length, i1).trim()
|
|
38
|
+
|
|
39
|
+
// UI 切片(含 React 壳):从核心段开头到 MacroPanel 标记,供结构断言用
|
|
40
|
+
const UI_END = '// ============ MacroPanel ============'
|
|
41
|
+
const uiSlice = src.slice(i0, src.indexOf(UI_END))
|
|
42
|
+
|
|
43
|
+
const api = new Function(extracted + '\nreturn {' +
|
|
44
|
+
'varSectionLoadState, varSectionSave, varSectionListSnapshots, varSectionEnterSnapshot,' +
|
|
45
|
+
'varSectionEditsToPayload, varSectionIsOverridden, varSectionLeafDisplay, varSectionNormalizeEdit,' +
|
|
46
|
+
'varSectionApplySaveResult, varSectionDefaultExpanded, varSectionSearchHits, varSectionSnapLabel,' +
|
|
47
|
+
'varSectionExportText, VAR_PATH_SEP }')()
|
|
48
|
+
|
|
49
|
+
// 假的 fetch:记录请求,按脚本回响应
|
|
50
|
+
function mockFetch(responder) {
|
|
51
|
+
const calls = []
|
|
52
|
+
const f = async (url, init) => {
|
|
53
|
+
calls.push({ url, init })
|
|
54
|
+
return responder(url, init)
|
|
55
|
+
}
|
|
56
|
+
f.calls = calls
|
|
57
|
+
return f
|
|
58
|
+
}
|
|
59
|
+
function jsonRes(status, obj) {
|
|
60
|
+
return { status, ok: status >= 200 && status < 300, json: async () => obj }
|
|
61
|
+
}
|
|
62
|
+
function htmlRes(status) {
|
|
63
|
+
return { status, ok: false, json: async () => { throw new SyntaxError('Unexpected token <') } }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SID = 'session-0123456789abcdef0123456789abcdef'
|
|
67
|
+
const SEP = api.VAR_PATH_SEP
|
|
68
|
+
|
|
69
|
+
console.log('=== 📊 运行时变量折叠区(逐字提取执行)===\n')
|
|
70
|
+
|
|
71
|
+
console.log('[A] 结构不变量(提取段必须自洽,否则"逐字提取"就成了假的)')
|
|
72
|
+
check('提取段含 varSectionSave / varSectionLoadState 声明',
|
|
73
|
+
/async function varSectionSave\s*\(/.test(extracted) && /async function varSectionLoadState\s*\(/.test(extracted))
|
|
74
|
+
check('提取段不引用 React / document / window / innerHTML(否则无法独立执行)',
|
|
75
|
+
!/\bReact\b|\bdocument\b|\bwindow\b|innerHTML/.test(extracted),
|
|
76
|
+
(extracted.match(/\bReact\b|\bdocument\b|\bwindow\b|innerHTML/g) || []).join(','))
|
|
77
|
+
check('标记在文件里各出现一次',
|
|
78
|
+
src.split(BEGIN).length - 1 === 1 && src.split(END).length - 1 === 1)
|
|
79
|
+
|
|
80
|
+
console.log('\n[B] 保存按钮 → 请求体**精确形状** { sessionId, merge:true, data }')
|
|
81
|
+
{
|
|
82
|
+
const edits = {}
|
|
83
|
+
edits['战斗' + SEP + 'hp'] = { value: 0, type: 'number' }
|
|
84
|
+
edits['旗标'] = { value: false, type: 'boolean' }
|
|
85
|
+
edits['备注'] = { value: '', type: 'string' }
|
|
86
|
+
edits['深度' + SEP + '嵌套' + SEP + '空串'] = { value: '', type: 'string' }
|
|
87
|
+
const f = mockFetch(() => jsonRes(200, { ok: true }))
|
|
88
|
+
const r = await api.varSectionSave({ sessionId: SID, edits, fetch: f })
|
|
89
|
+
check('只发一次请求', f.calls.length === 1, 'calls=' + f.calls.length)
|
|
90
|
+
const c = f.calls[0] || {}
|
|
91
|
+
check('打的是 POST /api/muv-engine/state',
|
|
92
|
+
c.url === '/api/muv-engine/state' && c.init && c.init.method === 'POST',
|
|
93
|
+
String(c.url) + ' ' + String(c.init && c.init.method))
|
|
94
|
+
check('请求体精确形状(merge:true;edits 拍成嵌套 data;值原样)',
|
|
95
|
+
c.init && c.init.body === '{"sessionId":"' + SID + '","merge":true,"data":{"战斗":{"hp":0},"旗标":false,"备注":"","深度":{"嵌套":{"空串":""}}}}',
|
|
96
|
+
String(c.init && c.init.body))
|
|
97
|
+
check('成功 ⇒ kind=ok 且带回写回处数', r.kind === 'ok' && r.count === 4, JSON.stringify(r))
|
|
98
|
+
|
|
99
|
+
const f2 = mockFetch(() => jsonRes(200, { ok: true }))
|
|
100
|
+
const r2 = await api.varSectionSave({ sessionId: '', edits, fetch: f2 })
|
|
101
|
+
check('无会话 ⇒ kind=no-session 且**不发请求**', r2.kind === 'no-session' && f2.calls.length === 0, JSON.stringify(r2))
|
|
102
|
+
const f3 = mockFetch(() => jsonRes(200, { ok: true }))
|
|
103
|
+
const r3 = await api.varSectionSave({ sessionId: SID, edits: {}, fetch: f3 })
|
|
104
|
+
check('没有修改 ⇒ kind=nothing 且不发请求', r3.kind === 'nothing' && f3.calls.length === 0, JSON.stringify(r3))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log('\n[C] 失败保留 edits + 状态行说人话;成功清空;刷新丢弃')
|
|
108
|
+
{
|
|
109
|
+
const edits = {}
|
|
110
|
+
edits['a' + SEP + 'b'] = { value: 1, type: 'number' }
|
|
111
|
+
const f = mockFetch(() => jsonRes(500, { ok: false, error: 'session-not-live:会话当前不活跃' }))
|
|
112
|
+
const r = await api.varSectionSave({ sessionId: SID, edits, fetch: f })
|
|
113
|
+
check('失败 ⇒ kind=error 且 keepEdits=true', r.kind === 'error' && r.keepEdits === true, JSON.stringify(r))
|
|
114
|
+
check('状态行带服务端原因 + "本地修改已保留"(说人话)',
|
|
115
|
+
r.message.includes('session-not-live') && r.message.includes('保留'), String(r.message))
|
|
116
|
+
check('失败后 edits 表**原封不动**(同一引用,可重试)',
|
|
117
|
+
api.varSectionApplySaveResult(edits, r) === edits)
|
|
118
|
+
check('成功 ⇒ edits 清空', Object.keys(api.varSectionApplySaveResult(edits, { kind: 'ok' })).length === 0)
|
|
119
|
+
check('网络层直接抛(引擎没起)⇒ 同样 keepEdits=true 不向上抛',
|
|
120
|
+
(async () => {
|
|
121
|
+
const bad = mockFetch(() => { throw new TypeError('Failed to fetch') })
|
|
122
|
+
const rr = await api.varSectionSave({ sessionId: SID, edits, fetch: bad })
|
|
123
|
+
return rr.kind === 'error' && rr.keepEdits === true && rr.message.includes('Failed to fetch')
|
|
124
|
+
})())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('\n[D] 已覆盖标记三态(对照 initvarData)')
|
|
128
|
+
{
|
|
129
|
+
const init = { a: 1, s: 'x', deep: { k: null } }
|
|
130
|
+
check('值不同 ⇒ 标', api.varSectionIsOverridden(init, ['a'], 2) === true)
|
|
131
|
+
check('值相同 ⇒ 不标', api.varSectionIsOverridden(init, ['a'], 1) === false)
|
|
132
|
+
check('initvarData 无此键 ⇒ 不标', api.varSectionIsOverridden(init, ['missing'], 999) === false)
|
|
133
|
+
check('深路径值相同(null==null)⇒ 不标', api.varSectionIsOverridden(init, ['deep', 'k'], null) === false)
|
|
134
|
+
check('没有 initvarData(null/非对象)⇒ 不标',
|
|
135
|
+
api.varSectionIsOverridden(null, ['a'], 1) === false && api.varSectionIsOverridden(undefined, ['a'], 1) === false)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('\n[E] null / 0 / false / "" 的叶子:渲染正常 + 保存原样提交(不被 merge 吃掉)')
|
|
139
|
+
{
|
|
140
|
+
check('null 叶子:不可编辑、显示文本 "null"',
|
|
141
|
+
(() => { const d = api.varSectionLeafDisplay(null); return d.editable === false && d.text === 'null' })())
|
|
142
|
+
check('0 叶子:可编辑、显示 "0"',
|
|
143
|
+
(() => { const d = api.varSectionLeafDisplay(0); return d.editable === true && d.text === '0' && d.type === 'number' })())
|
|
144
|
+
check('"" 叶子:可编辑、显示空串',
|
|
145
|
+
(() => { const d = api.varSectionLeafDisplay(''); return d.editable === true && d.text === '' })())
|
|
146
|
+
check('false 叶子:可编辑、布尔型',
|
|
147
|
+
(() => { const d = api.varSectionLeafDisplay(false); return d.editable === true && d.type === 'boolean' })())
|
|
148
|
+
check('编辑 0:数字输入 "0" 还原成数字 0(严格 ===,不是 "0")',
|
|
149
|
+
api.varSectionNormalizeEdit('number', '0') === 0)
|
|
150
|
+
check('编辑数字填了非数字 ⇒ 保留字符串(不强转)',
|
|
151
|
+
api.varSectionNormalizeEdit('number', 'abc') === 'abc')
|
|
152
|
+
check('布尔编辑还原成布尔 false', api.varSectionNormalizeEdit('boolean', false) === false)
|
|
153
|
+
check('字符串编辑原样', api.varSectionNormalizeEdit('string', '') === '')
|
|
154
|
+
|
|
155
|
+
const edits = {}
|
|
156
|
+
edits['hp' + SEP + 'cur'] = { value: 0, type: 'number' }
|
|
157
|
+
edits['f'] = { value: false, type: 'boolean' }
|
|
158
|
+
edits['m'] = { value: '', type: 'string' }
|
|
159
|
+
edits['n'] = { value: null, type: 'null' }
|
|
160
|
+
const payload = api.varSectionEditsToPayload(edits)
|
|
161
|
+
check('提交 payload:0/false/""/null 全部原样(JSON 逐字对照)',
|
|
162
|
+
JSON.stringify(payload) === '{"hp":{"cur":0},"f":false,"m":"","n":null}',
|
|
163
|
+
JSON.stringify(payload))
|
|
164
|
+
check('数组叶子:显示 "[数组 N 项]"(不可编辑)',
|
|
165
|
+
(() => { const d = api.varSectionLeafDisplay([1, 2, 3]); return d.editable === false && d.text === '[数组 3 项]' })())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\n[F] 大树懒展开:默认只展开顶层对象分支,深层点开才渲染')
|
|
169
|
+
{
|
|
170
|
+
const tree = { 顶层A: { x: 1, 子: { 深: 1 } }, 顶层B: 2, 顶层C: {} }
|
|
171
|
+
const de = api.varSectionDefaultExpanded(tree)
|
|
172
|
+
check('默认展开集合 = 顶层对象分支(空的顶层C 不算)',
|
|
173
|
+
JSON.stringify(Object.keys(de).sort()) === JSON.stringify(['顶层A']), JSON.stringify(Object.keys(de)))
|
|
174
|
+
check('深层路径不在默认展开集合里(懒展开,不预开)',
|
|
175
|
+
Object.keys(de).every(k => k in tree))
|
|
176
|
+
check('空树 / 非树 ⇒ 空集合',
|
|
177
|
+
Object.keys(api.varSectionDefaultExpanded(null)).length === 0 &&
|
|
178
|
+
Object.keys(api.varSectionDefaultExpanded({})).length === 0)
|
|
179
|
+
check('UI:对象子层只在 isOpen 时渲染(if (isOpen) 才 push 子层)',
|
|
180
|
+
/if \(isOpen\)/.test(uiSlice))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log('\n[G] 搜索过滤:命中 / 无命中 / 值命中')
|
|
184
|
+
{
|
|
185
|
+
const tree = { 战斗: { hp: 100, 魔力: 20 }, 状态: { 中毒: true } }
|
|
186
|
+
const h1 = api.varSectionSearchHits(tree, 'hp')
|
|
187
|
+
check('路径名命中 1 条叶子(战斗.hp)',
|
|
188
|
+
h1.length === 1 && h1[0].path.join('.') === '战斗.hp' && h1[0].value === 100, JSON.stringify(h1))
|
|
189
|
+
const h2 = api.varSectionSearchHits(tree, '中毒')
|
|
190
|
+
check('深层路径命中', h2.length === 1 && h2[0].path.join('.') === '状态.中毒', JSON.stringify(h2))
|
|
191
|
+
const h3 = api.varSectionSearchHits(tree, '100')
|
|
192
|
+
check('值也能命中', h3.length === 1 && h3[0].value === 100, JSON.stringify(h3))
|
|
193
|
+
check('无命中 ⇒ 空数组(UI 显示"没有匹配的变量")',
|
|
194
|
+
api.varSectionSearchHits(tree, '不存在zzz').length === 0)
|
|
195
|
+
check('空查询 ⇒ 空数组(走树视图,不走拍平视图)',
|
|
196
|
+
api.varSectionSearchHits(tree, '').length === 0)
|
|
197
|
+
check('limit 生效(大树防炸)',
|
|
198
|
+
api.varSectionSearchHits({ a: { b: { c: { d: { e: { f: { g: { h: 1 } } } } } } } }, '1', 3).length <= 3)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('\n[H] 快照入口(按楼回看):探测失败 ⇒ 隐藏,不报错不假装有数据')
|
|
202
|
+
{
|
|
203
|
+
const h404 = await api.varSectionListSnapshots({ sessionId: SID, fetch: mockFetch(() => jsonRes(404, { ok: false, error: 'no-snapshots' })) })
|
|
204
|
+
check('404 + !ok ⇒ kind=hidden', h404.kind === 'hidden', JSON.stringify(h404))
|
|
205
|
+
const hnet = await api.varSectionListSnapshots({ sessionId: SID, fetch: mockFetch(() => { throw new TypeError('Failed to fetch') }) })
|
|
206
|
+
check('网络错 ⇒ kind=hidden(静默,不抛)', hnet.kind === 'hidden')
|
|
207
|
+
const hempty = await api.varSectionListSnapshots({ sessionId: SID, fetch: mockFetch(() => jsonRes(200, { ok: true, snapshots: [] })) })
|
|
208
|
+
check('快照为空 ⇒ kind=hidden(保持隐藏)', hempty.kind === 'hidden')
|
|
209
|
+
const ok = await api.varSectionListSnapshots({
|
|
210
|
+
sessionId: SID,
|
|
211
|
+
fetch: mockFetch(() => jsonRes(200, { ok: true, snapshots: [{ key: 'msg-abc12345678', at: 1690000000000, data: {} }] }))
|
|
212
|
+
})
|
|
213
|
+
check('有快照 ⇒ kind=ok 且原样带回列表', ok.kind === 'ok' && ok.snapshots.length === 1, JSON.stringify(ok))
|
|
214
|
+
check('无会话 ⇒ hidden(不发探测)',
|
|
215
|
+
(await api.varSectionListSnapshots({ sessionId: '', fetch: mockFetch(() => jsonRes(200, { ok: true, snapshots: [{}] })) })).kind === 'hidden')
|
|
216
|
+
|
|
217
|
+
const e404 = await api.varSectionEnterSnapshot({ sessionId: SID, messageKey: 'msg-x', fetch: mockFetch(() => jsonRes(404, { ok: false })) })
|
|
218
|
+
check('进楼失败(404)⇒ kind=error 且 message 可读', e404.kind === 'error' && e404.message.includes('快照不存在'), JSON.stringify(e404))
|
|
219
|
+
const eok = await api.varSectionEnterSnapshot({
|
|
220
|
+
sessionId: SID, messageKey: 'msg-k',
|
|
221
|
+
fetch: mockFetch(() => jsonRes(200, { ok: true, state: { data: { 回看: 1 } } }))
|
|
222
|
+
})
|
|
223
|
+
check('进楼成功 ⇒ kind=ok 且带回该楼树', eok.kind === 'ok' && eok.tree && eok.tree.回看 === 1, JSON.stringify(eok))
|
|
224
|
+
check('快照标签:键尾 8 位 + 过大未存标记',
|
|
225
|
+
api.varSectionSnapLabel({ key: 'msg-abc12345678', at: 1690000000000, data: null }).includes('…12345678') &&
|
|
226
|
+
api.varSectionSnapLabel({ key: 'msg-abc12345678', at: 1690000000000, data: null }).includes('(过大未存)'))
|
|
227
|
+
check('UI:快照行只在 snapList 非空时渲染(探测失败整行不出现)',
|
|
228
|
+
uiSlice.includes("snapList && snapList.length > 0 && jsx.jsxs('div', { 'data-dsh-muv-var-snap-row'"))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log('\n[I] XSS:变量值是外部数据,绝不允许变成真实元素')
|
|
232
|
+
{
|
|
233
|
+
const evil = '<img src=x onerror=alert(1)>'
|
|
234
|
+
const d = api.varSectionLeafDisplay(evil)
|
|
235
|
+
check('恶意值原样作为**文本**交给渲染层(可编辑文本框的 value)',
|
|
236
|
+
d.editable === true && d.text === evil, JSON.stringify(d))
|
|
237
|
+
check('UI 切片无 innerHTML / insertAdjacentHTML / dangerouslySetInnerHTML',
|
|
238
|
+
!uiSlice.includes('innerHTML') && !uiSlice.includes('insertAdjacentHTML') && !uiSlice.includes('dangerouslySetInnerHTML'))
|
|
239
|
+
check('提取段同样无任何 DOM 写入手法',
|
|
240
|
+
!/innerHTML|outerHTML|insertAdjacentHTML|document\./.test(extracted))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log('\n[J] 接收方集成约束(防双滚动 / 默认折叠 / 复用既有链路 / 同一批监听)')
|
|
244
|
+
{
|
|
245
|
+
check('新区不自带滚动容器(无 maxHeight / overflow / max-height 字样 ⇒ 嵌外层滚动防双滚)',
|
|
246
|
+
!uiSlice.includes('maxHeight') && !uiSlice.includes('max-height') && !uiSlice.includes('overflow'),
|
|
247
|
+
(uiSlice.match(/maxHeight|max-height|overflow/g) || []).join(','))
|
|
248
|
+
check('默认折叠(open 初始 false)',
|
|
249
|
+
uiSlice.includes("const [open, setOpen] = React.useState(false)"))
|
|
250
|
+
check('sessionId 复用 muv-table 既有链路 currentSessionId()(面板里同一取值点)',
|
|
251
|
+
src.includes('sessionId: currentSessionId()'))
|
|
252
|
+
check('initvarData 复用面板 state(不二次请求 tavern-card)',
|
|
253
|
+
src.includes('initvarData: state.initvarData') && !uiSlice.includes('/api/muv-table/tavern-card'))
|
|
254
|
+
check('会话/预设切换挂同一批监听(varTick 在 check() 消费 forceReload 处 bump,新区不另挂监听)',
|
|
255
|
+
src.includes('setVarTick(t => t + 1)') && uiSlice.includes('reloadToken') &&
|
|
256
|
+
!uiSlice.includes("addEventListener('tavern-preset-changed'") &&
|
|
257
|
+
!uiSlice.includes("addEventListener('tavern-session-changed'"))
|
|
258
|
+
check('保存按钮走纯函数(请求体形状由 varSectionSave 保证)',
|
|
259
|
+
uiSlice.includes('varSectionSave({ sessionId, edits'))
|
|
260
|
+
check('功能标记齐全(search/refresh/save/export/snap-row/snap-select/back-latest/status/tree)',
|
|
261
|
+
['data-dsh-muv-var-search', 'data-dsh-muv-var-refresh', 'data-dsh-muv-var-save',
|
|
262
|
+
'data-dsh-muv-var-export', 'data-dsh-muv-var-snap-row', 'data-dsh-muv-var-snap-select',
|
|
263
|
+
'data-dsh-muv-var-back-latest', 'data-dsh-muv-var-status', 'data-dsh-muv-var-tree']
|
|
264
|
+
.every(k => uiSlice.includes("'" + k + "'")))
|
|
265
|
+
check('新区嵌在 MUV 既有滚动容器里(Content 容器把 RuntimeVarsSection 与变量树放进同一层)',
|
|
266
|
+
src.includes("jsx.jsx(RuntimeVarsSection, {"))
|
|
267
|
+
check('配色走面板既有 CSS 变量(C token / var(--dsw-alias-*)),无硬编码色值',
|
|
268
|
+
!/#c5a065|rgba\(39,174,96|#e74c3c'|#3498db'/.test(uiSlice.replace(/var\(--dsw-alias-[a-z-]+,#?[0-9a-fA-F)]*\)/g, '')))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`\n=== 结果: ${pass} 通过, ${fail} 失败 ===`)
|
|
272
|
+
process.exit(fail ? 1 : 0)
|