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>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-02 00:06:54 +08:00
1 parent 42cc64106d
commit 427968a721
188 files changed
+915 -967

No files matched your search

@@ -28,7 +28,7 @@ test('modal and sign page use one renderer and one state machine', () => {
assert.doesNotMatch(loader, /sign-register/);
});
test('static Zibll authentication dimensions and responsive contract are retained', () => {
test('static authentication dimensions and responsive contract are retained', () => {
const css = read('assets/css/routes/sign.css');
assert.match(css, /\.sign\s*\{[\s\S]*?max-width:\s*330px/);
assert.match(css, /#u_sign\.has-sign-img \.modal-dialog\s*\{\s*width:\s*800px/);
@@ -258,15 +258,15 @@ test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
// resolve to the theme's own symbol sheet exactly like the reference does.
assert.match(icons, /'gitee'\s*=>\s*'zibsvg-gitee'/);
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
assert.match(icons, /'gitee'\s*=>\s*'jiosvg-gitee'/);
assert.match(icons, /'google'\s*=>\s*'jiosvg-google-color'/);
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
assert.match(sprites, /jio_svgs\['gitee'\]/);
assert.match(sprites, /jio_svgs\['google-color'\]/);
// Sprite values must reach the shared icon renderer instead of being wrapped
// in a bogus <i class="zibsvg-…">.
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?jio_icon_markup\(\$class, \$extra_classes\)/);
// in a bogus <i class="jiosvg-…">.
assert.match(icons, /strpos\(\$class, 'jiosvg-'\) === 0[\s\S]*?jio_icon_markup\(\$class, \$extra_classes\)/);
});
test('the top-nav user popover reproduces the reference quick-login block', () => {
@@ -1,8 +1,8 @@
/**
* 余额充值弹窗壳契约(第 19 轮 Zibll 对齐 + 第 21 轮全页不可点击回归护栏)。
* 余额充值弹窗壳契约(第 19 轮参考主题对齐 + 第 21 轮全页不可点击回归护栏)。
*
* 关键约束:本主题的弹窗壳是服务端静态输出,与 Zibll(refresh_modal JS
* 按需创建、且其 CSS 无 bootstrap .modal 基础规则)架构不同。若弹窗壳在
* 关键约束:本主题的弹窗壳是服务端静态输出,与按需创建弹窗壳的运行时方案
* (其 CSS 无 bootstrap .modal 基础规则)架构不同。若弹窗壳在
* 静止态没有被显式隐藏,main.css 的 .flex{display:flex}(后加载、与
* bootstrap.min.css 的 .modal{display:none} 同特异性、后者胜)会让
* `class="modal flex jc fade"` 壳永久 display:flex 铺满全屏,叠加 .fade
@@ -29,7 +29,7 @@ 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 钩子', () => {
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"/);
@@ -41,7 +41,7 @@ test('弹窗壳静止态必须被 credits.css 显式隐藏(.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 生产环境即此形态)。
// .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),静止态同样
@@ -2,10 +2,10 @@
* 卡密兑换渠道契约(三个购买弹窗 · 1:1 对齐参考主题)。
*
* 背景:余额卡片顶部的独立「卡密兑换」通用弹窗已删除,兑换入口收敛到
* 三个购买弹窗的支付方式中(对齐参考主题 zibpay_get_payment_methods +
* zibpay_is_allow_card_pass_pay 过滤器):
* 三个购买弹窗的支付方式中(对齐参考实现的支付方式装配 +
* 卡密支付开关过滤器):
* 1) 余额充值弹窗(credits-wallet.php,服务端渲染)
* 2) 购买积分弹窗(credits-balance.php,服务端渲染,Zibll points-pay modal 结构)
* 2) 购买积分弹窗(credits-balance.php,服务端渲染,points-pay 弹窗结构)
* 3) 会员购买/续费弹窗(VipServiceModal JSON 视图模型 + pay-vip.js 渲染)
*
* 后台语义:余额充值 → 卡密兑换 → 三个独立开关
@@ -65,7 +65,7 @@ test('余额充值弹窗:卡密渠道为支付方式之一,卡号/卡密输
assert.match(wallet, /\$charge_card_in_methods/);
});
test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () => {
test('购买积分弹窗:points-pay 弹窗结构 + 卡密渠道', () => {
// 商品/自定义选择 + 支付方式 + hidden + 立即支付。
assert.match(creditsRenderer, /data-jio-points-pay-modal/);
assert.match(creditsRenderer, /data-jio-points-pay-form/);
@@ -114,7 +114,7 @@ test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单
assert.match(vipModal, /'currency_mark' =>/);
// 后端防御:purchase() 拒绝 card_pass(卡密直兑走 redeem)。
assert.match(vipPurchase, /jio_card_pass_redeem_required/);
// 前端:Zibll initiate_pay_input 同构组件——payment-method-radio logo 行、
// 前端:支付方式同构组件——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/);
@@ -128,11 +128,11 @@ test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单
assert.match(payVipJs, /mt6 but jb-red initiate-pay btn-block radius/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|jio_vip_pay_method|jio-vip-submit/);
// 按钮字样对齐参考主题:会员弹窗即 initiate_pay_input(4) 默认「立即支付」;
// 无可用支付方式渲染 Zibll 同款 badg 提示(而非禁用按钮/自实现文案)。
// 无可用支付方式渲染同款 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(jioPay, /'balance'\s+=> __\('余额', 'jio'\)/);
assert.match(jioPay, /'wechatpay'\s+=> __\('微信', 'jio'\)/);
// 显隐:通用 data-controller(==/!=)匹配 + 卡密选中隐藏套餐。
@@ -204,10 +204,10 @@ test('核销订单契约:金额恒为 0,订单名称按 note 生成', () =>
assert.match(userOrders, /永久/);
});
test('多渠道防挤压:图标 flex:none + 行不换行项目收缩成芯片(对齐 Zibll)', () => {
test('多渠道防挤压:图标 flex:none + 行不换行项目收缩成芯片', () => {
// 图标禁用 flex 压缩并抵消 Jio bootstrap 全局 img{max-width:100%}
// (flex 压缩时把图标钳制到被压窄的项宽,实测 5 渠道 400px 弹窗图标
// 被压成 16px 细条)。行保持 Zibll 原样不换行:空间不足时项目收缩为
// 被压成 16px 细条)。行保持不换行:空间不足时项目收缩为
// 「图标上/文字下」芯片(实测 5 渠道一行 5 个、6 渠道一行 6 个,图标
// 恒为固有比例),绝不允许整行提前断行。
const rule = mainCss.match(/\.payment-method-radio img \{[\s\S]*?\n\}/);
@@ -155,7 +155,7 @@ test('every comment-rendering singular loads the owned build bundle', () => {
test('comment markup keeps the 1:1 legacy DOM contract', () => {
const item = read('inc/helpers/comments/item.php');
// 点赞按钮(legacy: zib_get_comment_like — svg + text + count)。
// 点赞按钮:svg + 文字 + 计数。
assert.ok(item.includes("<text></text><count>"), 'like button must render <text> before <count>');
assert.ok(item.includes('jio_get_comment_icon(\'like\''), 'like button must use the sprite icon');
assert.ok(item.includes('jio_get_comment_icon(\'topping\')'), 'topping badge must use the sprite icon');
@@ -15,7 +15,7 @@ function renderFixtures() {
test('term aggregation renders the real SVG use element, total count, and configured first page', () => {
const fixture = renderFixtures();
const card = fixture.aggregation;
assert.match(card, /<svg class="zibsvg-hot" viewbox="0 0 16 16" aria-hidden="true"><use href="#zibsvg-hot"><\/use><\/svg>/);
assert.match(card, /<svg class="jiosvg-hot" viewbox="0 0 16 16" aria-hidden="true"><use href="#jiosvg-hot"><\/use><\/svg>/);
const sanitizer = read('inc/helpers/jio-graphic-card.php');
assert.match(sanitizer, /'svg'\s*=>\s*\['class'\s*=>\s*true,\s*'viewbox'\s*=>\s*true,\s*'aria-hidden'\s*=>\s*true\]/);
assert.match(sanitizer, /'use'\s*=>\s*\['href'\s*=>\s*true,\s*'xlink:href'\s*=>\s*true\]/);
@@ -109,6 +109,6 @@ test('archive filters use the shared ajax-next handler without a category REST/a
assert.equal(fs.existsSync(path.join(root, 'assets', 'js', 'core', 'archive.js')), false);
const widgetCss = read('assets/widgets/frontend.css');
assert.match(widgetCss, /\.zib-widget\.posts-mini-lists\s*\{\s*padding:\s*5px;/);
assert.match(widgetCss, /\.jio-widget-box\.posts-mini-lists\s*\{\s*padding:\s*5px;/);
assert.doesNotMatch(widgetCss, /\.widget-tab-post[^{}]*\{[^}]*!important/s);
});
@@ -79,7 +79,7 @@ function wp_reset_postdata(): void { $GLOBALS['discovery_current_post'] = null;
function jio_is_lazy(string $key, bool $default = false): bool { return false; }
function jio_get_lazy_attr(string $type, string $url, string $class = '', string $lazy = ''): string { return ' class="' . esc_attr($class) . '" src="' . esc_url($url) . '"'; }
function jio_format_number($number): string { return number_format((int) $number); }
function jio_renderer_svg_icon(string $name): string { return '<svg class="zibsvg-' . esc_attr($name) . '" viewbox="0 0 16 16" aria-hidden="true"><use href="#zibsvg-' . esc_attr($name) . '"></use></svg>'; }
function jio_renderer_svg_icon(string $name): string { return '<svg class="jiosvg-' . esc_attr($name) . '" viewbox="0 0 16 16" aria-hidden="true"><use href="#jiosvg-' . esc_attr($name) . '"></use></svg>'; }
function jio_renderer_collect_post_data(array $config = []): array {
$post = $GLOBALS['discovery_current_post'];
return ['post_id' => $post->ID, 'title' => '文章 ' . $post->ID, 'subtitle' => '', 'permalink' => 'https://example.test/post/' . $post->ID, 'time_ago' => '2天前', 'views' => 42, 'comments' => 3];
@@ -146,11 +146,11 @@ test('homepage tabs refresh the original pane DOM through the runtime post_ajax
const homepage = read('inc/helpers/jio-homepage.php')
const controller = read('src/Admin/GlobalController.php')
// legacy zib_index_tab_html 契约:data-toggle="tab" + data-ajax 栏目链接。
// 栏目链接契约:data-toggle="tab" + data-ajax。
assert.match(layout, /<a data-toggle="tab" data-ajax="/)
assert.match(layout, /href="#index-tab-main"/)
assert.match(layout, /class="index-tab affix-header-sm relative home-tab-nav-box mb10" offset-top="-8"/)
// pane 本身是原位刷新容器:zib_index_tab('content') 契约。
// pane 本身是原位刷新容器。
assert.match(layout, /class="posts-row ajaxpager tab-pane fade in active" id="index-tab-main"/)
assert.match(layout, /class="ajaxpager tab-pane fade" id="index-tab-/)
assert.match(layout, /<span class="post_ajax_trigger"><a class="ajax_load ajax-next ajax-open" href="/)
@@ -162,12 +162,12 @@ test('homepage tabs refresh the original pane DOM through the runtime post_ajax
assert.doesNotMatch(layout, /data-home-tab-panel|jio-homepage__panel|jio-homepage__tab|requestHomepagePanel|data-sticky-tabs/)
assert.doesNotMatch(controller, /homepage\/tab-posts|rest_homepage_tab_posts/)
// 排序菜单 = zib_ajax_option_menu('home') 契约:win-ajax-replace="filter" + ajax-next 链接。
// 排序菜单契约:win-ajax-replace="filter" + ajax-next 链接。
assert.match(homepage, /function jio_homepage_render_ajax_option_menu/)
assert.match(homepage, /win-ajax-replace="filter"/)
assert.match(homepage, /ajax-replace="true" class="' \. esc_attr\(\$_class\)/)
assert.match(homepage, /!empty\(\$_GET\['nofilter'\]\)/)
// 主查询吃 ?orderby=(legacy zib_sift_posts_per_orde),排序原位刷新才有效。
// 主查询吃 ?orderby=,排序原位刷新才有效。
assert.match(homepage, /function jio_homepage_get_request_sort/)
assert.match(read('inc/helpers/homepage/content.php'), /jio_homepage_get_request_sort\(\)/)
@@ -198,9 +198,9 @@ test('modal fragment lifecycle reruns auto_fun after loaded.bs.modal', () => {
assert.match(source, /loaded\.bs\.modal[\s\S]*?auto_fun/);
});
test('JSON success lifecycle remains zib_ajax.success', () => {
test('JSON success lifecycle remains jio_ajax.success', () => {
const source = read('assets/js/runtime/main.js');
assert.match(source, /zib_ajax\.success/);
assert.match(source, /jio_ajax\.success/);
});
test('Miniimgbox owns data-imgbox and lazySizes has one integration owner', () => {
@@ -53,10 +53,10 @@ test('widget icon cards restore the reference surface height without compact ove
assert.match(
widgetCss,
/\.zib-widget\s*\{[^}]*margin-bottom:\s*20px[^}]*padding:\s*15px[^}]*box-shadow:\s*0 0 10px/s,
/\.jio-widget-box\s*\{[^}]*margin-bottom:\s*20px[^}]*padding:\s*15px[^}]*box-shadow:\s*0 0 10px/s,
);
assert.match(widgetCss, /\.zib-widget\.padding-10\s*\{\s*padding:\s*10px/);
assert.match(widgetCss, /\.zib-widget\.nobottom\.notop\s*\{[^}]*padding-top:\s*0[^}]*padding-bottom:\s*0/s);
assert.match(widgetCss, /\.jio-widget-box\.padding-10\s*\{\s*padding:\s*10px/);
assert.match(widgetCss, /\.jio-widget-box\.nobottom\.notop\s*\{[^}]*padding-top:\s*0[^}]*padding-bottom:\s*0/s);
assert.doesNotMatch(widgetCss, /\.row\.gutters-5/);
assert.doesNotMatch(widgetCss, /\.icon-card\s*,\s*\.icon-cover-card\s*\{\s*font-size:\s*12px/);
assert.doesNotMatch(widgetCss, /\.icon-cover-card\s*\{[^}]*padding-(?:top|bottom)/s);
@@ -70,7 +70,7 @@ test('header icon-card defaults use bundled Font Awesome icons', () => {
];
for (const source of defaults) {
assert.doesNotMatch(source, /'icon'\s*=>\s*'zibsvg-/);
assert.doesNotMatch(source, /'icon'\s*=>\s*'jiosvg-/);
for (const icon of ['gem', 'star', 'fire', 'wrench']) {
assert.match(source, new RegExp(`'icon'\\s*=>\\s*'fa fas fa-${icon}'`));
}
@@ -85,15 +85,15 @@ test('icon card renderers route every picker format through the shared icon rend
assert.match(helper, /if \(function_exists\('jio_icon_markup'\)\) \{\s*return jio_icon_markup\(\$value, \$extra\);/s);
// Both renderers must delegate: emitting a bare FA class string produced
// `<i class="fa fas fa-zibsvg-hot">`, i.e. an invisible glyph, for the two
// `<i class="fa fas fa-jiosvg-hot">`, i.e. an invisible glyph, for the two
// SVG formats the admin picker can store.
assert.match(helper, /\$icon_inner = jio_card_icon_inner\(\$icon_fa, 'em09'\);/);
assert.match(helper, /\$icon_inner = jio_card_icon_inner\(\$icon_fa\);/);
// The picker prefixes must drive the SVG branches, and the stale
// `jiosvg-` prefix (never emitted by any picker) must be gone.
assert.match(helper, /str_starts_with\(\$value, 'zibsvg-'\) \|\| str_starts_with\(\$value, 'customsvg-'\)/);
assert.doesNotMatch(helper, /jiosvg-/);
// `zibsvg-` prefix (never emitted by any picker) must be gone.
assert.match(helper, /str_starts_with\(\$value, 'jiosvg-'\) \|\| str_starts_with\(\$value, 'customsvg-'\)/);
assert.doesNotMatch(helper, /zibsvg-/);
assert.doesNotMatch(helper, /'fa fas fa-'\s*\.\s*\$icon_fa/);
assert.doesNotMatch(helper, /<i class="'\s*\.\s*esc_attr\(\$icon_fa\)\s*\.\s*' em09"/);
});
@@ -50,7 +50,7 @@ test('widget and nav-menu icon fields render the shared picker', () => {
test('picker JS composes the three value formats and loads the REST catalog', () => {
const js = read('assets/js/admin-icon-picker.js');
for (const token of ['X-WP-Nonce', 'customsvg-', 'zibsvg-', '`fa ${group.id} fa-${name}`', 'data-jio-icon-picker', 'jio-icon-picker__value']) {
for (const token of ['X-WP-Nonce', 'customsvg-', 'jiosvg-', '`fa ${group.id} fa-${name}`', 'data-jio-icon-picker', 'jio-icon-picker__value']) {
assert.match(js, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.match(js, /DOMParser/, 'custom SVG is sanitized client side');
@@ -132,7 +132,7 @@ test('custom settings pages and standalone save paths speak the three icon forma
test('nav menu icons render every format without legacy fa-prefix mangling', () => {
const helper = read('inc/helpers/menu-decoration.php');
assert.match(helper, /str_starts_with\(\$icon_class, 'zibsvg-'\)/);
assert.match(helper, /str_starts_with\(\$icon_class, 'jiosvg-'\)/);
assert.match(helper, /jio_icon_markup\(\$icon_class, 'icon mr3 menu-item-icon'\)/);
assert.doesNotMatch(helper, /'<i class="' \. esc_attr\(\$icon_class\)/, 'no raw i-tag bypassing the unified helper');
});
@@ -149,7 +149,7 @@ test('icon markup upgrades legacy FA4 values to FA5 and never emits the FA4 form
assert.match(helper, /preg_replace\('\/\^fa\\s\+fa-\/i', '', \(string\) \$name\)/, 'icon keys normalize the legacy FA4 form');
});
test('brand icons render despite the zibll-legacy fa-fab double prefix', () => {
test('brand icons render despite the legacy fa-fab double prefix', () => {
const rule = /\.fa\.fab\{font-family:"Font Awesome 5 Brands";font-weight:400\}/;
for (const file of ['assets/css/main.css', 'assets/css/admin-icon-picker.css', 'assets/admin/src/style.css']) {
assert.match(read(file), rule, `${file} re-asserts the brand family`);
@@ -119,10 +119,10 @@ test('admin operations run through admin_post handlers with capability and nonce
assert.match(service, /min\(50, max\(4, \(int\) \(\$args\['code_length'\] \?\? self::CODE_LENGTH\)\)\)/)
})
test('import accepts Zibll-compatible lines with per-code rewards', () => {
test('import accepts the documented line format with per-code rewards', () => {
const service = read('src', 'Payment', 'InviteCode.php')
assert.match(service, /public function import_codes\(array \$args\)/)
// Zibll 行格式:`邀请码<分隔符>奖励<分隔符>标识`,奖励参数串键位兼容。
// 行格式:`邀请码<分隔符>奖励<分隔符>标识`,奖励参数串键位兼容。
assert.match(service, /level_integral/)
assert.match(service, /vip_time/)
assert.match(service, /in_array\(\$reward, \['无', 'none', '0'\]/)
@@ -6,9 +6,9 @@ const path = require('node:path');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('modern widget schemas expose Zibll field names and defaults', () => {
test('modern widget schemas expose reference field names and defaults', () => {
const source = read('src/WidgetSystem/WidgetSchemas.php');
for (const field of ['mini_opt', 'paginate', 'tabs', "'id' => 'cat'", "'id' => 'zibpay_type'", 'layout_ratio', 'slide_scale', 'm_layout_type', 'm_cover_ratio', 'image_layer', 'image_select']) {
for (const field of ['mini_opt', 'paginate', 'tabs', "'id' => 'cat'", "'id' => 'pay_type'", 'layout_ratio', 'slide_scale', 'm_layout_type', 'm_cover_ratio', 'image_layer', 'image_select']) {
assert.match(source, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.doesNotMatch(source, /'id' => 'topics'/);
@@ -22,7 +22,7 @@ test('tab and slider renderers retain reference DOM contracts', () => {
const slider = read('src/WidgetSystem/Renderers/SliderRenderer.php');
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'data-widget-lazy', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.doesNotMatch(content, /post_ajax_trigger|ias-btn|data-widget-ias/);
for (const token of ['relative zib-slider', 'new-swiper slide-widget', 'swiper-wrapper', 'swiper-slide', 'slide-layer', 'data-swiper-parallax', 'swiper-button-prev', 'swiper-pagination', 'widget-slider-graphic', 'slider-col', 'cover-col', 'widget-graphic-cover']) assert.match(slider, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
for (const token of ['relative jio-slider-box', 'new-swiper slide-widget', 'swiper-wrapper', 'swiper-slide', 'slide-layer', 'data-swiper-parallax', 'swiper-button-prev', 'swiper-pagination', 'widget-slider-graphic', 'slider-col', 'cover-col', 'widget-graphic-cover']) assert.match(slider, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
test('widget skeleton lives inside the AJAX container so the REST client can find it', () => {
@@ -67,7 +67,7 @@ test('reference slider CSS includes responsive height and combo selectors', () =
for (const token of ['--m-cover-ratio', '--cover-ratio', '--layout-ratio']) assert.match(mainCss + css, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
test('icon card widgets expose Zibll vertical and horizontal contracts', () => {
test('icon card widgets expose reference vertical and horizontal contracts', () => {
const schemas = read('src/WidgetSystem/WidgetSchemas.php');
const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const helper = read('inc/helpers/jio-icon-cards.php');
@@ -88,7 +88,7 @@ test('icon card widgets expose Zibll vertical and horizontal contracts', () => {
assert.doesNotMatch(helper, /wp_kses_post\(\$desc\)/);
});
test('button group uses the complete Zibll schema, DOM and responsive sizing contract', () => {
test('button group uses the complete schema, DOM and responsive sizing contract', () => {
const schema = read('src/WidgetSystem/WidgetSchemas.php');
const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const css = read('assets/widgets/frontend.css');
@@ -105,7 +105,7 @@ test('button group uses the complete Zibll schema, DOM and responsive sizing con
assert.match(css, /\.widget-buttons\.m-size-medium \.but\s*\{\s*padding:\s*8px 16px/);
});
test('vertical notice supports only custom and article sources with Zibll swiper sizing', () => {
test('vertical notice supports only custom and article sources with reference swiper sizing', () => {
const schema = read('src/WidgetSystem/WidgetSchemas.php');
const renderer = read('src/WidgetSystem/Renderers/GeneralRenderer.php');
const postQuery = read('src/WidgetSystem/PostQuery.php');
@@ -175,7 +175,7 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
})
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
test('user card VIP CTA matches the reference header VIP icon', () => {
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'jio'\)/)
@@ -190,7 +190,7 @@ test('VIP purchase modal matches legacy desktop-center and mobile-bottom positio
assert.doesNotMatch(purchaseModal, /mobileBottom:\s*false/)
assert.match(modalRuntimeJs, /options\.mobileBottom !== false && isMobile\(\)/)
// 壳几何 1:1 对齐 Zibll modal_pay_uservip:桌面 600px(bootstrap3 ≥768px
// 壳几何 1:1:桌面 600px(bootstrap3 ≥768px
// .modal-dialog)+ max-width calc(100% - 20px)(.modal.flex .modal-dialog);
// 高度/滚动 = .modal.flex .modal-content 的 max-height calc(100vh - 20px)
// + overflow-y auto(阈值放宽到整屏,正常内容不出现滚动条);
@@ -301,13 +301,13 @@ test('header multifunction search keeps focus and submit alignment route-indepen
test('user profile and author widgets render the reference user-card contract', () => {
assert.match(frontendRenderer, /in_array\(\$definition->slug\(\), \['user_card', 'author_card'\], true\)/)
assert.match(frontendRenderer, /self::render_reference_widget\(\$definition, \$instance, \$widget, \$content\)/)
assert.match(frontendRenderer, /\$classes = \['zib-widget-wrap', 'jio-'/)
assert.match(frontendRenderer, /\$classes = \['jio-widget-box-wrap', 'jio-'/)
assert.match(frontendRenderer, /\$classes\[\] = 'jio-user-widget-wrap'/)
assert.match(frontendRenderer, /echo '<div class="widget-container"><div>';/)
assert.match(frontendRenderer, /\$content_classes = \['widget-content'\]/)
// 登录卡片与 reference zib_get_user_card_box 同构:封面、头像框、名字行、统计徽章、简介、按钮。
// 登录卡片与参考实现的用户卡同构:封面、头像框、名字行、统计徽章、简介、按钮。
assert.match(userRenderer, /<div class="mb20">/)
assert.match(userRenderer, /user-card zib-widget widget/)
assert.match(userRenderer, /user-card jio-widget-box widget/)
assert.match(userRenderer, /user-cover graphic" style="padding-bottom: 50%;"/)
assert.match(userRenderer, /avatar-img avatar-lg/)
assert.match(userRenderer, /user-name flex jc/)
@@ -339,7 +339,7 @@ test('user profile and author widgets render the reference user-card contract',
assert.match(userRenderer, /loged_title/)
assert.doesNotMatch(userRenderer, /user-card__auth-btns|render_social_buttons\(/)
// 后加载的 widget bundle 明确保留单层卡片并让封面贴边。
assert.match(widgetCss, /\.user-card\.zib-widget\s*\{[^}]*padding:\s*0;[^}]*overflow:\s*hidden;/s)
assert.match(widgetCss, /\.user-card\.jio-widget-box\s*\{[^}]*padding:\s*0;[^}]*overflow:\s*hidden;/s)
assert.match(legacyMainCss, /\.user-card\.widget \.user-cover \+ \.card-content\s*\{[^}]*margin-top:\s*-55px;/s)
// 参考主题契约:.widget-content 是无样式结构层,小工具卡片自带内边距;
// 任何样式层都不得给 .widget-content 加 padding,否则小工具与内容列宽之间出现左右空隙。
@@ -426,7 +426,7 @@ test('header user-card membership stays on one row with a date-only expiry', ()
// The CTA wrapper is the reference padding wrapper, not a bespoke hook.
assert.match(headerUserbox, /'<div class="mt10 em09" style="padding:2px;">'/)
assert.doesNotMatch(headerUserbox, /jio-header-payvip/)
// Expiry badge follows zib_get_user_vip_exp_date_text(): an expiry that has
// Expiry badge follows the reference expiry text: an expiry that has
// passed reads 会员已过期, a live one reads the bare date, 永久会员 is reserved
// for an explicit permanent membership, and no expiry data renders no badge.
assert.match(headerUserbox, /\$expires <= time\(\)/)
@@ -8,15 +8,15 @@ const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
test('paid article and sidebar widget use the purchase-card DOM', () => {
const pay = read('inc/helpers/jio-pay.php');
const widget = read('src/WidgetSystem/Renderers/ContentRenderer.php');
const shell = read('src/WidgetSystem/FrontendRenderer.php');
const single = read('inc/helpers/jio-single-render.php');
const hooks = read('inc/core-hooks.php');
assert.match(pay, /zib-widget pay-box pay-widget/);
assert.match(pay, /zib-widget pay-box order-type-/);
assert.match(pay, /jio-widget-box pay-box pay-widget/);
assert.match(pay, /jio-widget-box pay-box order-type-/);
assert.match(pay, /flex pay-flexbox/);
assert.match(pay, /hide-sm pay-thumb/);
assert.match(pay, /pay-tag abs-center/);
@@ -38,7 +38,7 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
assert.doesNotMatch(pay, /jio_get_user_center_tab_url\('vip'\)/);
assert.match(pay, /cashier-link/);
assert.match(pay, /function jio_get_paid_box_html/);
assert.match(pay, /pay-box zib-widget paid-box order-type-/);
assert.match(pay, /pay-box jio-widget-box paid-box order-type-/);
assert.match(pay, /flex ac jb-green padding-10 em09/);
assert.match(pay, /订单号/);
assert.doesNotMatch(pay, /'流水号'/);
@@ -65,7 +65,7 @@ test('the purchase card owns one direct stylesheet and no retired paywall path',
assert.match(assets, /assets\/css\/frontend\/pay-box\.css|\'pay-box\'/);
assert.match(shortcodes, /return jio_get_paywall_html\(\$post, \['message' => \$message\]\);/);
assert.match(css, /\.zib-widget\.pay-box/);
assert.match(css, /\.jio-widget-box\.pay-box/);
assert.match(css, /\.pay-box \.pay-vip-level-icon/);
assert.match(css, /\.pay-box \.pay-vip-exclusive/);
assert.doesNotMatch(css, /@media/);
@@ -88,7 +88,7 @@ test('基础渠道列表与卡密渠道都经过同一排序实现', () => {
test('会员弹窗不再保留重复的支付方式列表回退分支', () => {
assert.doesNotMatch(vipPackages, /class_exists\('\\\\Jio\\\\Payment\\\\Gateway'\)/);
// 卡密渠道注入契约保持(对齐参考主题 zibpay_get_payment_methods)。
// 卡密渠道注入契约保持。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'jio'\)/);
});
@@ -49,7 +49,7 @@ test('front submit follows the reference newposts shell and cards', () => {
assert.match(layout, /if \(is_page_template\('templates\/pages\/page-submit\.php'\)\) \{\s*return true;/);
assert.equal((page.match(/class="sidebar show-sidebar"/g) || []).length, 1);
assert.match(page, /'jio-submit',[\s\S]*?\['jio-public'\]/);
assert.match(main, /zib-widget full-widget-sm editor-main-box/);
assert.match(main, /jio-widget-box full-widget-sm editor-main-box/);
assert.match(main, /relative newposts-title/);
assert.match(terms, /newpost-cat-drop drop-select dropdown dependency-box/);
assert.match(terms, /data-toggle-class="open" data-target="\.newpost-cat-drop"/);
@@ -59,7 +59,7 @@ test('front submit follows the reference newposts shell and cards', () => {
assert.doesNotMatch(submitCss, /\.term-select-box \.dropdown-menu|\.term-select-box \.drop-select\s*{/);
assert.match(mainCss, /\.drop-select \.dropdown-menu\s*{[^}]*box-shadow:\s*0 0 0 8000px/);
assert.match(mainCss, /\.open > \.dropdown-menu\s*{[^}]*visibility:\s*unset/);
assert.match(pay, /zib-widget dependency-box mb10-sm newpost-pay-box/);
assert.match(pay, /jio-widget-box dependency-box mb10-sm newpost-pay-box/);
assert.match(pay, /pay-dawnload-edit/);
assert.match(submit, /separator muted-3-color theme-box/);
assert.match(submit, /new-posts-submit/);
@@ -175,9 +175,10 @@ test('quick-edit save is guarded by screen, autosave, revision and capability',
assert.match(list, /\$post->post_type !== 'post'/)
})
test('no legacy zib-style field names or topic links survive', () => {
test('no legacy upstream field names or topic links survive', () => {
assert.doesNotMatch(list, /zib_bulk_edit/)
assert.doesNotMatch(list, /posts_zibpay/)
// 商品类型筛选字段的规范名是 pay_type,zibpay_type 是历史名。
assert.doesNotMatch(list, /zibpay_type/)
assert.doesNotMatch(list, /pay_cuont/)
// 本项目不使用「自动售卡」类目。
@@ -242,7 +242,7 @@ test('related posts, author page and widgets use their dedicated renderers', ()
assert.match(related, /<li><a class="icon-circle"/);
assert.match(related, /\['category', 'topics', 'post_tag'\]/);
assert.match(related, /'terms'\s+=>\s+array_map\('intval', \(array\) \$term_ids\)/);
assert.match(related, /class=\"zib-widget\"/);
assert.match(related, /class=\"jio-widget-box\"/);
assert.doesNotMatch(related, /post_related_source/);
assert.doesNotMatch(related, /post__not_in/);
assert.match(related, /\$args\['meta_key'\]\s*=\s*'jio_post_views'/);
@@ -321,7 +321,7 @@ test('after-box author card mirrors legacy user-card structure', () => {
assert.match(userCard, /swiper-container more-posts swiper-scroll/);
assert.match(userCard, /'class'\s+=>\s+'em09', 'cut'\s+=>\s+32/);
assert.doesNotMatch(userCard, /jio-more-post-card/);
assert.match(css, /^\.zib-widget \{/m);
assert.match(css, /^\.jio-widget-box \{/m);
assert.doesNotMatch(css, /relates-thumb \.graphic\.style-3 \.graphic-img/);
assert.doesNotMatch(authorActions, /\.user-card-author|\.user-card \.author|user-card-posts/);
assert.match(identity, /function jio_get_user_follow\(/);
@@ -354,7 +354,7 @@ test('AJAX pagination can recover, caps auto loading and rehydrates appended med
assert.doesNotMatch(php, /next-page ajax-next/);
assert.match(php, /lazyload-action="ias"/);
assert.match(js, /lazybeforeunveil/);
// 1:1 对齐参考主题 zib_paging():.ajax-pag 内只渲染翻页控件,不预渲染 loader。
// .ajax-pag 内只渲染翻页控件,不预渲染 loader。
// 预渲染的 loader 会被 runtime post_ajax() 复用并丢弃动态骨架,导致排序切换
// 永远是写死 2 条 list 骨架。骨架改由 pagination.js 在请求时按视口创建。
assert.doesNotMatch(php, /post_ajax_loader/);
@@ -391,8 +391,8 @@ test('front-end carousel CSS is conditional on carousel block content', () => {
});
test('the pinned visual baseline records all source checksums', () => {
const baseline = JSON.parse(read('tests/visual/zibll-9.0-article-baseline.json'));
assert.equal(baseline.theme, 'Zibll');
const baseline = JSON.parse(read('tests/visual/article-baseline.json'));
assert.equal(baseline.theme, 'reference');
assert.equal(baseline.version, '9.0');
const files = Object.values(baseline.files);
assert.ok(files.length >= 6);
@@ -868,7 +868,7 @@ test('content max width is one contract from admin option to runtime var', () =>
const themeSearch = read('src/Settings/ThemeSearch.php');
const builder = read('src/Frontend/InlineStyleBuilder.php');
// Free number input (zibll layout_max_width parity): UI range, REST validation
// Free number input: UI range, REST validation
// and runtime clamp must agree, or saves are rejected / silently clamped.
const vueField = layoutSchema.match(/key: 'container_max_width'[^\n]*/)[0];
assert.match(vueField, /type: 'number'/);
@@ -19,7 +19,7 @@ for (const style of ['list', 'card', 'mini']) {
assert.match(html, /data-widget-page="2"/);
assert.match(html, /class="post_ajax_loader"/);
assert.doesNotMatch(html, /post_ajax_loader is-loading|class="posts-row|ajax-next/);
if (style === 'mini') assert.match(html, /class="zib-widget posts-mini-lists"/);
if (style === 'mini') assert.match(html, /class="jio-widget-box posts-mini-lists"/);
});
test(`article ${style} widget reserves visible skeletons for lazy loading`, () => {
@@ -18,7 +18,7 @@ test('article TOC widget uses the shared reference tree styles, not posts-navs p
'.posts-nav-lists li.active::before',
]) assert.ok(mainCss.includes(selector), `main.css retains reference tree rule ${selector}`);
assert.match(mainCss, /\.posts-nav-box\s*\{[^}]*font-family:\s*inherit;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;/s);
const panel = mainCss.match(/\.posts-nav-box \.zib-widget\s*\{([^}]*)\}/)?.[1] || '';
const panel = mainCss.match(/\.posts-nav-box \.jio-widget-box\s*\{([^}]*)\}/)?.[1] || '';
for (const declaration of [
/background-color:\s*var\(--main-bg-color\)/,
/padding:\s*15px/,
@@ -31,7 +31,7 @@ test('article TOC widget uses the shared reference tree styles, not posts-navs p
test('mobile TOC panel and its collapse control remain styled and accessible', () => {
const mainCss = read('assets/css/main.css');
assert.match(mainCss, /\.mobile-navbar \.posts-nav-box \.zib-widget\s*\{[^}]*background:\s*var\(--muted-border-color\);[^}]*box-shadow:\s*none;/s);
assert.match(mainCss, /\.mobile-navbar \.posts-nav-box \.jio-widget-box\s*\{[^}]*background:\s*var\(--muted-border-color\);[^}]*box-shadow:\s*none;/s);
assert.match(mainCss, /\.posts-nav-lists \.nav-toggle-collapse\s*\{[^}]*border:\s*0;[^}]*background:\s*transparent;/s);
assert.match(mainCss, /\.posts-nav-box \.posts-nav-lists :where\(a, \.nav-toggle-collapse\):focus-visible/);
assert.match(mainCss, /@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.posts-nav-lists \.nav-toggle-collapse \{\s*transition:\s*none;/);
@@ -12,7 +12,7 @@ test('article list uses the reference widget container and keeps widget behavior
const renderer = read('src/WidgetSystem/FrontendRenderer.php');
const catalog = read('src/WidgetSystem/WidgetCatalog.php');
assert.match(renderer, /\['user_card', 'author_card', 'tab_posts', 'posts'\]/);
assert.match(renderer, /\$classes = \['zib-widget-wrap', 'jio-' \. str_replace/);
assert.match(renderer, /\$classes = \['jio-widget-box-wrap', 'jio-' \. str_replace/);
assert.match(renderer, /echo '<div class="widget-container"><div>'/);
assert.match(renderer, /WidgetVisibility::device_class\(\$instance\)/);
assert.match(renderer, /!empty\(\$instance\['sidebar_affix'\]\)/);
@@ -156,7 +156,7 @@ test('poster canvas layers match the reference rebate branch 1:1', () => {
assert.match(body, /'推荐人: ' \+ String\(referrerName\)/)
assert.match(body, /drawTextOverflow\(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight \+ 162\)/)
// 站点名走「周」前缀(参考主题 zib__('week_prefix'))。
// 站点名走「周」前缀。
assert.match(body, /'周' \+ '日一二三四五六'\.charAt\(new Date\(\)\.getDay\(\)\)/)
assert.doesNotMatch(body, /\['星期日', '星期一'/)
@@ -201,12 +201,12 @@ test('governance display state is a single source shared by every prompt surface
})
test('ban prompt surface mirrors the reference theme modals and block page', () => {
// 查看禁封状态弹窗(1:1 zib_get_ban_info_modal:多彩头 + 信息行 + 申诉按钮)。
// 查看禁封状态弹窗:多彩头 + 信息行 + 申诉按钮。
assert.match(userBanHelper, /function jio_get_user_ban_info_modal/)
assert.match(userBanHelper, /jio-user-ban-info-modal/)
assert.match(userBanHelper, /您已解封,请刷新页面/)
assert.match(userBanHelper, /您当前已无法进行申诉/)
// 禁封申诉弹窗(1:1 zib_get_user_ban_appeal_modal:edit-header + 信息框 +
// 禁封申诉弹窗:edit-header + 信息框 +
// 资料项 inputs + 审核中态 + 禁止申诉态;仅本人)。
assert.match(userBanHelper, /function jio_get_user_ban_appeal_modal/)
assert.match(userBanHelper, /jio-user-ban-appeal-modal/)
@@ -236,13 +236,13 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
// 申诉表单 data[key] 结构与后端兼容。
assert.match(banAppeal, /is_array\(\$_POST\['data'\]\)/)
assert.match(banAppeal, /申诉说明/)
// 禁封记录弹窗 + 记录链接(1:1 zib_get_ban_log_modal / zib_get_user_ban_log_link)。
// 禁封记录弹窗 + 记录链接。
assert.match(userBanHelper, /function jio_get_user_ban_log_modal/)
assert.match(userBanHelper, /function jio_get_user_ban_log_link/)
assert.match(userBanHelper, /data-jio-ban-log=/)
assert.match(userBanHelper, /当前用户已有%d次禁封记录,请谨慎操作/)
assert.match(userBanHelper, /该用户共有%d次禁封记录/)
// 登录后禁封状态页(1:1 zib_user_ban_restrict_login / zib_die_page)。
// 登录后禁封状态页。
assert.match(userBanHelper, /function jio_render_user_ban_block_page/)
assert.match(banBlock, /抱歉,您的账号当前处于禁封状态,暂时无法登录/)
assert.match(banBlock, /退出登录/)
@@ -254,7 +254,7 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
assert.match(restriction, /add_action\('template_redirect', \[\$this, 'restrict_frontend_access'\], 1\)/)
assert.match(restriction, /public function restrict_frontend_access\(\): void/)
assert.ok(!restriction.includes('enforce_login_ban'), 'login-time ban block must stay removed')
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
// 评论拒绝态。
assert.match(userBanHelper, /function jio_get_user_ban_nocan_html/)
assert.match(commentsHelper, /jio_get_user_ban_nocan_html/)
assert.match(commentsHelper, /暂时无法发表评论/)
@@ -287,7 +287,7 @@ test('ban prompt surface mirrors the reference theme modals and block page', ()
})
test('report entry hides targets already banned or suspended', () => {
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
// 已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
assert.match(userBanHelper, /function jio_is_user_ban_restricted/)
assert.match(commentItem, /jio_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /jio_is_user_ban_restricted\(\$post_author_id\)/)
@@ -448,7 +448,7 @@ test('comments expose a report entry for other users when reporting is enabled',
assert.doesNotMatch(commentItem, /\$report\s*=\s*jio_get_comment_report_html\(\$comment\)/)
})
test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal 1:1', () => {
test('frontend ban modal mirrors the reference ban modal 1:1', () => {
// 禁封方式/禁封时间/预置原因徽章/其它原因/禁封说明/删除全部内容/禁止用户申诉,
// 以及已禁封用户的状态信息与解封留言表单(弹窗经 REST /user-ban/modal 输出)。
for (const label of ['拉入小黑屋', '禁封用户', '禁封方式', '禁封时间', '选择禁封原因', '其它原因', '禁封说明', '删除全部内容', '禁止用户申诉', '请填写禁止申诉说明', '解封用户', '移出小黑屋', '给用户的留言', '开始时间', '结束时间', '原因', '说明', '禁封记录', '确认禁封']) {
@@ -474,7 +474,7 @@ test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal
assert.match(userBanHelper, /function jio_get_user_cover_menu_action_html/)
assert.match(userBanHelper, /function jio_get_comment_ban_or_report_item/)
assert.match(userBanHelper, /function jio_get_post_author_ban_item/)
// 权限模型(参考主题 zib_current_user_can('set_user_ban') 语义):
// 权限模型:
// manage_options + 非本人 + 非超级管理员 + 功能开关。
assert.match(userBanHelper, /current_user_can\('manage_options'\)/)
assert.match(userBanHelper, /\$current_id === \$user_id/)
@@ -483,7 +483,7 @@ test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal
// REST 路由:modal 拉取 + set 提交(permission_callback 校验)。
assert.match(userController, /'\/user-ban\/modal'/)
assert.match(userController, /'\/user-ban\/set'/)
// 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...))。
// 解封视图多彩横幅。
assert.match(userBanHelper, /modal-colorful-header colorful-bg c-yellow/)
// 提交后整页刷新(禁封 reload:true)/ 仅关闭弹窗(解封 hide_modal:true)。
assert.match(userBanJs, /window\.location\.reload\(\)/)
+5 -5
View File
@@ -122,10 +122,10 @@ final class WidgetSystemTest {
$generic_categories = PostQuery::arguments(['category_ids' => '2,5', 'category_id' => 9], 1);
$this->same([9], $generic_categories['category__in'], 'generic category_id continues to override category_ids');
$this->false(isset($generic_categories['cat']), 'generic categories do not switch to modern cat syntax');
$paywalled = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['1', '6'], 'paginate' => ''], 1, ['cat' => '1']);
$paywalled = PostQuery::arguments(['cat' => '1', 'pay_type' => ['1', '6'], 'paginate' => ''], 1, ['cat' => '1']);
$this->contains('reading', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters map to pay meta');
$this->contains('video', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters cover every selected type');
$top_level_paywall = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['2'], 'paginate' => 'ajax'], 1);
$top_level_paywall = PostQuery::arguments(['cat' => '1', 'pay_type' => ['2'], 'paginate' => 'ajax'], 1);
$this->contains('download', json_encode($top_level_paywall['meta_query'] ?? []), 'top-level reference commerce filters map to pay meta');
$generic_paywall = PostQuery::arguments(['category_ids' => '1', 'pay_type' => 'gallery'], 1);
$this->contains('gallery', json_encode($generic_paywall['meta_query'] ?? []), 'generic widget pay_type behavior remains available');
@@ -144,12 +144,12 @@ final class WidgetSystemTest {
$this->same(6, $tab_defaults['count'], 'modern tabs default to six posts');
$this->same(['热门推荐', '最近更新', '猜你喜欢'], array_column($tab_defaults['tabs'], 'title'), 'modern tab defaults match reference ordering');
$tab = FieldSchema::normalize($tab_fields, ['tabs' => [[
'title' => '<b>栏目</b><script>x</script>', 'cat' => '1,-2 3', 'zibpay_type' => ['1', '6'],
'title' => '<b>栏目</b><script>x</script>', 'cat' => '1,-2 3', 'pay_type' => ['1', '6'],
'order' => 'asc',
]]]);
$this->same('1,-2,3', $tab['tabs'][0]['cat'], 'tab cat keeps signed reference IDs');
$this->false(isset($tab['tabs'][0]['topics']), 'topic filters are gone from tab configuration');
$this->same(['1', '6'], $tab['tabs'][0]['zibpay_type'], 'tab commerce filters are preserved');
$this->same(['1', '6'], $tab['tabs'][0]['pay_type'], 'tab commerce filters are preserved');
$this->contains('<b>栏目</b>', $tab['tabs'][0]['title'], 'tab title keeps controlled HTML');
$this->false(str_contains($tab['tabs'][0]['title'], '<script>'), 'tab title strips unsafe HTML');
@@ -211,7 +211,7 @@ final class WidgetSystemTest {
$notice_defaults = FieldSchema::defaults(WidgetSchemas::for('notice'));
$this->same(3, $notice_defaults['interval'], 'notice keeps reference interval');
$this->same('c-blue', $notice_defaults['color'], 'notice keeps reference color');
$this->same(2, count($notice_defaults['img_ids']), 'notice ships reference custom messages');
$this->same(0, count($notice_defaults['img_ids']), 'notice ships no preset messages');
$notice = GeneralRenderer::render('notice', $notice_defaults);
$this->contains('swiper-bulletin mb20 c-blue main-radius', $notice, 'notice emits reference surface classes');
$this->contains('data-direction="vertical" data-loop="true" data-autoplay="1"', $notice, 'notice emits reference swiper options');
@@ -7,7 +7,7 @@ const { spawnSync } = require('node:child_process');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('search route keeps the reference Zibll results and empty-state structure', () => {
test('search route keeps the reference results and empty-state structure', () => {
const template = read('search.php');
const styles = read('assets/css/main.css');
@@ -182,7 +182,7 @@ test('renders reference list shape for three real headings and excludes no-nav a
assert.equal(list.querySelectorAll('li').length, 3);
assert.deepEqual(list.querySelectorAll('a').map((link) => link.textContent), ['Main section', 'Nested section', 'Final section']);
assert.equal(box.querySelector('.title-theme').textContent, '目录');
assert.ok(box.querySelector('.zib-widget'));
assert.ok(box.querySelector('.jio-widget-box'));
headings.forEach((heading) => assert.match(heading.id, /^wznav_/));
});
@@ -1,9 +1,9 @@
'use strict';
// 文章详情页顶部封面高度契约(对齐参考主题):
// 单图/多图(轮播)封面标题叠加封面,高度 = calc(比例% + 50px)(zibll .single-cover /
// 单图/多图(轮播)封面标题叠加封面,高度 = calc(比例% + 50px)(.single-cover /
// .single-cover-slide);视频封面标题在下方 footer、无叠加,高度 = 裸比例%
// (zibll .dplayer-scale-height)。
// (视频封面高度计算契约)。
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -36,8 +36,7 @@ test('single cover: video mode uses the bare ratio height like the reference dpl
/single-cover--video imgbox-container" style="padding-bottom:<\?php echo esc_attr\(\$video_padding\); \?>%;"/,
'视频封面必须输出裸比例高度(标题在下方 footer,无叠加补偿)'
);
// +50px 是标题叠加封面的补偿量,仅单图/多图封面适用;视频封面
// 对齐参考主题 .dplayer-scale-height,不得继承该公式。
// +50px 是标题叠加封面的补偿量,仅单图/多图封面适用;视频封面不得继承该公式。
assert.doesNotMatch(
singleRender,
/single-cover--video imgbox-container" style="padding-bottom:calc\(/,
@@ -140,7 +139,7 @@ test('video cover: footer breadcrumb sits in the reference flex row', () => {
const breadcrumbCss = read('assets/css/frontend/breadcrumb.css');
const coverCss = read('assets/css/frontend/single-post/cover.css');
// 对齐 zibll:footer 里面包屑直接位于 flex jsb at 行,无自造包装层。
// footer 里面包屑直接位于 flex jsb at 行,无自造包装层。
assert.match(
singleRender,
/single-video-footer dplayer-featured">\s*\n\s*<div class="flex jsb at">\s*\n\s*<\?php echo \$crumbs;/,
@@ -17,7 +17,7 @@ test('missing and invalid tab style use the reference mini layout and ignore glo
for (const name of ['default', 'invalid']) {
const current = cases[name];
assert.equal(current.instance_style, 'mini');
assert.match(current.tab_markup, /widget-tab-post style-mini posts-mini-lists zib-widget/);
assert.match(current.tab_markup, /widget-tab-post style-mini posts-mini-lists jio-widget-box/);
assert.match(current.first, /class="posts-mini ajax-item"/);
assert.doesNotMatch(current.first, /class="mr10"|class="item-meta|class="badg badg-sm/);
assert.doesNotMatch(current.first, /class="posts-item list|class="posts-item card|class="no_margin/);
@@ -27,7 +27,7 @@ test('missing and invalid tab style use the reference mini layout and ignore glo
test('un-normalized missing and invalid style use one shared mini resolution in both renderers', () => {
const cases = render();
for (const name of ['raw_default', 'raw_invalid']) {
assert.match(cases[name].tab_markup, /widget-tab-post style-mini posts-mini-lists zib-widget/);
assert.match(cases[name].tab_markup, /widget-tab-post style-mini posts-mini-lists jio-widget-box/);
assert.match(cases[name].first, /class="posts-mini ajax-item"/);
assert.doesNotMatch(cases[name].first, /class="posts-item list|class="posts-item card/);
}
@@ -140,7 +140,7 @@ test('list margin defaults inherit the site setting while an explicit widget fal
test('multi-column frontend output uses the reference widget wrapper and carries layout variables', () => {
const wrapper = render().frontend_wrapper;
assert.match(wrapper, /class="zib-widget-wrap jio-tab-posts-wrap"/);
assert.match(wrapper, /class="jio-widget-box-wrap jio-tab-posts-wrap"/);
assert.match(wrapper, /data-jio-widget="tab_posts" data-widget-id="jio_widget_tab_posts-4"/);
assert.match(wrapper, /--jio-widget-space-top:8px/);
assert.match(wrapper, /--jio-widget-space-m-bottom:10px/);
@@ -12,7 +12,7 @@ const { MiniDocument } = require('../helpers/mini-dom.cjs');
function tabMarkup(document, { firstPage = 1, firstSegmentLoaded = true } = {}) {
const root = document.createElement('div');
root.className = 'widget-tab-post style-mini posts-mini-lists zib-widget';
root.className = 'widget-tab-post style-mini posts-mini-lists jio-widget-box';
root.setAttribute('data-widget', 'tab_posts');
root.setAttribute('data-instance', 'widget-7');
const nav = document.createElement('ul');
@@ -8,7 +8,7 @@ const test = require('node:test')
const root = path.resolve(__dirname, '..', '..')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
// Reference theme (zibll css/main.css): .jb-yellow sets --this-bg-b: #f59f54 and
// Reference: .jb-yellow sets --this-bg-b: #f59f54 and
// --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%). build/public.min.css
// is enqueued after main.min.css, so the legacy .jb-* compat layer in core/buttons.css
// overrides main.css there; the two must agree or every .jb-yellow surface (the
@@ -49,7 +49,7 @@ test('the badge typography is pinned to the reference bootstrap body values', ()
assert.notEqual(start, -1, 'missing the user-card img-badge adaptation rule')
const block = main.slice(start, main.indexOf('}', start))
assert.match(block, /line-height:\s*1\.42857143/)
// Reference body font stack (zibll bootstrap.min.css; zibll main.css does not
// Reference body font stack (bootstrap.min.css; main.css does not
// override it), while Jio's reset imposes the modern --font-body stack.
assert.match(block, /font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif/)
// The Jio html reset forces antialiased + optimizeLegibility; the badge
@@ -15,7 +15,7 @@ test('user and author widgets render the reference state contract', () => {
assert.equal(result.status, 0, result.stderr || result.stdout)
const data = JSON.parse(result.stdout)
assert.match(data.guest, /^<div class="mb20"><div class="user-card zib-widget widget">/)
assert.match(data.guest, /^<div class="mb20"><div class="user-card jio-widget-box widget">/)
assert.match(data.guest, /data-lazy-type="lazy_other"/)
assert.match(data.guest, /alt="默认头像"/)
assert.match(data.guest, /<div class="user-info mt10"><div class="text-center"><p class="muted-color box-body em12">/)
@@ -26,7 +26,9 @@ test('user and author widgets render the reference state contract', () => {
assert.match(data.guest, /class="social-login-item github toggle-radius"/)
assert.match(data.guest, /class="social-login-item qq toggle-radius" data-provider="clogin_qq"/)
assert.match(data.guest, /title="GitHub登录"/)
assert.doesNotMatch(data.guest, /jio-widget|jio-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
// The reference user card keeps its own shell; it must not adopt the modern
// widget shell (jio-widget / jio-panel-wrap) or the OAuth button components.
assert.doesNotMatch(data.guest, /class="[^"]*\bjio-widget(?![\w-])|jio-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
// The social-login area carries no agreement copy; the reference guest card
// only shows the separator, the .social_loginbar and the provider buttons.
assert.doesNotMatch(data.guest, /我已阅读并同意|使用社交账号登录即表示|注册即表示/)
@@ -35,7 +37,7 @@ test('user and author widgets render the reference state contract', () => {
assert.doesNotMatch(data.guest_no_registration, /signup-loader/)
assert.equal(data.closed, '')
assert.match(data.logged, /^<div class="mb20"><div class="user-card zib-widget widget">/)
assert.match(data.logged, /^<div class="mb20"><div class="user-card jio-widget-box widget">/)
assert.match(data.logged, /data-lazy-type="lazy_cover"/)
assert.match(data.logged, /<div class="card-content mt10 relative">/)
assert.match(data.logged, /<div class="user-name flex jc">/)
@@ -47,7 +49,7 @@ test('user and author widgets render the reference state contract', () => {
assert.match(data.logged_buttons, /rel="nofollow" href="https:\/\/example\.test\/user-center" class="but pw-1em ml6 jb-blue">用户中心<\/a>/)
assert.doesNotMatch(data.logged_buttons, /<icon|<text>/)
assert.match(data.author, /^<div class="mb20"><div class="user-card zib-widget widget">/)
assert.match(data.author, /^<div class="mb20"><div class="user-card jio-widget-box widget">/)
assert.match(data.author, /data-lazy-type="lazy_cover"/)
assert.match(data.author, /data-post-type="post" data-post-style="card"/)
assert.doesNotMatch(data.author, /user-btns mt20/)
@@ -3,10 +3,10 @@
*
* 硬约束:
* 1) 「我的服务 / 功能设置」两个网格使用参考主题的 icon-but-box / user-icon-but-box
* 外壳,图标全部来自主题内置 SVG sprite(jio_icon_markup('zibsvg-*')),
* 外壳,图标全部来自主题内置 SVG sprite(jio_icon_markup('jiosvg-*')),
* 不再出现 Font Awesome 字形。
* 2) 侧栏其余部件(余额/积分卡、创作/推广卡、VIP 入口、抽屉触发器)同样只使用
* sprite 图标,箭头统一为 zibsvg-right,保证侧栏只剩一种图标族。
* sprite 图标,箭头统一为 jiosvg-right,保证侧栏只剩一种图标族。
* 3) 旧的 uc-service-grid 外壳、色调底色与计数角标必须已彻底移除,不得残留。
*/
'use strict';
@@ -34,7 +34,7 @@ test('服务/设置网格使用参考主题 icon-but-box 外壳与 sprite 图标
assert.match(nav, /class="flex ac hh text-center icon-but-box user-icon-but-box"/);
assert.match(nav, /<div class="em16"><\?php echo \$icon_html;/);
assert.match(nav, /<div class="px12 muted-color mt3">/);
assert.match(nav, /jio_icon_markup\('zibsvg-' \. \(string\) \$item\['icon'\]\)/);
assert.match(nav, /jio_icon_markup\('jiosvg-' \. \(string\) \$item\['icon'\]\)/);
// 参考主题选型:我的服务 = trend-color / user-auth / order-color。
assert.match(nav, /'icon'\s+=>\s+'trend-color'/);
@@ -68,14 +68,14 @@ test('侧栏部件(VIP 与创作/推广卡除外)不再出现 Font Awesome
test('其余侧栏部件按参考主题选型替换为 sprite 图标', () => {
const commerce = read(sidebarDir + 'sidebar-commerce.php');
assert.match(commerce, /jio_icon_markup\('zibsvg-money-color-2'\)/);
assert.match(commerce, /jio_icon_markup\('zibsvg-points-color'\)/);
assert.match(commerce, /jio_icon_markup\('zibsvg-right'\)/);
assert.match(commerce, /jio_icon_markup\('jiosvg-money-color-2'\)/);
assert.match(commerce, /jio_icon_markup\('jiosvg-points-color'\)/);
assert.match(commerce, /jio_icon_markup\('jiosvg-right'\)/);
const creator = read(sidebarDir + 'sidebar-creator.php');
assert.match(creator, /jio_icon_markup\('zibsvg-merchant-color', 'mr6'\)/);
assert.match(creator, /jio_icon_markup\('zibsvg-money-color', 'mr6'\)/);
assert.match(creator, /class="mb10-sm zib-widget padding-10 flex1 pointer"/);
assert.match(creator, /jio_icon_markup\('jiosvg-merchant-color', 'mr6'\)/);
assert.match(creator, /jio_icon_markup\('jiosvg-money-color', 'mr6'\)/);
assert.match(creator, /class="mb10-sm jio-widget-box padding-10 flex1 pointer"/);
assert.match(creator, /flex ab jsb col-ml6/);
assert.match(creator, /<i class="ml6 fa fas fa-angle-right em12"/);
assert.match(creator, /'创作中心'/);
@@ -93,8 +93,8 @@ test('其余侧栏部件按参考主题选型替换为 sprite 图标', () => {
assert.doesNotMatch(creator, /data-onclick/);
const vip = read(sidebarDir + 'sidebar-vip.php');
assert.match(vip, /jio_icon_markup\('zibsvg-vip_1', 'em12 mr6'\)/);
assert.match(vip, /jio_icon_markup\('zibsvg-time', 'mr3'\)/);
assert.match(vip, /jio_icon_markup\('jiosvg-vip_1', 'em12 mr6'\)/);
assert.match(vip, /jio_icon_markup\('jiosvg-time', 'mr3'\)/);
assert.match(vip, /mb20 mb10-sm main-radius payvip-icon btn-block badg pointer/);
assert.match(vip, /style="padding: 14px 10px;"/);
assert.match(vip, /fa-chevron-circle-right abs-right/);
@@ -103,7 +103,7 @@ test('其余侧栏部件按参考主题选型替换为 sprite 图标', () => {
assert.doesNotMatch(vip, /data-onclick/);
const profile = read(sidebarDir + 'profile-sidebar.php');
assert.match(profile, /jio_icon_markup\('zibsvg-menu'\)/);
assert.match(profile, /jio_icon_markup\('jiosvg-menu'\)/);
});
test('旧 uc-service-grid 外壳与色调底色已彻底移除', () => {
@@ -125,32 +125,32 @@ test('旧 uc-service-grid 外壳与色调底色已彻底移除', () => {
assert.doesNotMatch(shell, /\.user-icon-but-box > a\.is-active/);
assert.match(shell, /\.user-icon-but-box > a \{[^}]*color: inherit/s);
// 外壳与标题 1:1 参考主题 zib-widget padding-6 / padding-6 ml3。
// 外壳与标题 1:1 参考主题 jio-widget-box padding-6 / padding-6 ml3。
const nav = read(sidebarDir + 'sidebar-nav.php');
assert.match(nav, /class="uc-sidebar-iconbox zib-widget padding-6 mb10-sm"/);
assert.match(nav, /class="uc-sidebar-iconbox zib-widget padding-6"/);
assert.match(nav, /class="uc-sidebar-iconbox jio-widget-box padding-6 mb10-sm"/);
assert.match(nav, /class="uc-sidebar-iconbox jio-widget-box padding-6"/);
assert.match(nav, /<div class="padding-6 ml3"><\?php esc_html_e\('我的服务'/);
assert.match(nav, /<div class="padding-6 ml3"><\?php esc_html_e\('功能设置'/);
assert.doesNotMatch(nav, /uc-sidebar__heading/);
assert.match(shell, /\.uc-sidebar-iconbox\.zib-widget \{/);
assert.match(shell, /\.uc-sidebar-iconbox\.jio-widget-box \{/);
// 卡片上的 <i> 已换成 <svg>,元素选择器需覆盖两者。
assert.match(shell, /\.uc-summary-card__body > svg/);
// 旧的琥珀 VIP 券与 uc-center-card 已被参考主题结构替换。
assert.doesNotMatch(shell, /uc-center-card/);
assert.doesNotMatch(shell, /uc-sidebar-vip__/);
assert.match(shell, /\.uc-sidebar-centers > \.zib-widget/);
assert.match(shell, /\.uc-sidebar-centers > \.jio-widget-box/);
// 文章/评论/收藏/粉丝统计 1:1 参考主题 zib_user_center_page_sidebar_statistics()。
// 文章/评论/收藏/粉丝统计。
const stats = read(sidebarDir + 'sidebar-stats.php');
assert.match(stats, /mb10-sm mb20 flex ac jsa zib-widget padding-10 text-center/);
assert.match(stats, /mb10-sm mb20 flex ac jsa jio-widget-box padding-10 text-center/);
assert.match(stats, /<a class="user-statistics-item"/);
assert.match(stats, /<div class="em14"><\?php echo esc_html\(jio_cut_count\(/);
assert.match(stats, /<div class="em09 opacity5 mt6">/);
assert.doesNotMatch(stats, /uc-stats|uc-sidebar__section/);
assert.doesNotMatch(shell, /\.uc-stats/);
assert.doesNotMatch(read('assets/css/routes/user-center/responsive.css'), /\.uc-stats/);
assert.match(shell, /\.uc-sidebar-statistics\.zib-widget \{/);
assert.match(shell, /\.uc-sidebar-statistics\.jio-widget-box \{/);
});
test('所用 sprite key 均在前端 sprite 中定义', () => {
@@ -168,10 +168,10 @@ test('导航栏用户浮层的余额/积分磁贴与侧栏使用同一组 sprite
const nav = read(sidebarDir + 'sidebar-commerce.php');
const wallet = read('inc/helpers/jio-header-userbox.php');
assert.match(wallet, /jio_icon_markup\('zibsvg-money-color-2', 'em14'\)/);
assert.match(wallet, /jio_icon_markup\('zibsvg-points-color', 'em14'\)/);
assert.match(nav, /jio_icon_markup\('zibsvg-money-color-2'\)/);
assert.match(nav, /jio_icon_markup\('zibsvg-points-color'\)/);
assert.match(wallet, /jio_icon_markup\('jiosvg-money-color-2', 'em14'\)/);
assert.match(wallet, /jio_icon_markup\('jiosvg-points-color', 'em14'\)/);
assert.match(nav, /jio_icon_markup\('jiosvg-money-color-2'\)/);
assert.match(nav, /jio_icon_markup\('jiosvg-points-color'\)/);
// 磁贴箭头沿用参考主题的 fa-angle-right,不做多余替换。
assert.match(wallet, /fa fas fa-angle-right em12/);
@@ -19,7 +19,7 @@ test('user level renderer keeps the canonical panel mapping and reference layout
assert.match(renderer, /class="col-md-7"/);
assert.match(renderer, /colorful-bg jb-vip1 jio-level-card/);
assert.match(renderer, /colorful-bg c-gray jio-level-card/);
assert.match(renderer, /class="zib-widget jio-level-experience"/);
assert.match(renderer, /class="jio-widget-box jio-level-experience"/);
assert.match(renderer, /id="tab_integral_get"/);
assert.match(renderer, /id="tab_integral_my"/);
assert.match(renderer, /id="tab_integral_date"/);
@@ -1,12 +1,12 @@
/**
* VIP 后台设置弹窗文案字段契约(对照参考主题 zibll 修正)。
* VIP 后台设置弹窗文案字段契约。
*
* 硬约束:
* 1) 弹窗文案字段更名为「会员介绍」(pay_user_vip_desc, input) 与
* 「开通会员更多内容」(pay_user_vip_more, textarea rows 4 +
* sanitize_type html),并从「会员等级」组移到分区末尾的
* 「界面文案」(modal-copy) 独立分组;
* 2) 文案默认值对齐 zibll(黄金会员/钻石会员、尊享专属特权、权益四项、
* 2) 文案默认值(黄金会员/钻石会员、尊享专属特权、权益四项、
* 会员介绍与开通会员更多内容默认值),SPA schema / PHP schema /
* PaymentRuntime 回退三处一致——Settings::get_all() 不合并
* schema 默认值,运行时回退即实际默认值;
@@ -49,7 +49,7 @@ test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为
assert.ok(!src.includes('弹窗顶部会员说明') && !src.includes('弹窗底部补充说明 HTML'), `${name} 旧名称应已移除`);
assert.ok(src.includes('会员介绍') && src.includes('开通会员更多内容'), `${name} 应含新名称`);
}
// 会员介绍为单行 input(zibll text「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。
// 会员介绍为单行 input(「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。
assert.match(
vipSchemaJs,
/key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*sanitize_type: 'html',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/
@@ -66,7 +66,7 @@ test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为
schemaPhp,
/'key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html'[\s\S]*?'rows' => 4/
);
// 帮助文本对齐 zibll 的 desc。
// 帮助文本与字段说明一致。
assert.ok(vipSchemaJs.includes('显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍'));
assert.ok(vipSchemaJs.includes('显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码'));
});
@@ -98,7 +98,7 @@ test('两个文案字段位于分区末尾的界面文案分组且不再属于
assert.ok(basicPhp.includes("'key' => 'pay_user_vip_1_s'") && basicPhp.includes("'key' => 'pay_user_vip_2_s'"), 'SchemaPayment.php 会员等级组应保留两个等级开关');
// 二级会员先于一级会员开启时的危险提示(参考主题同款)。
assert.ok(basicJs.includes('vip_level_order_notice') && basicPhp.includes('vip_level_order_notice'));
// 依赖:任一等级启用时显示(zibll pay_user_vip_1_s 依赖的双等级适配)。
// 依赖:任一等级启用时显示(双等级开关适配)。
assert.ok(
vipSchemaJs.includes("dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }"),
'vip.js 应为 any 双等级依赖'
@@ -124,7 +124,7 @@ test('续费/升级介绍字段存在且被档位卡消费', () => {
assert.match(vipTabPhp, /\$card_action = 'upgrade'/);
});
test('文案默认值三处一致并对齐 zibll', () => {
test('文案默认值三处一致', () => {
// SPA defaults 块与字段级 default。
assert.ok(vipSchemaJs.includes("pay_user_vip_1_name: '黄金会员'"));
assert.ok(vipSchemaJs.includes("pay_user_vip_2_name: '钻石会员'"));
@@ -192,7 +192,7 @@ test('会员介绍按 post 级 HTML 安全净化并在弹窗与用户中心渲
});
test('开通会员更多内容只在弹窗内渲染,不裸露在用户中心页面', () => {
// 参考主题 zibpay_user_page_tab_content_vip 只渲染 pay_user_vip_desc;
// 会员页 tab 只渲染 pay_user_vip_desc;
// pay_user_vip_more 仅出现在两个弹窗函数(购买/续费弹窗)中。
assert.ok(!vipTabPhp.includes("vip_settings['more']"), '用户中心 VIP 页不应渲染 more');
assert.ok(!vipTabPhp.includes('muted-box padding-10 mini-radius mt15'), '用户中心 VIP 页不应有 more 提示盒');
@@ -218,8 +218,7 @@ test('弹窗内 more 与卡密说明按 HTML 渲染而非纯文本', () => {
});
test('积分兑换说明按 post 级 HTML 放行并原样渲染', () => {
// 参考主题该字段 sanitize=false 且弹窗内原样拼接;Jio 对齐为保存/输出
// 双端 wp_kses_post + 弹窗 wp_kses_post 渲染。
// 积分兑换说明:保存端 wp_kses_post 且弹窗内原样拼接;
assert.match(vipSchemaJs, /key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html'/);
assert.match(schemaPhp, /'key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html'/);
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
@@ -5,10 +5,10 @@
* 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` 全宽横条,仅非会员展示)。
* - /user/vip/ 档位购买卡下方(参考实现的会员页 tab 的
* `c-yellow jio-widget-box flex ac jsb` 全宽横条,仅非会员展示)。
* 两处入口均带 data-jio-vip-points-open,由 pay-vip.js 文档级委托打开
* 同一静态弹窗。
* 2) 静止态必须被 vip-modal.css 显式隐藏(.flex 覆盖回归护栏),打开态为
@@ -169,7 +169,7 @@ test('开关自包含:不依赖 jQuery/Bootstrap modal 运行时', () => {
});
test('会员弹窗内的入口为参考主题同款按钮并开本地弹窗', () => {
// 参考主题 zibpay_get_pay_uservip_modal 的 class 与图标顺序。
// 参考实现的会员购买弹窗的 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` 由图标契约(
@@ -180,8 +180,8 @@ test('会员弹窗内的入口为参考主题同款按钮并开本地弹窗', ()
});
test('/user/vip/ 档位购买卡下方的入口为参考主题用户中心同款横条', () => {
// 参考主题 zibpay_user_page_tab_content_vip:my_box → vip_card → points link,
// 横条为 `c-yellow zib-widget flex ac jsb`(zib-widget → 本主题jio-panel表面)。
// 会员页 tab:my_box → vip_card → points link,
// 横条为 `c-yellow jio-widget-box flex ac jsb`(jio-widget-box → 本主题jio-panel表面)。
assert.match(footer, /function jio_get_vip_points_exchange_entry_html/);
assert.match(vipTab, /jio_get_vip_points_exchange_entry_html\(\$user_id, \$level\)/);
// 结构 1:1:左 points-color 图标 + 标题(flex1 em12 c-yellow),
@@ -195,7 +195,7 @@ test('/user/vip/ 档位购买卡下方的入口为参考主题用户中心同款
});
test('/user/vip/ 入口门控与弹窗壳同源:非会员 + 兑换开启 + 有可兑换套餐', () => {
// 参考主题 zibpay_get_vip_points_exchange_link 门控:已是会员不展示。
// 已是会员不展示积分兑换入口。
assert.match(footer, /if \(\$vip_level > 0\) \{\s*return '';/);
// 可用性直接取服务端 catalog(内含 credits 门控与等级启用过滤),
// 与 wp_footer 弹窗壳的渲染条件一致,避免出现无弹窗可开的死入口。
@@ -1,5 +1,5 @@
/**
* VIP 后台设置结构化契约(对照参考主题 zibll 1:1)。
* VIP 后台设置结构化契约。
*
* 硬约束:
* 1) 会员商品 / 续费商品 / 升级价格 / 积分兑换明细全部为结构化 group 或
@@ -3,7 +3,7 @@
*
* 硬约束:
* 1) 未开通用户不渲染到期时间行——状态标签已显示「未开通」,参考主题
* zibpay_user_page_tab_content_vip 对从未开通用户同样不输出任何到期
* 会员页 tab 对从未开通用户同样不输出任何到期
* 信息(仅生效中/永久/已过期才有);
* 2) 生效中、永久、已过期三个分支的到期文案保持产出;
* 3) 到期行整体按文案非空条件渲染,不留空壳 div。