Files
wordpress-theme-aksrt/tests/widget-system/balance-charge-modal.test.cjs
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

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

121 lines
6.9 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.
/**
* 余额充值弹窗壳契约(第 19 轮 Zibll 对齐 + 第 21 轮全页不可点击回归护栏)。
*
* 关键约束:本主题的弹窗壳是服务端静态输出,与 Zibll(refresh_modal JS
* 按需创建、且其 CSS 无 bootstrap .modal 基础规则)架构不同。若弹窗壳在
* 静止态没有被显式隐藏,main.css 的 .flex{display:flex}(后加载、与
* bootstrap.min.css 的 .modal{display:none} 同特异性、后者胜)会让
* `class="modal flex jc fade"` 壳永久 display:flex 铺满全屏,叠加 .fade
* 的 opacity:0 形成隐形遮罩 → 整页所有按钮不可点击(2026-09-17 实测故障)。
*
* 因此 credits.css 必须以 (0,2,0) 特异性压制:
* .modal[data-jio-charge-modal] { display: none; }
* .modal[data-jio-charge-modal].in { display: flex; }
* 展示态由 credits.js 自包含开关的内联 display:flex 驱动(不依赖
* jQuery/Bootstrap 运行时;block 会破坏 .flex.jc 双轴居中——
* .modal.flex .modal-dialog{margin:0} 在 block 下使对话框顶在左上角),
* 关闭后内联样式撤除即回到隐藏规则。
*/
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 creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css');
const balanceRest = read('src', 'Credits', 'BalanceServiceRest.php');
test('弹窗壳为 Zibll refresh_modal 同构静态壳并带 data 钩子', () => {
assert.match(wallet, /class="modal flex jc fade"[^>]*data-jio-charge-modal/);
assert.match(wallet, /data-jio-charge-form/);
assert.match(wallet, /data-dismiss="modal"/);
assert.match(wallet, /initiate-pay/);
});
test('弹窗壳静止态必须被 credits.css 显式隐藏(.flex 覆盖回归护栏)', () => {
// (0,2,0) 特异性压制 main.css 的 .flex{display:flex}。
assert.match(creditsCss, /\.modal\[data-jio-charge-modal\]\s*\{\s*display:\s*none;/);
// 打开态必须是 flex 而非 block:.modal.flex .modal-dialog{margin:0} 在
// block 下使对话框顶在左上角(无头 Chrome 几何实证),flex 下由壳的
// .flex.jc 双轴居中(Zibll 生产环境即此形态)。
assert.match(creditsCss, /\.modal\[data-jio-charge-modal\]\.in\s*\{\s*display:\s*flex;/);
assert.doesNotMatch(creditsCss, /\.modal\[data-jio-charge-modal\]\.in\s*\{\s*display:\s*block;/);
// 购买积分弹窗壳同构(服务端静态输出 .modal.flex.jc.fade),静止态同样
// 必须显式隐藏,否则隐形遮罩拦截整页点击(2026-09-18 实测回归)。
const balance = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
assert.match(balance, /class="modal flex jc fade"[^>]*data-jio-points-pay-modal/);
assert.match(creditsCss, /\.modal\[data-jio-points-pay-modal\]\s*\{\s*display:\s*none;/);
assert.match(creditsCss, /\.modal\[data-jio-points-pay-modal\]\.in\s*\{\s*display:\s*flex;/);
});
test('开关自包含:不依赖 jQuery/Bootstrap modal 运行时', () => {
assert.match(creditsJs, /function openChargeModal/);
assert.match(creditsJs, /function closeChargeModal/);
assert.match(creditsJs, /data-jio-charge-open/);
// 动态创建遮罩层(bootstrap.min.css 已有 .modal-backdrop 规则)。
assert.match(creditsJs, /modal-backdrop fade in/);
// 充值弹窗开关不得回退到 jQuery modal 运行时(时序异常时弹窗打不开)。
assert.doesNotMatch(creditsJs, /\.modal\(\s*['"]show['"]/);
assert.doesNotMatch(creditsJs, /hidden\.bs\.modal/);
// 打开前移除旧缓存 hidden 标记;关闭走 300ms fade-out 收尾。
assert.match(creditsJs, /removeAttribute\('hidden'\)/);
assert.match(creditsJs, /void modal\.offsetWidth/);
// 打开态内联 display 必须是 flex(block 会破坏 .flex.jc 双轴居中)。
assert.match(creditsJs, /modal\.style\.display = 'flex'/);
assert.doesNotMatch(creditsJs, /modal\.style\.display = 'block'/);
});
/**
* 余额充值提交契约(预设档位被 REST 参数层拦截的回归护栏)。
*
* custom_price 只在 package=custom 时被消费,选中预设档位时前端必带该键且值为
* 空串。WP REST 的 default 仅在「键完全缺失」时生效:键存在而值为空串时 default
* 不参与,空串直接进 pattern 校验 → 请求在参数层就被拦下(rest_invalid_param
* 「无效参数:custom_price」),业务回调根本不执行。
*
* 因此参数声明只保留 type/default/清洗,金额格式与区间的唯一裁决留在
* create_charge_order()(AssetAmount::balance + custom_limit 区间校验)。
*/
test('充值请求参数:custom_price 空串放行,金额格式只由业务层裁决', () => {
const chargeRoute = balanceRest.slice(
balanceRest.indexOf("'/pay/balance/charge'"),
balanceRest.indexOf("'/pay/transfer'"),
);
assert.match(chargeRoute, /'custom_price'\s*=>\s*\['type'\s*=>\s*'string',\s*'default'\s*=>\s*''/);
assert.doesNotMatch(
chargeRoute,
/custom_price'[\s\S]*?'pattern'/,
'custom_price must not carry a second amount rule: the empty string is a valid "not applicable" value',
);
const createCallback = balanceRest.slice(
balanceRest.indexOf('public function rest_charge_create'),
balanceRest.indexOf('public function rest_transfer'),
);
assert.match(createCallback, /\$custom_price\s*=\s*\(string\) \$request->get_param\('custom_price'\)/);
assert.match(createCallback, /\$payment_method\s*=\s*\(string\) \$request->get_param\('payment_method'\)/);
// 手工重读请求体会让声明的 default/sanitize_callback 形同虚设(第二套取值口径)。
assert.doesNotMatch(createCallback, /get_json_params\(\)|->get_params\(\)/);
});
test('充值弹窗:选中预设档位清空自定义金额,提交路径无重复读取', () => {
// 选中预设档位即放弃自定义金额;否则输入框里残留的数字会在提交时按「合法自定义
// 金额」把 package 顶回 custom,用户刚点的档位被静默忽略。
assert.match(creditsJs, /if \(value !== 'custom' && customInput\) \{[\s\S]{0,60}customInput\.value = '';/);
// 支付方式在提交处理器开头已读取,选档位不会改动它,故充值弹窗内只应存在一处读取
// (积分购买弹窗是同构的另一套初始化,各自一处,不在此断言范围)。
const chargeInit = creditsJs.slice(
creditsJs.indexOf('function initBalanceCharge'),
creditsJs.indexOf('function initTransfer'),
);
assert.strictEqual(
[...chargeInit.matchAll(/form\.querySelector\('input\[name="payment_method"\]'\)/g)].length,
1,
'the submit handler must not re-read payment_method (dead duplicate guard)',
);
});