#!/usr/bin/env node /** * test_render.js —— 通知正文渲染的分组与「哪个签到完成了」可见性 * * 2026-11-07 需求:执行日志/通知里必须一眼看出「是什么签到完成了」。 * 这个测试冻结 renderAccount 的分组契约: * 1. 三套体系各自成组,组标题带 emoji * 2. 组内每条都带「子项名 + detail」,不能只剩 detail * 3. 整组全 skip → 收成一行 * 4. 金币组明确标注「不计成长值」,不与成长值混算 * 5. 分享卡链接单独一行 * * 用法: node qqmusic/test_render.js */ 'use strict' const { renderAccount } = require('./scrip_qqmusic_checkin.js') let pass = 0, fail = 0 const ok = (n, c, e = '') => { if (c) { pass++; console.log(` ✓ ${n}`) } else { fail++; console.log(` ✗ ${n}${e ? ' — ' + e : ''}`) } } const has = (out, s, label) => ok(label || `含「${s}」`, out.includes(s), out) const not = (out, s, label) => ok(label || `不含「${s}」`, !out.includes(s), out) /* ── 1. 三套体系都有收获 ── */ console.log('\n── 1. 三套体系都有收获 ──') const r1 = renderAccount('1006764347', { total: 45, verdict: 'done', cardUrl: '', steps: [ { name: '任务中心·签到', status: 'done', detail: '+15' }, { name: '任务中心·挂件', status: 'done', detail: '已佩戴' }, { name: '任务中心·装扮', status: 'skip', detail: '' }, { name: '福利站·签到', status: 'done', detail: '15成长值(本月 8 天)' }, { name: '福利站·礼券', status: 'done', detail: '+10 心愿值' }, { name: '福利站·月礼', status: 'skip', detail: '' }, { name: '金币·签到', status: 'done', detail: '已签 + 30 金币' }, { name: '金币·抽奖签到', status: 'done', detail: '已签 + 已领' }, ], }) console.log(r1.split('\n').map(l => ' │ ' + l).join('\n')) has(r1, '📈 任务中心', '有任务中心组标题') has(r1, '🎁 会员福利站', '有福利站组标题') has(r1, '💰 金币中心', '有金币中心组标题') has(r1, '不计成长值', '金币组标注「不计成长值」') has(r1, '✓ 签到 +15', '任务中心签到带子项名 + detail') has(r1, '✓ 礼券 +10 心愿值', '福利站礼券带子项名 + detail') has(r1, '✓ 签到 已签 + 30 金币', '金币签到带子项名 + detail') has(r1, '✓ 抽奖签到', '金币抽奖签到也在') has(r1, '无需处理 1 项:装扮', '任务中心 skip 收成一行') has(r1, '无需处理 1 项:月礼', '福利站 skip 收成一行') // 关键:不能出现「只有 detail 没有名字」的行 const detailOnly = r1.split('\n').filter(l => /^\s+[✓△✗·] (已签|\+|已领|已佩戴)/.test(l)) ok('没有「只有 detail 没有子项名」的行', detailOnly.length === 0, JSON.stringify(detailOnly)) /* ── 2. 整组全 skip → 收成一行 ── */ console.log('\n── 2. 金币全已签(整组 skip)──') const r2 = renderAccount('1006764347', { total: 30, verdict: 'done', cardUrl: '', steps: [ { name: '任务中心·签到', status: 'done', detail: '+15' }, { name: '金币·签到', status: 'skip', detail: '今日已签(连续第 7 天)' }, { name: '金币·抽奖签到', status: 'skip', detail: '今日已签' }, ], }) console.log(r2.split('\n').map(l => ' │ ' + l).join('\n')) has(r2, '💰 金币中心', '金币组还在') has(r2, '今日无需处理', '整组 skip 收成一行') has(r2, '签到、抽奖签到', '两个子项都列出来了') not(r2, '✓ 签到 今日已签', '没有多余的成功行') /* ── 3. 金币 ActID 过期 → warn 单独列 ── */ console.log('\n── 3. 金币 ActID 过期 ──') const r3 = renderAccount('1006764347', { total: 30, verdict: 'warn', cardUrl: '', steps: [ { name: '任务中心·签到', status: 'done', detail: '+15' }, { name: '金币·签到', status: 'warn', detail: '查询失败 code -1/-1(可能 ActID 过期)' }, ], }) console.log(r3.split('\n').map(l => ' │ ' + l).join('\n')) has(r3, '🟡', 'verdict=warn 用黄脸') has(r3, '△ 签到 查询失败', 'warn 带子项名 + detail') has(r3, 'ActID', 'detail 里能看出是 ActID 问题') /* ── 4. 分享卡链接 ── */ console.log('\n── 4. 分享卡链接 ──') const r4 = renderAccount('1006764347', { total: 30, verdict: 'done', cardUrl: 'https://y.qq.com/m/vip/experience_card/dist/index.html?cardid=ABC', steps: [{ name: '分享卡', status: 'done', detail: '新建 1 张,还能被领 3 次' }], }) console.log(r4.split('\n').map(l => ' │ ' + l).join('\n')) has(r4, '🔗 领卡链接 https://y.qq.com', '链接单独一行且有前缀') has(r4, '🎫 VIP 体验券', '有分享卡组标题') /* ── 5. 空 steps 不崩 ── */ console.log('\n── 5. 空 steps ──') const r5 = renderAccount('1006764347', { total: 0, verdict: 'done', cardUrl: '', steps: [] }) ok('不抛错', typeof r5 === 'string') has(r5, '成长值 +0', '首行仍有成长值') /* ── 6. 未知前缀落到「其他」组 ── */ console.log('\n── 6. 未知前缀 → 其他组 ──') const r6 = renderAccount('1006764347', { total: 0, verdict: 'done', cardUrl: '', steps: [{ name: '某个新功能', status: 'done', detail: 'ok' }], }) has(r6, '📌 其他', '落到「其他」组') has(r6, '某个新功能', '名字保留完整(没有 · 可切)') console.log(`\nRESULT: ${fail === 0 ? 'PASS' : `FAIL(${fail})`} (${pass} passed, ${fail} failed)`) process.exit(fail === 0 ? 0 : 1)