Files
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding
survived the sweeps in CSS classes, the SVG sprite prefix, a persisted
widget field ID, JS events, admin Vue class names, test infrastructure
and docblocks.

Renames:
- zib-widget / zib-widget-wrap / zib-slider → jio-widget-box /
  jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern
  widget shell, jio-box-* by the card primitives)
- zib-partners-scroll + zps-* → jio-partners-scroll + jps-*
- zib-combo-info-card + zci-* → jio-combo-info-card + jci-*
- zibsvg- → jiosvg-, including the exported picker helpers and the
  fa-zibsvg- family-prefix guard
- zibpay_type → pay_type; the dual-read fallback in PostQuery collapses
  to a single key
- [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success →
  jio_ajax.success
- aks-* admin Vue classes → jio-*
- zib_palette() → palette()

User-facing vendor traces are gone: the floating "more" button default no
longer ships a zibll.com link or 子比主题 marketing copy, the notice
widget ships no preset promo entries, and the four 官网教程 links are
removed from the post-list admin schema.

Test infrastructure renamed to neutral names (docker-compose.reference.yml,
Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with
their DB/volume/env/slug identifiers following.

Kept intentionally, documented in tests/fixtures/README.md: the golden
fixture reference fingerprint (a SHA-256 record of an external directory,
read by validate-widget-golden / generate-widget-golden-fixture /
audit-widget-system), the doesNotMatch guards that prove the old tokens are
gone, and the reference theme's own meta key in the visual fixtures.

No backward-compat or data migration: saved widget configs holding old
icon keys or the old field ID silently lose them, per the project's
rewrite-over-compatibility rule.

Verified: 132/132 node contract tests, php tests/widget-system/run.php
(146 assertions), audit-admin-contracts, audit-psr4-paths and
validate-widget-golden all green; tracked bundles regenerated and in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 00:06:54 +08:00

121 lines
6.9 KiB
JavaScript
Raw Permalink 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 轮参考主题对齐 + 第 21 轮全页不可点击回归护栏)。
*
* 关键约束:本主题的弹窗壳是服务端静态输出,与按需创建弹窗壳的运行时方案
* (其 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('弹窗壳为静态服务端渲染壳并带 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 双轴居中(生产环境即此形态)。
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)',
);
});