Files
wordpress-theme-aksrt/tests/widget-system/card-pass-universal-redeem.test.cjs
T
kate522 1889c9f048 fix(frontend): 卡密兑换通知接入全局 notyf Toast 固定 id 流
- credits.js initCardPass 从 setTip 内联提示迁移为固定 id 三态 notyf 流
  (aksrt-card-pass-toast:load → success/danger 原位更新),
  成功后延迟 800ms 刷新以保证成功 Toast 可见,对齐其余资产操作。
- credits-wallet.php 删除 data-aksrt-card-tip 死元素;credits.css 删除
  .uc-card-pass__tip 死样式。
- 契约:toast-contract 将卡密兑换纳入固定 id 流清单并禁止 tip 回潮;
  card-pass-universal-redeem 新增 Toast 流断言(27/27 通过)。
- 验证:容器内真实抓取 /user/balance/ 确认弹窗正常渲染且无 tip 残留。
2026-09-17 19:28:47 +08:00

145 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 通用卡密兑换契约(/user/balance/ 余额卡片弹窗)。
*
* 背景:余额卡片右下角入口原为「卡密兑换余额」专用弹窗(强制 data-type="balance")。
* 现改为通用「卡密兑换」弹窗:标题、文案与提交不再绑定余额类型,
* 请求不带类型,由后端 ChargeCard::redeem_for_user() 按卡密自动识别
* (balance / vip / credits),各类型仍受自身开关与收款接口 card_pass 主开关约束。
* 前台只保留这一个兑换入口:页内联兑换块(积分/会员)与死文件 credits-vip.php
* 已删除,会员/积分卡密统一经通用弹窗兑换。
*
* 后台语义:余额充值 → 卡密兑换 → 「启用卡密兑换功能」
* (原「启用卡密充值余额功能」,设置键不变,仅语义/文案更新)。
*
* 卡密核销订单契约:核销不发生真实支付(现金在售卡时已确认),
* 订单金额恒为 0(create_redeem_order 源头 + UserOrders 视图层归零覆盖历史行,
* 不做数据迁移);订单名称按 note.meta 生成:
* 卡密兑换-余额充值X / 卡密兑换-积分充值X / 购入[会员档位名]X天(永久)。
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..', '..');
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
const wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php');
const creditsJs = read('assets', 'js', 'credits.js');
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
const userOrders = read('src', 'Payment', 'UserOrders.php');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
const usersSchemaPhp = read('src', 'Admin', 'Schema', 'SchemaUsers.php');
const usersSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'users', 'credits.js');
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
const creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
const creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css');
test('余额卡片弹窗为通用卡密兑换:标题与提交不再绑定余额类型', () => {
assert.match(wallet, /esc_html_e\('卡密兑换', 'aksrt'\)/);
assert.doesNotMatch(wallet, /卡密兑换余额/);
assert.doesNotMatch(wallet, /data-type="balance"/);
assert.doesNotMatch(wallet, /esc_html__\('兑换余额', 'aksrt'\)/);
// 弹窗根节点不带类型值 → 前端 type='' → 后端按卡密自动识别。
assert.match(wallet, /data-aksrt-card-pass-modal data-aksrt-card-pass=""/);
assert.match(wallet, /data-aksrt-card-redeem(?![^>]*data-type)/);
// 卡号输入恒展示且为选填(是否必填由后端按卡密类型判定)。
assert.match(wallet, /data-aksrt-card-no(?![^>]*required)/);
assert.match(wallet, /请输入卡号(选填)/);
});
test('入口展示受「启用卡密兑换功能」总开关控制', () => {
assert.match(wallet, /\$card_pass_redeem\['enabled'\]/);
assert.doesNotMatch(wallet, /\$card_pass_balance/);
// ChargeCard 缺失时不做 pass_charge 兜底(兑换 REST 依赖该类,兜底无意义)。
assert.doesNotMatch(wallet, /\$recharge\['pass_charge'\]/);
});
test('前台唯一兑换入口:页内联块与死文件已删除,JS 不再有 required 卡号校验', () => {
assert.equal(
fs.existsSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'credits-vip.php')),
false,
'credits-vip.php 死文件应已删除'
);
assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass|data-aksrt-card-redeem|card_pass_credits/);
assert.doesNotMatch(creditsJs, /hasAttribute\('required'\)/);
// 会员/积分卡密无独立开关与说明/单密码配置残留。
assert.doesNotMatch(usersSchemaPhp, /asset_points_card_/);
assert.doesNotMatch(usersSchemaJs, /asset_points_card_/);
assert.doesNotMatch(chargeCard, /asset_points_card_/);
assert.doesNotMatch(chargeCard, /'pass_charge_desc'/);
// 失去宿主的内联块样式不回潮(弹窗仍在用 __input/__btn 子元素样式;
// 通知走全局 notyf Toast,__tip 内联提示样式已随迁移删除)。
assert.doesNotMatch(creditsCss, /^\.uc-card-pass\s*\{/m);
assert.doesNotMatch(creditsCss, /uc-card-pass__title|uc-card-pass__desc|uc-card-pass__tip|\.uc-assets-col > \.uc-card-pass/);
});
test('兑换通知走全局 notyf Toast 固定 id 流,无内联 tip 残留', () => {
assert.match(creditsJs, /notyf\('正在兑换…', 'load', '', 'aksrt-card-pass-toast'\)/);
assert.match(creditsJs, /notyf\([^;]+?'danger', 5, 'aksrt-card-pass-toast'\)/);
assert.match(creditsJs, /notyf\([^;]+?'success', 5, 'aksrt-card-pass-toast'\)/);
// 初始化函数内不得再有 setTip 与 tip 元素查询(initCardPass 段)。
const from = creditsJs.indexOf('function initCardPass(');
const to = creditsJs.indexOf('function initBalanceCharge(', from + 1);
const slice = creditsJs.slice(from, to);
assert.ok(from !== -1 && to !== -1);
assert.doesNotMatch(slice, /setTip|data-aksrt-card-tip/);
assert.doesNotMatch(wallet, /data-aksrt-card-tip/);
// 成功后延迟刷新,保证成功 Toast 可见(与其余资产操作一致)。
assert.match(slice, /notyf\([^;]+?'success', 5, 'aksrt-card-pass-toast'\);[\s\S]{0,120}window\.setTimeout\(function \(\) \{ window\.location\.reload\(\); \}, 800\)/);
});
test('前端:VIP 缓存失效按后端识别类型', () => {
assert.match(creditsJs, /var redeemedType = \(data && data\.type\) \|\| type;/);
assert.match(creditsJs, /redeemedType === 'vip'/);
});
test('后端:核销订单金额恒为 0,options 路由与 labels 死代码不回潮', () => {
assert.match(chargeCard, /\/pay\/card-pass\/redeem/);
assert.doesNotMatch(chargeCard, /card-pass\/options/);
assert.doesNotMatch(chargeCard, /rest_options/);
assert.doesNotMatch(chargeCard, /'labels'\s+=>\s+\$labels/);
// 通用兑换能力后端契约保持:三类可兑换 + 空类型自动识别。
assert.match(chargeCard, /REDEEMABLE_TYPES = \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
assert.match(chargeCard, /'enum' => \['', self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
// create_redeem_order:余额卡不再把面额写进订单金额。
assert.doesNotMatch(chargeCard, /TYPE_BALANCE\)\s*\{[^}]*parsed_amount/s);
assert.match(chargeCard, /订单金额恒为 0/);
// 积分卡开关仅作为后端类型闸门。
assert.match(chargeCard, /'credits' => \['enabled' => /);
});
test('唯一类型闸门:总开关统一约束三类卡密,独立开关不回潮', () => {
// assert_type_enabled 只查总开关(+ 积分系统级依赖),不再有按类型的独立开关。
assert.match(chargeCard, /卡密兑换功能未开启/);
assert.match(chargeCard, /积分系统未开启/);
assert.doesNotMatch(chargeCard, /余额卡密兑换未开启|会员卡密兑换未开启|积分卡密兑换未开启/);
// 会员/积分卡密独立开关与说明为死代码。
assert.doesNotMatch(chargeCard, /pay_vip_pass_charge_s|pay_vip_pass_charge_desc|pass_charge'\s*=>/);
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs], ['vip.js', vipSchemaJs]]) {
assert.doesNotMatch(src, /pay_vip_pass_charge_s|pay_vip_pass_charge_desc|启用卡密兑换会员功能/, name);
assert.doesNotMatch(src, /启用积分卡密兑换/, name);
}
assert.match(vipSchemaJs, /title: '会员卡密'/);
assert.match(vipSchemaJs, /pay_vip_pass_charge_only_password/);
});
test('订单中心:卡密核销订单实付归零并按 note 生成订单名称', () => {
assert.match(userOrders, /if \(\$payment_method === 'card_pass'\)\s*\{\s*\$amount = \$is_points \? '0' : '0\.00';/);
assert.match(userOrders, /if \(!empty\(\$meta\['card_pass'\]\)\)\s*\{[\s\S]*?resolve_card_pass_title\(\$meta\)/);
assert.match(userOrders, /卡密兑换-余额充值/);
assert.match(userOrders, /卡密兑换-积分充值/);
assert.match(userOrders, /购入\[%1\$s\]%2\$s/);
assert.match(userOrders, /永久/);
});
test('后台 schema 语义更新为「启用卡密兑换功能」', () => {
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs]]) {
assert.match(src, /启用卡密兑换功能/, name);
assert.match(src, /title['"]?\s*(?::|=>)\s*'卡密兑换'/, name);
assert.doesNotMatch(src, /启用卡密充值余额功能/, name);
assert.doesNotMatch(src, /卡密充值说明/, name);
}
});