Files
wordpress-theme-aksrt/tests/widget-system/card-pass-modal-redeem.test.cjs
T
kate522andClaude Opus 5 5c6b422866 fix(frontend): route every user-center points label through the custom currency name
后台「积分配置 → 自定义货币名称」改名后,用户中心仍有大量硬编码
「积分」:做任务赚积分 / 购买积分 / 签到领积分、积分记录 / 积分任务、
每日可免费获取 XX 积分、我的积分、积分+1、创作中心积分收益等。

get_currency_name() 早已存在且可用,问题只是这些文案没有走它。
本PR 全部替换,无双路、无迁移:

服务端渲染(用户中心 renderers)
- credits-balance.php:购买/做任务赚/签到领 + 整个购买弹窗
- profile-credits.php:积分记录 / 积分任务 页签
- credits-points-tasks.php:每日上限三态与空态
- sidebar-creator.php / profile-creator.php:积分收益
- transfer-modal.php:积分转账提示
- Signin.php:签到详情弹窗(我的积分 / 奖励 tooltip / 积分+N)
- VipServiceModal.php:我的积分 / 积分兑换会员
- download-permission-card.php:购买价格

客户端渲染(JS 无法调用 PHP,注入 currencyName 到 aksrtData)
- signin.js:签到状态行「已签到 N 天 +1」
- credits.js:转账标题/金额标签 + 购买提示 toast
- cms-download.js:积分不足视图、我的积分 入口、余额不足文案
- pay-vip.js:积分兑换会员入口(优先 catalog.currency_name)
- order-center.js:订单行单位回退

契约测试同步改为断言模板构造而非死字符串,并新增两例:
改名后跟随、未注入时回退「积分」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 15:09:49 +08:00

