import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import crypto from 'node:crypto'; const source=fs.readFileSync('G:/项目/NumberSwipe/modes/01-number-stroke/web/style.css','utf8'),css=fs.readFileSync(new URL('../style.css',import.meta.url),'utf8'); test('original design tokens and core shell CSS are preserved',()=>{assert.ok(css.startsWith(source));for(const token of ['--primary: #6558f5','--bg: #ffffff','--cell: #e6e7ec','--danger: #e5484d','--hint: #f5a524','--font: ui-rounded'])assert.ok(css.includes(token));for(const selector of ['.level-tile','.topbar','.remaining','.bottombar','.toast','#app'])assert.ok(css.includes(selector));}); test('all UI element references exist in built single-file HTML',()=>{const html=fs.readFileSync(new URL('../index.html',import.meta.url),'utf8'),ui=fs.readFileSync(new URL('../ui.mjs',import.meta.url),'utf8'),ids=new Set([...html.matchAll(/id="([^"]+)"/g)].map(m=>m[1]));for(const match of ui.matchAll(/\$\('([^']+)'\)/g))assert.ok(ids.has(match[1]),'缺少DOM元素 '+match[1]);}); test('gameplay update preserves campaign order',()=>{const hashes=JSON.parse(fs.readFileSync(new URL('../reports/ui-before.json',import.meta.url),'utf8'));for(const [name,expected]of Object.entries(hashes).filter(([name])=>name.startsWith("order-"))){const actual=crypto.createHash('sha256').update(fs.readFileSync(new URL('../'+name,import.meta.url))).digest('hex');assert.equal(actual,expected,name);}}); test('completion feedback and next action occupy separate normal-flow elements',()=>{const html=fs.readFileSync(new URL('../index.html',import.meta.url),'utf8');assert.match(html,/