Files
wordpress-theme-aksrt/tests/widget-system/card-pass-modal-redeem.test.cjs
T
kate522 721fa29b8d fix(vip): 会员弹窗支付方式渠道对齐 Zibll initiate_pay_input 组件
支付方式 UI 不再自实现(uc-vip-shop__method 文本 radio),改为与其他
两个弹窗(充值/积分)完全同源的 Zibll zibpay_get_initiate_pay_input
组件结构:

- 支付方式行:.payment-method-radio.hollow-radio + logo 图 +
  data-for/data-value 选中模型(多渠道时展示「请选择支付方式」)
- 我的余额盒:data-controller == balance 条件显隐,展示货币符号与余额
  (视图模型透传 wallet_balance / currency_mark)
- 卡密盒:Zibll password_box 同构(mb10 padding-h10 padding-w6 +
  desc muted-box + form-control 卡号/卡密输入)
- hidden payment_method + initiate-pay 按钮类(mt6 but jb-red
  btn-block radius)
- 服务端 payment_method_logo_url() 统一映射 pay-*-logo.svg
- 显隐统一走 data-controller(==/!=)通用匹配,卡密选中隐藏 .charge-box
- 清理 uc-vip-shop__method* 死样式与旧 change 监听;契约测试同步锁定
  (.gitignore 白名单补 card-pass-modal-redeem.test.cjs)
2026-09-18 12:44:38 +08:00

191 lines
12 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 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, /请选择需购买的积分|请输入需购买的积分数额/);
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/);
// 显隐:通用 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' => true\]/);
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, /永久/);
});