229 lines
14 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.
/**
* 卡密兑换渠道契约(三个购买弹窗 · 1:1 对齐参考主题)。
*
* 背景:余额卡片顶部的独立「卡密兑换」通用弹窗已删除,兑换入口收敛到
* 三个购买弹窗的支付方式中(对齐参考主题 zibpay_get_payment_methods +
* zibpay_is_allow_card_pass_pay 过滤器):
* 1) 余额充值弹窗(credits-wallet.php,服务端渲染)
* 2) 购买积分弹窗(credits-balance.php,服务端渲染,Zibll points-pay modal 结构)
* 3) 会员购买/续费弹窗(VipServiceModal JSON 视图模型 + pay-vip.js 渲染)
*
* 后台语义:余额充值 → 卡密兑换 → 三个独立开关
* 启用卡密兑换余额功能 / 启用卡密兑换积分功能 / 启用卡密兑换会员功能
* 任一开关关闭后,对应前台弹窗不出现卡密兑换渠道收款接口。
*
* REST 契约:POST /pay/card-pass/redeem,type 必填(balance/vip/credits),
* 不再有「空类型按卡密自动识别」;核销订单金额恒为 0。
*/
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 creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
const creditsJs = read('assets', 'js', 'credits.js');
const payVipJs = read('assets', 'js', 'pay-vip.js');
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
const userOrders = read('src', 'Payment', 'UserOrders.php');
const vipPackages = read('src', 'Content', 'VipServicePackages.php');
const aksrtPay = read('inc', 'helpers', 'aksrt-pay.php');
const mainCss = read('assets', 'css', 'main.css');
const vipPurchase = read('src', 'Content', 'VipServicePurchase.php');
const vipModal = read('src', 'Content', 'VipServiceModal.php');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
test('独立入口与通用弹窗已删除', () => { assert.equal(
fs.existsSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'credits-vip.php')),
false,
'credits-vip.php 死文件应已删除'
);
// 通用卡密弹窗与 initCardPass 不回潮。
assert.doesNotMatch(wallet, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
assert.doesNotMatch(creditsJs, /function initCardPass\(|initCardPass\(\)/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
// 卡密 logo 就位(三个弹窗共用)。
assert.equal(fs.existsSync(path.join(root, 'assets', 'img', 'pay-card-pass-logo.svg')), true);
});
test('余额充值弹窗:卡密渠道为支付方式之一,卡号/卡密输入按单密码模式渲染', () => {
assert.match(wallet, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
// 支付方式列表注入 card_pass(由 get_charge_view / get_public_options['balance'] 驱动)。
assert.match(wallet, /card_pass_redeem\['desc'\]/);
assert.match(wallet, /card_pass_redeem\['only_password'\]/);
// 卡密输入盒契约与参考主题一致。
assert.match(wallet, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(wallet, /name="card_pass\[card\]"/);
assert.match(wallet, /name="card_pass\[password\]"/);
// 仅卡密可用时隐藏金额选择(参考主题语义)。
assert.match(wallet, /\$charge_card_only/);
assert.match(wallet, /\$charge_card_in_methods/);
});
test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () => {
// 商品/自定义选择 + 支付方式 + hidden + 立即支付。
assert.match(creditsRenderer, /data-aksrt-points-pay-modal/);
assert.match(creditsRenderer, /data-aksrt-points-pay-form/);
assert.match(creditsRenderer, /data-aksrt-points-pay-open/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-points-exchange-(open|modal|form|amount|preview|close)/);
// 商品/自定义提示语跟随自定义货币名称(后台「积分配置 → 自定义货币名称」)。
assert.match(creditsRenderer, /请选择需购买的%s', 'aksrt'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /请输入需购买的%s数额', 'aksrt'\), esc_html\(\$currency\)/);
assert.match(creditsRenderer, /name="points_product"/);
assert.match(creditsRenderer, /name="points_custom"/);
assert.match(creditsRenderer, /name="payment_method"/);
assert.match(creditsRenderer, /立即支付/);
// 卡密渠道受「启用卡密兑换积分功能」控制。
assert.match(creditsRenderer, /points_card_pass\['enabled'\]/);
assert.match(creditsRenderer, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(creditsRenderer, /name="card_pass\[password\]"/);
});
test('购买积分前端:商品/支付方式选中模型 + 余额/卡密提交分流', () => {
assert.match(creditsJs, /function initPointsPay\(\)/);
assert.match(creditsJs, /data-aksrt-points-pay-open/);
assert.match(creditsJs, /\[data-for="points_product"\]/);
// 卡密选中:隐藏 .charge-box、显示卡密输入盒(契约与参考主题一致)。
assert.match(creditsJs, /data-controller="payment_method"\]\[data-value="card_pass"/);
assert.match(creditsJs, /value === 'card_pass' \? 'none' : ''/);
// 余额支付 → /pay/balance/points-exchange(product + custom 契约)。
assert.match(creditsJs, /\/pay\/balance\/points-exchange/);
assert.match(creditsJs, /JSON\.stringify\(\{ product: pkg, custom: validCustom \? custom : '0' \}\)/);
// 卡密支付 → /pay/card-pass/redeem type=credits。
assert.match(creditsJs, /redeemCardPass\('credits'/);
// bootAssets 挂载新函数,无死调用。
assert.match(creditsJs, /function bootAssets\(\)[\s\S]*?initPointsPay\(\);/);
});
test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单)', () => {
// 后端:available_payment_methods 追加 card_pass + 视图模型透传。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/);
assert.match(vipPackages, /function vip_card_pass_context\(\)/);
assert.match(vipPackages, /vip_card_pass_context\(\)/);
assert.match(vipModal, /'card_pass' => \$card_pass/);
assert.match(vipModal, /'card_pass' => \$card_pass,/);
// 支付方式组件与其他两个弹窗同源:logo URL 服务端映射 + 余额/货币符号透传。
assert.match(vipModal, /function payment_method_logo_url\(string \$id\): string/);
assert.match(vipModal, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
assert.match(vipModal, /'wallet_balance' => \(string\) \(\$catalog\['wallet_balance'\]/);
assert.match(vipModal, /'currency_mark' =>/);
// 后端防御:purchase() 拒绝 card_pass(卡密直兑走 redeem)。
assert.match(vipPurchase, /aksrt_card_pass_redeem_required/);
// 前端:Zibll initiate_pay_input 同构组件——payment-method-radio logo 行、
// 我的余额盒(data-controller == balance)、卡密盒(password_box 类结构)、
// hidden payment_method、initiate-pay 按钮。
assert.match(payVipJs, /payment-method-radio hollow-radio flex-auto pointer/);
assert.match(payVipJs, /item\.setAttribute\("data-for", "payment_method"\)/);
assert.match(payVipJs, /mb10 muted-box padding-h10/);
assert.match(payVipJs, /"data-condition", "=="/);
assert.match(payVipJs, /"data-value", "balance"/);
assert.match(payVipJs, /mb10 padding-h10 padding-w6/);
assert.match(payVipJs, /"data-value", "card_pass"/);
assert.match(payVipJs, /hidden\.name = "payment_method"/);
assert.match(payVipJs, /mt6 but jb-red initiate-pay btn-block radius/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|aksrt_vip_pay_method|aksrt-vip-submit/);
// 按钮字样对齐参考主题:会员弹窗即 initiate_pay_input(4) 默认「立即支付」;
// 无可用支付方式渲染 Zibll 同款 badg 提示(而非禁用按钮/自实现文案)。
assert.doesNotMatch(payVipJs, /cta_label/);
assert.match(payVipJs, /canPurchase \? "立即支付"/);
assert.match(payVipJs, /badg px12 c-yellow-2/, "暂时无法支付,请与客服联系");
// 渠道展示名对齐参考主题 zibpay_get_payment_method_args(余额/微信/支付宝)。
assert.match(aksrtPay, /'balance'\s+=> __\('余额', 'aksrt'\)/);
assert.match(aksrtPay, /'wechatpay'\s+=> __\('微信', 'aksrt'\)/);
// 显隐:通用 data-controller(==/!=)匹配 + 卡密选中隐藏套餐。
assert.match(payVipJs, /function applyVipMethodVisibility\(/);
assert.match(payVipJs, /querySelectorAll\('\[data-controller="payment_method"\]'\)/);
assert.match(payVipJs, /querySelectorAll\("\.charge-box"\)/);
// 提交分流:卡密直兑 /pay/card-pass/redeem type=vip。
assert.match(payVipJs, /function redeemVipCardPass\(/);
assert.match(payVipJs, /\/pay\/card-pass\/redeem/);
assert.match(payVipJs, /type: "vip"/);
assert.match(payVipJs, /view\.card_pass && view\.card_pass\.enabled/);
});
test('REST 契约:type 必填(balance/vip/credits),无空类型自动识别', () => {
assert.match(chargeCard, /'type' => \['type' => 'string', 'required' => true, 'enum' => \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]\]/);
assert.match(chargeCard, /卡密兑换类型无效/);
assert.match(chargeCard, /REDEEMABLE_TYPES = \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
// 兼容层:redeem_for_user 仍保留 expected_type 形参(前端恒传明确类型)。
assert.match(chargeCard, /function redeem_for_user\(int \$user_id, string \$password, string \$card = '', string \$expected_type = ''/);
});
test('后端类型闸门:三个独立开关各约束自己的类型', () => {
assert.match(chargeCard, /'balance' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'credits' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'vip'\s+=> \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
// 会员单密码默认关闭(对齐参考主题 default false):未显式开启时卡密盒显示卡号+密码。
assert.doesNotMatch(chargeCard, /'only_password' => true/);
assert.match(read('src', 'Settings', 'PaymentRuntime.php'), /'only_password' => !empty\(\$settings\['pay_vip_pass_charge_only_password'\]\),/);
assert.doesNotMatch(read('src', 'Settings', 'PaymentRuntime.php'), /!isset\(\$settings\['pay_vip_pass_charge_only_password'\]\)/);
assert.match(chargeCard, /卡密兑换余额功能未开启/);
assert.match(chargeCard, /卡密兑换积分功能未开启/);
assert.match(chargeCard, /卡密兑换会员功能未开启/);
assert.match(chargeCard, /积分系统未开启/);
// 收款接口 card_pass 主开关:三开关任一开启即生效。
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
assert.match(paymentRuntime, /\$card_pass_enabled = !empty\(\$settings\['pay_balance_pass_charge_s'\]\)[\s\S]*?\|\| !empty\(\$settings\['pay_vip_pass_charge_s'\]\);/);
});
test('后台 schema:卡密兑换拆分为三个独立开关', () => {
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs]]) {
assert.match(src, /启用卡密兑换余额功能/, name);
assert.match(src, /启用卡密兑换积分功能/, name);
assert.match(src, /启用卡密兑换会员功能/, name);
assert.match(src, /points_pass_exchange_s/, name);
assert.match(src, /points_pass_exchange_desc/, name);
assert.match(src, /points_pass_exchange_only_password/, name);
assert.match(src, /pay_vip_pass_charge_desc/, name);
assert.match(src, /pay_vip_pass_charge_only_password/, name);
// 旧总开关语义不回潮。
assert.doesNotMatch(src, /启用卡密兑换功能'|启用卡密兑换功能"/, name);
}
// recharge.js defaults 补齐三开关字段。
assert.match(schemaJs, /points_pass_exchange_s: false/);
assert.match(schemaJs, /pay_vip_pass_charge_s: false/);
// vip schema 的「会员卡密」组迁移至 recharge 卡密兑换组,原组删除。
assert.doesNotMatch(vipSchemaJs, /title: '会员卡密'/);
assert.doesNotMatch(vipSchemaJs, /pay_vip_pass_charge_only_password/);
const schemaPaymentVip = schemaPhp;
assert.doesNotMatch(schemaPaymentVip, /title' => '会员卡密'/);
});
test('核销订单契约:金额恒为 0,订单名称按 note 生成', () => {
assert.match(chargeCard, /订单金额恒为 0/);
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('多渠道防挤压:图标 flex:none + 行不换行项目收缩成芯片(对齐 Zibll)', () => {
// 图标禁用 flex 压缩并抵消 AKSRT bootstrap 全局 img{max-width:100%}
// (flex 压缩时把图标钳制到被压窄的项宽,实测 5 渠道 400px 弹窗图标
// 被压成 16px 细条)。行保持 Zibll 原样不换行:空间不足时项目收缩为
// 「图标上/文字下」芯片(实测 5 渠道一行 5 个、6 渠道一行 6 个,图标
// 恒为固有比例),绝不允许整行提前断行。
const rule = mainCss.match(/\.payment-method-radio img \{[\s\S]*?\n\}/);
assert.ok(rule, 'main.css 应存在 .payment-method-radio img 规则块');
assert.match(rule[0], /height:\s*35px;/);
assert.match(rule[0], /width:\s*auto;/);
assert.match(rule[0], /max-width:\s*none;/);
assert.match(rule[0], /flex:\s*none;/);
// 方法行标记与参考主题逐字一致(.flex.mb10,无 hh)。
assert.match(wallet, /class="flex mb10">/);
assert.match(creditsRenderer, /class="flex mb10">/);
assert.match(payVipJs, /node\("div", "flex mb10"\)/);
assert.doesNotMatch(payVipJs, /"flex hh mb10"/);
});
test('渠道名称禁折行(三字渠道名对芯片宽度零余量,站点字体差异即触发换行)', () => {
assert.match(mainCss, /\.payment-method-radio > div \{[^}]*white-space:\s*nowrap;/s);
});