Files
wordpress-theme-aksrt/tests/widget-system/vip-points-exchange-modal.test.cjs
T
kate522 2801a89c1f fix(vip): 在 /user/vip/ 档位购买卡下方补充积分兑换会员入口
参考主题 zibpay_user_page_tab_content_vip:非会员 + 积分兑换会员开启 +
有可兑换套餐时,在两张档位购买卡下方输出全宽入口横条(points-color 图标
+ 标题 + 立即兑换 badg),点击经 data-aksrt-vip-points-open 打开 wp_footer
静态兑换弹窗,与会员购买弹窗内入口共用同一弹窗。

- aksrt-vip.php 新增 aksrt_get_vip_points_exchange_entry_html(),门控与
  弹窗壳同源(取服务端 catalog),已是会员不展示(对齐参考主题契约)
- vip-tab.php 在卡片区之后输出入口;credits.css 补 .uc-vip-points-entry
  表面样式(与档位卡同一变量体系,暗色自适应)
- pay-vip.js 改文档级委托承接 data-aksrt-vip-points-open,一处绑定覆盖
  静态页面横条与弹窗内按钮;移除 bindModalUi 内的重复分支
- 契约测试更新为双入口(11 项),node --test 420 项通过,php -l 通过

未写数据迁移;未做真实视觉效果验收。
2026-09-23 14:30:54 +08:00

