/** * v1.1.0 自动化测试脚本 * 验证 M001-M005 的核心逻辑 */ const fs = require('fs'); const path = require('path'); const htmlPath = path.join(__dirname, '..', 'index.html'); const html = fs.readFileSync(htmlPath, 'utf-8'); const results = { M001: [], M002: [], M003: [], M004: [], M005: [], }; function check(module, tcId, desc, expected, actual, pass) { results[module].push({ tcId, desc, expected, actual, pass }); const icon = pass ? '✅' : '❌'; console.log(`${icon} [${module}] ${tcId}: ${desc}`); if (!pass) { console.log(` 预期: ${expected}`); console.log(` 实际: ${actual}`); } } // ============================================================ // M001: 名单编辑功能(删除按钮) // ============================================================ console.log('\n=== M001: 名单编辑功能 ==='); // TC001: 删除按钮存在 check('M001', 'TC001', 'name-tag 有 delete-btn 按钮', '存在 .delete-btn 元素', html.includes("delBtn.className = 'delete-btn'") ? '存在' : '不存在', html.includes("delBtn.className = 'delete-btn'")); // TC002: 删除按钮点击事件 check('M001', 'TC002', '删除按钮有点击事件处理', '存在 delete-btn 的 click 事件委托', html.includes("e.target.closest('.delete-btn')") ? '存在' : '不存在', html.includes("e.target.closest('.delete-btn')")); // TC003: removeByName 方法存在 check('M001', 'TC003', 'NameManager.removeByName 方法存在', '存在 removeByName 方法', html.includes('removeByName: function(name)') ? '存在' : '不存在', html.includes('removeByName: function(name)')); // TC004: 删除后同步清理必中名单 check('M001', 'TC004', '删除后同步清理必中名单', 'removeByName 中清理 mustWinList', html.includes("state.backdoor.mustWinList.indexOf(name)") ? '存在' : '不存在', html.includes("state.backdoor.mustWinList.indexOf(name)")); // TC005: 删除后同步清理排除名单 check('M001', 'TC005', '删除后同步清理排除名单', 'removeByName 中清理 excludeList', html.includes("state.backdoor.excludeList.indexOf(name)") ? '存在' : '不存在', html.includes("state.backdoor.excludeList.indexOf(name)")); // TC006: 删除后同步清理概率配置 check('M001', 'TC006', '删除后同步清理概率配置', 'removeByName 中清理 probabilities', html.includes("delete state.backdoor.probabilities[name]") ? '存在' : '不存在', html.includes("delete state.backdoor.probabilities[name]")); // TC007: 抽奖中禁用删除按钮 check('M001', 'TC007', '抽奖中(isDrawing=true)删除按钮被禁用', 'isDrawing 时 delBtn.disabled = true', html.includes("delBtn.disabled = true") ? '存在' : '不存在', html.includes("delBtn.disabled = true")); // TC008: 抽奖中 name-list 添加 disabled class check('M001', 'TC008', '抽奖中 name-list 添加 disabled class', 'isDrawing 时 container.classList.add("disabled")', html.includes("container.classList.add('disabled')") ? '存在' : '不存在', html.includes("container.classList.add('disabled')")); // TC009: CSS 中 disabled 隐藏删除按钮 check('M001', 'TC009', 'CSS .disabled 隐藏删除按钮', '存在 .name-list.disabled .delete-btn { display: none }', html.includes('.name-list.disabled .name-tag .delete-btn') ? '存在' : '不存在', html.includes('.name-list.disabled .name-tag .delete-btn')); // TC010: 删除最后一个人的边界处理 check('M001', 'TC010', '删除逻辑 splice 正确处理边界', 'splice 在 idx === -1 时跳过', html.includes('if (idx === -1) return') ? '存在' : '不存在', html.includes('if (idx === -1) return')); // ============================================================ // M002: 概率后门(每人概率设置) // ============================================================ console.log('\n=== M002: 概率后门 ==='); // TC011: 后门面板有概率设置区域 check('M002', 'TC011', '后门面板新增概率设置区域', 'HTML 中有概率设置 section', html.includes('🎲 概率设置') ? '存在' : '不存在', html.includes('🎲 概率设置')); // TC012: 概率列表容器 check('M002', 'TC012', '有 probability-list 容器', '存在 #probabilityList 元素', html.includes('id="probabilityList"') ? '存在' : '不存在', html.includes('id="probabilityList"')); // TC013: 状态中有 probabilities 字段 check('M002', 'TC013', '状态结构包含 probabilities', 'state.backdoor.probabilities 存在', html.includes('probabilities: {},') ? '存在' : '不存在', html.includes('probabilities: {},')); // TC014: person 对象有 weight 字段 check('M002', 'TC014', 'person 对象有 weight 字段', 'generatePerson 返回 weight: 1', html.includes('weight: 1') ? '存在' : '不存在', html.includes('weight: 1')); // TC015: 加权随机算法 - 过滤权重为0 check('M002', 'TC015', '加权随机算法过滤权重为0的人', 'pickOne 中过滤 weight > 0', html.includes('return w > 0') ? '存在' : '不存在', html.includes('return w > 0')); // TC016: 加权随机算法 - 计算总权重 check('M002', 'TC016', '加权随机算法计算总权重', '存在 totalWeight 累加逻辑', html.includes('totalWeight += self.getWeight') ? '存在' : '不存在', html.includes('totalWeight += self.getWeight')); // TC017: 加权随机算法 - 累加选择 check('M002', 'TC017', '加权随机算法使用累加方式选择', '存在 cumulative 累加比较', html.includes('cumulative += self.getWeight') ? '存在' : '不存在', html.includes('cumulative += self.getWeight')); // TC018: 必中名单优先级最高 check('M002', 'TC018', '必中名单优先级高于概率权重', 'pickOne 中必中名单判断在加权随机之前', html.includes('若必中名单非空,取交集(最高优先级,等概率)') ? '存在' : '不存在', html.includes('若必中名单非空,取交集(最高优先级,等概率)')); // TC019: 排除名单在加权随机之前过滤 check('M002', 'TC019', '排除名单在加权随机之前过滤', 'pickOne 中先 applyExclude', html.includes('candidates = this.applyExclude(candidates)') ? '存在' : '不存在', html.includes('candidates = this.applyExclude(candidates)')); // TC020: 快速设置按钮 check('M002', 'TC020', '快速设置按钮(0/1/5/10/50/100)', 'quickValues 包含 [0,1,5,10,50,100]', html.includes('[0, 1, 5, 10, 50, 100]') ? '存在' : '不存在', html.includes('[0, 1, 5, 10, 50, 100]')); // TC021: 重置所有权重功能 check('M002', 'TC021', '重置所有概率功能', '存在 resetAllWeights 方法', html.includes('resetAllWeights: function()') ? '存在' : '不存在', html.includes('resetAllWeights: function()')); // TC022: 重置按钮绑定 check('M002', 'TC022', '重置按钮事件绑定', 'btnResetAllWeights 绑定 click 事件', html.includes('btnResetAllWeights') && html.includes('resetAllWeights') ? '存在' : '不存在', html.includes('btnResetAllWeights') && html.includes('resetAllWeights')); // TC023: 权重输入框范围限制 check('M002', 'TC023', '权重输入框有 min/max 限制', 'weight-input 有 min=0 max=100', html.includes("weightInput.min = 0") && html.includes("weightInput.max = 100") ? '存在' : '不存在', html.includes("weightInput.min = 0") && html.includes("weightInput.max = 100")); // TC024: 权重值保存到 state check('M002', 'TC024', 'saveProbabilities 保存权重到 state', 'state.backdoor.probabilities 被赋值', html.includes('state.backdoor.probabilities = newProbs') ? '存在' : '不存在', html.includes('state.backdoor.probabilities = newProbs')); // TC025: 权重同步到 pool check('M002', 'TC025', '权重同步更新到 pool 中 person.weight', 'person.weight 被更新', html.includes('person.weight = newProbs[person.name]') ? '存在' : '不存在', html.includes('person.weight = newProbs[person.name]')); // ============================================================ // M003: 滚动动画改进(左→右老虎机效果) // ============================================================ console.log('\n=== M003: 滚动动画改进 ==='); // TC026: 水平滚动容器 check('M003', 'TC026', '有 scroll-container 水平滚动容器', '存在 .scroll-container 和 overflow:hidden', html.includes('.scroll-container') && html.includes('overflow: hidden') ? '存在' : '不存在', html.includes('.scroll-container') && html.includes('overflow: hidden')); // TC027: 滚动轨道 check('M003', 'TC027', '有 scroll-track 滚动轨道', '存在 .scroll-track 和 display:flex', html.includes('.scroll-track') && html.includes('display: flex') ? '存在' : '不存在', html.includes('.scroll-track') && html.includes('display: flex')); // TC028: 使用 translateX 实现水平滚动 check('M003', 'TC028', '使用 translateX 实现水平滚动', 'scrollTrackEl.style.transform = translateX', html.includes("scrollTrackEl.style.transform = 'translateX(") ? '存在' : '不存在', html.includes("scrollTrackEl.style.transform = 'translateX(")); // TC029: 总时长约 3.5 秒 check('M003', 'TC029', '动画总时长约 3.5 秒', 'duration = 3500', html.includes('duration = 3500') ? '存在' : '不存在', html.includes('duration = 3500')); // TC030: 使用 easeOutCubic 缓动 check('M003', 'TC030', '使用 easeOutCubic 缓动函数', '存在 easeOutCubic 函数定义', html.includes('easeOutCubic(t)') ? '存在' : '不存在', html.includes('easeOutCubic(t)')); // TC031: 使用 requestAnimationFrame check('M003', 'TC031', '使用 requestAnimationFrame 实现动画', '存在 requestAnimationFrame 调用', html.includes('requestAnimationFrame(tick)') ? '存在' : '不存在', html.includes('requestAnimationFrame(tick)')); // TC032: 最终停在正确的中奖人 check('M003', 'TC032', '动画结束时停在 winner', 'callback(winnerPerson) 传入正确中奖人', html.includes('callback(winnerPerson)') ? '存在' : '不存在', html.includes('callback(winnerPerson)')); // TC033: 滚动容器初始隐藏 check('M003', 'TC033', '滚动容器初始隐藏', 'scroll-container 有 .hidden class', html.includes('id="scrollContainer" class="scroll-container hidden"') ? '存在' : '不存在', html.includes('id="scrollContainer" class="scroll-container hidden"')); // TC034: 滚动结束后隐藏容器 check('M003', 'TC034', '滚动结束后隐藏 scroll-container', 'progress >= 1 时 scrollContainerEl.classList.add("hidden")', html.includes("scrollContainerEl.classList.add('hidden')") ? '存在' : '不存在', html.includes("scrollContainerEl.classList.add('hidden')")); // ============================================================ // M004: 弹窗显示中奖结果 // ============================================================ console.log('\n=== M004: 弹窗显示中奖结果 ==='); // TC035: 弹窗 HTML 结构存在 check('M004', 'TC035', '有 winner-overlay 弹窗结构', '存在 #winnerOverlay 元素', html.includes('id="winnerOverlay"') ? '存在' : '不存在', html.includes('id="winnerOverlay"')); // TC036: 弹窗包含大尺寸头像 check('M004', 'TC036', '弹窗包含大尺寸头像', '存在 #winnerAvatarLarge 元素', html.includes('id="winnerAvatarLarge"') ? '存在' : '不存在', html.includes('id="winnerAvatarLarge"')); // TC037: 弹窗包含"恭喜"字样 check('M004', 'TC037', '弹窗包含"恭喜中奖"字样', '存在 .winner-congrats 含"恭喜中奖"', html.includes('恭喜中奖') ? '存在' : '不存在', html.includes('恭喜中奖')); // TC038: 弹窗 z-index 高于烟花 Canvas check('M004', 'TC038', '弹窗 z-index (10001) > 烟花 Canvas (9999)', 'winner-overlay z-index: 10001, fireworksCanvas z-index: 9999', 'z-index: 10001 vs z-index: 9999', html.includes('z-index: 10001') && html.includes('z-index: 9999')); // TC039: 关闭按钮存在 check('M004', 'TC039', '弹窗有关闭按钮', '存在 #winnerClose 按钮', html.includes('id="winnerClose"') ? '存在' : '不存在', html.includes('id="winnerClose"')); // TC040: 点击遮罩关闭弹窗 check('M004', 'TC040', '点击遮罩层可关闭弹窗', 'winnerOverlay click 事件判断 e.target', html.includes('e.target === DOM.winnerOverlay') ? '存在' : '不存在', html.includes('e.target === DOM.winnerOverlay')); // TC041: 关闭按钮事件绑定 check('M004', 'TC041', '关闭按钮绑定 click 事件', 'winnerClose click 事件绑定', html.includes('DOM.winnerClose.addEventListener') ? '存在' : '不存在', html.includes('DOM.winnerClose.addEventListener')); // TC042: 弹窗动画(winnerPopIn) check('M004', 'TC042', '弹窗有弹出缩放+淡入动画', '存在 @keyframes winnerPopIn', html.includes('@keyframes winnerPopIn') ? '存在' : '不存在', html.includes('@keyframes winnerPopIn')); // TC043: 弹窗动画包含 scale 和 opacity check('M004', 'TC043', '弹窗动画包含 scale 和 opacity', 'winnerPopIn 含 scale(0.5) 和 opacity: 0', html.includes("scale(0.5)") && html.includes("opacity: 0") ? '存在' : '不存在', html.includes("scale(0.5)") && html.includes("opacity: 0")); // TC044: showWinnerPopup 方法存在 check('M004', 'TC044', 'AnimationEngine.showWinnerPopup 方法存在', '存在 showWinnerPopup 方法', html.includes('showWinnerPopup: function(winnerPerson)') ? '存在' : '不存在', html.includes('showWinnerPopup: function(winnerPerson)')); // TC045: 抽奖结束后调用 showWinnerPopup check('M004', 'TC045', '抽奖动画结束后调用 showWinnerPopup', 'startLottery callback 中调用 showWinnerPopup', html.includes('AnimationEngine.showWinnerPopup(finalWinner)') ? '存在' : '不存在', html.includes('AnimationEngine.showWinnerPopup(finalWinner)')); // ============================================================ // M005: 烟花效果改进 // ============================================================ console.log('\n=== M005: 烟花效果改进 ==='); // TC046: 烟花无上升阶段 check('M005', 'TC046', '烟花无上升阶段(直接爆炸)', 'Firework.state 初始为 "exploding" 而非 "rising"', html.includes("this.state = 'exploding'") ? '存在' : '不存在', html.includes("this.state = 'exploding'")); // TC047: 无 rising 状态 check('M005', 'TC047', '代码中无 rising 状态', '无 "rising" 状态引用', !html.includes("'rising'") ? '不存在' : '存在(异常)', !html.includes("'rising'")); // TC048: 爆炸点围绕弹窗位置 check('M005', 'TC048', '爆炸点围绕弹窗位置分布', '获取 popupRect 并围绕其生成爆炸点', html.includes('popupEl.getBoundingClientRect()') && html.includes('spreadRadius') ? '存在' : '不存在', html.includes('popupEl.getBoundingClientRect()') && html.includes('spreadRadius')); // TC049: 烟花持续 5 秒 check('M005', 'TC049', '烟花持续时间 ≥ 3 秒(实际 5 秒)', 'endTime = performance.now() + 5000', html.includes('endTime = performance.now() + 5000') ? '存在' : '不存在', html.includes('endTime = performance.now() + 5000')); // TC050: 粒子数量充足 check('M005', 'TC050', '每朵烟花粒子数量充足(100-140个)', '100 + Math.random() * 40 个粒子', html.includes('100 + Math.floor(Math.random() * 40)') ? '存在' : '不存在', html.includes('100 + Math.floor(Math.random() * 40)')); // TC051: 烟花结束后 Canvas 清除 check('M005', 'TC051', '烟花结束后 Canvas 正确清除', 'now > endTime 时 ctx.clearRect', html.includes('ctx.clearRect(0, 0, canvas.width, canvas.height)') ? '存在' : '不存在', html.includes('ctx.clearRect(0, 0, canvas.width, canvas.height)')); // TC052: 烟花在弹窗周围随机位置生成 check('M005', 'TC052', '烟花在弹窗周围随机角度和半径生成', '使用随机 angle 和 radius 生成爆炸点', html.includes('Math.random() * Math.PI * 2') && html.includes('spreadRadius * (0.5') ? '存在' : '不存在', html.includes('Math.random() * Math.PI * 2') && html.includes('spreadRadius * (0.5')); // TC053: 烟花调用位置正确 check('M005', 'TC053', '抽奖结束后调用 fireFireworks', 'startLottery callback 中调用 fireFireworks', html.includes('AnimationEngine.fireFireworks()') ? '存在' : '不存在', html.includes('AnimationEngine.fireFireworks()')); // TC054: 烟花颜色丰富 check('M005', 'TC054', '烟花有多种颜色调色板', 'randomColorPalette 返回多种颜色组合', html.includes('randomColorPalette') ? '存在' : '不存在', html.includes('randomColorPalette')); // TC055: 烟花粒子有重力效果 check('M005', 'TC055', '烟花粒子有重力和摩擦效果', 'Particle 有 gravity 和 friction 属性', html.includes('this.gravity') && html.includes('this.friction') ? '存在' : '不存在', html.includes('this.gravity') && html.includes('this.friction')); // ============================================================ // 汇总 // ============================================================ console.log('\n=== 汇总 ==='); let totalPass = 0, totalFail = 0; for (const mod of ['M001', 'M002', 'M003', 'M004', 'M005']) { const pass = results[mod].filter(r => r.pass).length; const fail = results[mod].filter(r => !r.pass).length; totalPass += pass; totalFail += fail; console.log(`${mod}: ${pass} 通过, ${fail} 失败, 共 ${results[mod].length} 项`); } console.log(`总计: ${totalPass} 通过, ${totalFail} 失败, 共 ${totalPass + totalFail} 项`); // 输出 JSON 结果供报告生成 console.log('\n--- JSON OUTPUT ---'); console.log(JSON.stringify(results, null, 2));