165 lines
9.3 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 对齐)。
*
* 硬约束:
* 1) 服务端静态输出参考主题 refresh_modal 同构壳(`modal flex jc fade` >
* `modal-dialog modal-mini full-sm` > `modal-content` > `modal-body`)。
* 入口有两处,分别对应参考主题的两个变体:
* - 会员购买弹窗内(zibpay_get_pay_uservip_modal 的
* `btn-block but mb10 padding-lg c-green mini-radius` 按钮);
* - /user/vip/ 档位购买卡下方(zibpay_user_page_tab_content_vip 的
* `c-yellow zib-widget flex ac jsb` 全宽横条,仅非会员展示)。
* 两处入口均带 data-aksrt-vip-points-open,由 pay-vip.js 文档级委托打开
* 同一静态弹窗。
* 2) 静止态必须被 vip-modal.css 显式隐藏(.flex 覆盖回归护栏),打开态为
* flex 而非 block,且层级高于 runtime 宿主(100050),以复现参考主题在
* 会员弹窗之上再叠一层兑换弹窗的行为。
* 3) 旧的客户端自实现弹窗(renderPointsModal / aksrt-vip-points-option /
* catalog.vip_url)与旧版 /user/vip/ 自定义入口(uc-vip-points-link)
* 必须已彻底移除。
*/
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 modal = read('src', 'Content', 'VipServiceModal.php');
const catalogSource = read('src', 'Content', 'VipServicePurchase.php');
const footer = read('inc', 'helpers', 'aksrt-vip.php');
const vipJs = read('assets', 'js', 'pay-vip.js');
const creditsJs = read('assets', 'js', 'credits.js');
const vipCss = read('assets', 'css', 'frontend', 'vip-modal.css');
const vipTab = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
const creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css');
test('弹窗为参考主题 refresh_modal 同构静态壳并带 data 钩子', () => {
assert.match(modal, /public function get_points_exchange_modal_html/);
assert.match(modal, /class="modal flex jc fade"[^']*data-aksrt-vip-points-modal/);
assert.match(modal, /class="modal-dialog modal-mini full-sm"/);
assert.match(modal, /class="modal-content"><div class="modal-body">/);
assert.match(modal, /data-dismiss="modal"/);
assert.match(modal, /#icon-close/);
assert.match(modal, /data-aksrt-vip-points-form/);
});
test('弹窗内容保持参考主题结构:标题/我的积分/卡片选择/立即兑换', () => {
// 头部:.mb10.touch + modal-title.flex.ac + vip_1 图标。
assert.match(modal, /'<div class="mb10 touch">'/);
assert.match(modal, /class="modal-title flex ac"/);
assert.match(modal, /#icon-vip_1/);
// 我的积分盒(点回用户中心积分页)。
assert.match(modal, /muted-box padding-h10 mb20/);
assert.match(modal, /我的积分/);
assert.match(modal, /#icon-points/);
assert.match(modal, /aksrt_get_user_center_tab_url\('credits'\)/);
// 卡片列表 + 参考主题 data-for/data-value 选中模型。
assert.match(modal, /请选择需兑换的会员/);
assert.match(modal, /flex hh exchange-card-box/);
assert.match(modal, /vip-card vip-theme/);
assert.match(modal, /data-for="product_id" data-value=/);
assert.match(modal, /flex jsb ac hh em16 mt10/);
// 表单:hidden 契约 + 立即兑换按钮 + 积分不足提示。
assert.match(modal, /name="order_type" value="4"/);
assert.match(modal, /name="action" value="points_initiate_pay"/);
assert.match(modal, /mt6 but jb-yellow btn-block radius padding-lg/);
assert.match(modal, /data-aksrt-vip-points-submit/);
assert.match(modal, /badg c-red btn-block/);
});
test('弹窗壳随 wp_footer 全局输出且不进入 catalog 载荷', () => {
assert.match(footer, /function aksrt_render_vip_points_exchange_modal/);
assert.match(footer, /add_action\('wp_footer', 'aksrt_render_vip_points_exchange_modal'\)/);
assert.match(footer, /VipService::get_instance\(\)->get_points_exchange_modal_html/);
// 兑换入口不再跳用户中心:catalog 里的 vip_url 已清除。
assert.doesNotMatch(catalogSource, /'vip_url'/);
assert.doesNotMatch(modal, /'vip_url'/);
});
test('弹窗壳静止态必须被 vip-modal.css 显式隐藏(.flex 覆盖回归护栏)', () => {
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s*\{\s*display:\s*none;/);
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\.in\s*\{\s*display:\s*flex;/);
assert.doesNotMatch(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\.in\s*\{\s*display:\s*block;/);
// 层级必须高于 runtime 宿主(.aksrt-modal-runtime 的 z-index: 100050)。
assert.match(vipCss, /\.modal\[data-aksrt-vip-points-modal\]\s*\{[^}]*z-index:\s*100060;/s);
assert.match(vipCss, /\.modal-backdrop\[data-aksrt-vip-points-backdrop\]\s*\{\s*z-index:\s*100055;/);
});
test('开关自包含:不依赖 jQuery/Bootstrap modal 运行时', () => {
assert.match(vipJs, /function openPointsModal\(trigger\)/);
assert.match(vipJs, /function closePointsModal\(\)/);
assert.doesNotMatch(vipJs, /\.modal\(\s*['"]show['"]/);
assert.doesNotMatch(vipJs, /hidden\.bs\.modal/);
assert.match(vipJs, /pointsBackdrop\.className = "modal-backdrop fade in"/);
assert.match(vipJs, /modal\.style\.display = "flex"/);
assert.doesNotMatch(vipJs, /modal\.style\.display = "block"/);
assert.match(vipJs, /void modal\.offsetWidth/);
});
test('会员弹窗内的入口为参考主题同款按钮并开本地弹窗', () => {
// 参考主题 zibpay_get_pay_uservip_modal 的 class 与图标顺序。
assert.match(vipJs, /node\("a", "btn-block but mb10 padding-lg c-green mini-radius"\)/);
assert.match(vipJs, /#icon-points-color/);
// 图标顺序与 class 串保持参考主题一致;FA5 样式类 `fas` 由图标契约(
// legacy-navigation-contract 的 FA5-only 约束)强制要求,故在此一并断言。
assert.match(vipJs, /fa fas fa-angle-right em12 ml10/);
assert.match(vipJs, /data-aksrt-vip-points-open/);
assert.match(vipJs, /openPointsModal\(pointsLink\)/);
});
test('/user/vip/ 档位购买卡下方的入口为参考主题用户中心同款横条', () => {
// 参考主题 zibpay_user_page_tab_content_vip:my_box → vip_card → points link,
// 横条为 `c-yellow zib-widget flex ac jsb`(zib-widget → 本主题aksrt-panel表面)。
assert.match(footer, /function aksrt_get_vip_points_exchange_entry_html/);
assert.match(vipTab, /aksrt_get_vip_points_exchange_entry_html\(\$user_id, \$level\)/);
// 结构 1:1:左 points-color 图标 + 标题(flex1 em12 c-yellow),
// 右「立即兑换」badg + 右箭头。
assert.match(footer, /uc-vip-points-entry flex ac jsb/);
assert.match(footer, /flex1 em12 c-yellow/);
assert.match(footer, /#icon-points-color/);
assert.match(footer, /badg radius c-yellow/);
assert.match(footer, /fa fas fa-chevron-circle-right ml6/);
assert.match(footer, /data-aksrt-vip-points-open/);
});
test('/user/vip/ 入口门控与弹窗壳同源:非会员 + 兑换开启 + 有可兑换套餐', () => {
// 参考主题 zibpay_get_vip_points_exchange_link 门控:已是会员不展示。
assert.match(footer, /if \(\$vip_level > 0\) \{\s*return '';/);
// 可用性直接取服务端 catalog(内含 credits 门控与等级启用过滤),
// 与 wp_footer 弹窗壳的渲染条件一致,避免出现无弹窗可开的死入口。
assert.match(footer, /get_catalog\(\$user_id\)/);
assert.match(footer, /empty\(\$points_exchange\['enabled'\]\) \|\| \$packages === \[\]/);
// 模板传入当前会员等级(>0 时不输出)。
assert.match(footer, /int \$vip_level = -1/);
});
test('页面入口样式落在 credits.css 且与档位卡同一表面体系', () => {
assert.match(creditsCss, /\.uc-vip-points-entry \{/);
assert.match(creditsCss, /\.uc-vip-points-entry\s*\{[^}]*var\(--main-bg-color, var\(--surface-1, #fff\)\)/s);
assert.match(creditsCss, /\.uc-vip-points-entry\s*\{[^}]*box-shadow:\s*var\(--shadow-sm\)/s);
});
test('入口开合由文档级委托驱动,弹窗内不再单独挂监听', () => {
// /user/vip/ 静态横条与弹窗内按钮共用一处 document 委托(冒泡)。
assert.match(vipJs, /function bindPointsEntry\(\)/);
assert.match(vipJs, /bindPointsEntry\(\);/);
assert.match(vipJs, /document\.addEventListener\("click", function \(event\) \{[\s\S]*?data-aksrt-vip-points-open/);
// bindModalUi 内的重复分支已移除:openPointsModal(pointsLink) 只保留一处调用。
const openCalls = vipJs.match(/openPointsModal\(pointsLink\)/g) || [];
assert.strictEqual(openCalls.length, 1);
});
test('旧的客户端自实现弹窗与旧版自定义入口已彻底移除', () => {
assert.doesNotMatch(vipJs, /renderPointsModal|exchangeFromModal|vipPointsPay/);
assert.doesNotMatch(vipJs, /aksrt-vip-points-option|aksrt-vip-points-modal__/);
assert.doesNotMatch(vipCss, /\.aksrt-vip-points-option|\.aksrt-vip-points-submit/);
// 旧版 /user/vip/ 自定义入口(uc-vip-points-link)不得回归;现行入口为
// 参考主题 1:1 的 uc-vip-points-entry 横条。
assert.doesNotMatch(vipTab, /uc-vip-points-link/);
assert.doesNotMatch(creditsCss, /\.uc-vip-points-link/);
// 用户中心侧的旧兑换控制器([data-aksrt-vip-points],全站无对应 DOM)。
assert.doesNotMatch(creditsJs, /initVipPointsExchange|data-aksrt-vip-points/);
});