Files
wordpress-theme-aksrt/tests/widget-system/vip-modal-skeleton.test.cjs
T
kate522 cb76b95b01 feat(vip): skeleton loading state and faster modal/site startup
The VIP purchase/renew modal opened with a single "loading..." line, which
reads as a hang while /pay/vip/modal round-trips. Every frontend pageview
also paid an option write inside wp_head, and the header user-card CSS was
enqueued even for guests who never see the card.

pay-vip.js now builds a full-layout skeleton (user box, level tabs, the
two-column package panel) from vipSkeleton(), reusing the global
aksrt-modal-runtime__skeleton line/block classes; the VIP-specific shapes
live in the conditionally loaded vip-modal.css, and the 640px two-column
threshold matches .pay-vip-form-row. The modal cache TTL goes 15s -> 60s
(purchase, card-pass redeem and points exchange already invalidate the
vip tag), and hover/focus on a .pay-vip trigger now prefetches the payload
through the same cacheKey and flight openRemote uses, so a warm click
opens instantly.

The bottom note and the whole help_text pipeline behind it are removed; the
admin-editable pay_user_vip_desc still renders at the top of the modal, and
the recharge/gateway help_text keys are untouched. get_catalog() reads
points and wallet balance through one new LedgerService::points_and_balance()
call. The StatsTracker counter write moves to shutdown so wp_head only does
the visitor check and setcookie, and header-userbox.css is enqueued for
logged-in users only.

Verification: node --test across the suite (628 tests, including the 7 new
contract assertions in tests/widget-system/vip-modal-skeleton.test.cjs) and
php -l on every changed PHP file. No visual acceptance performed, per request.
2026-10-18 13:44:01 +08:00

135 lines
7.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.
/**
* 会员弹窗骨架屏 / 打开提速 / help_text 清理 / 启动优化契约。
*
* 硬约束:
* 1) 会员弹窗加载占位为完整布局仿形骨架(vipSkeleton()),复用全局
* aksrt-modal-runtime__skeleton-line/-block 与 shimmer 动画,
* VIP 专属形状样式落在 assets/css/frontend/vip-modal.css;
* 2) 弹窗数据缓存 TTL 60s,悬停/聚焦入口按等级意图预热,预热与
* openRemote 共用 cacheKey 与 flight;
* 3) 弹窗底部提示句与整条 help_text 管线已移除,顶部说明 desc 保留;
* 4) 积分/钱包余额合并为一次 LedgerService::points_and_balance();
* 5) 访问统计写库移到 shutdown,wp_head 只做访客判定与 setcookie;
* 6) header-userbox 样式仅登录用户入队。
*/
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');
// 取某个方法体(到下一个同类方法声明为止),避免断言误伤其它功能的同名键。
function sliceMethod(source, signature, nextSignature) {
const start = source.indexOf(signature);
assert.notStrictEqual(start, -1, `missing ${signature}`);
const end = source.indexOf(nextSignature, start + signature.length);
return source.slice(start, end === -1 ? undefined : end);
}
const vipJs = read('assets', 'js', 'pay-vip.js');
const vipCss = read('assets', 'css', 'frontend', 'vip-modal.css');
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
const modal = read('src', 'Content', 'VipServiceModal.php');
const catalogSource = read('src', 'Content', 'VipServicePurchase.php');
const ledger = read('src', 'Ledger', 'LedgerService.php');
const stats = read('src', 'Core', 'StatsTracker.php');
const scripts = read('src', 'Core', 'ScriptsPart1b.php');
test('会员弹窗使用完整布局仿形骨架屏', () => {
assert.match(vipJs, /function vipSkeleton\(\) \{/);
assert.match(vipJs, /node\("div", "aksrt-vip-modal-skeleton"\)/);
assert.match(vipJs, /skeleton\.setAttribute\("aria-busy", "true"\)/);
// 结构:用户框(头像+两行)、等级卡行、双列面板(权益+套餐区)。
assert.match(vipJs, /aksrt-vip-modal-skeleton__user/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__avatar/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__lines/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__tabs/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__tab/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__pane/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__info/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__products/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__button/);
// 线条/色块复用全局骨架类。
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-wide/);
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-short/);
assert.match(vipJs, /aksrt-modal-runtime__skeleton-block/);
// openRemote 传入自定义骨架,不再使用纯文本占位。
assert.match(vipJs, /skeleton: vipSkeleton\(\),/);
assert.doesNotMatch(vipJs, /加载中/);
});
test('骨架样式落在 vip-modal.css 并对齐 640px 双列阈值', () => {
assert.match(vipCss, /\.aksrt-vip-modal-skeleton \{/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__avatar \{[\s\S]*?border-radius: 50%/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__tab \{[\s\S]*?flex: 1/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__button \{[\s\S]*?height: 38px/);
assert.match(vipCss, /animation: aksrt-modal-skeleton/);
assert.match(vipCss, /@media \(min-width: 640px\)[\s\S]*?\.aksrt-vip-modal-skeleton__pane \{[\s\S]*?grid-template-columns: 1fr 1\.2fr/);
});
test('弹窗打开提速:缓存 TTL 提升与意图预热', () => {
assert.match(vipJs, /cacheTtl: 60000,/);
// 预热与 openRemote 同一 cacheKey(method + AksrtRequest.url)。
assert.match(vipJs, /function vipModalCacheKey\(level\) \{/);
assert.match(vipJs, /"GET:" \+ window\.AksrtRequest\.url\("\/pay\/vip\/modal", \{ vip_level: level \}\)/);
assert.match(vipJs, /cache\.acquire\(vipModalCacheKey\(level\)/);
assert.match(vipJs, /ttl: 60000,/);
assert.match(vipJs, /tags: \["vip"\]/);
// 悬停/聚焦触发,绑定进 bindOnce。
assert.match(vipJs, /\["pointerenter", "focusin"\]/);
assert.match(vipJs, /function bindPrefetch\(\) \{/);
assert.match(vipJs, /document\.addEventListener\("click", onDocClick, true\);\s*bindPrefetch\(\);/);
// 每等级每页只预热一次。
assert.match(vipJs, /prefetchedLevels\[level\] = true;/);
});
test('弹窗底部提示与 help_text 管线已彻底移除', () => {
assert.doesNotMatch(vipJs, /help_text/);
// 仅收敛 VIP 设置函数:充值/网关设置的 help_text 与本文无关,保持原样。
const vipSettings = sliceMethod(
paymentRuntime,
'public static function get_payment_vip_settings',
'public static function get_payment_revenue_settings'
);
assert.doesNotMatch(vipSettings, /help_text/);
assert.doesNotMatch(vipSettings, /开通后立即生效/);
assert.doesNotMatch(modal, /help_text/);
const viewModel = sliceMethod(modal, 'public function get_modal_view_model', 'public function get_points_exchange_modal_html');
assert.doesNotMatch(viewModel, /help_text/);
assert.doesNotMatch(catalogSource, /help_text/);
// 顶部说明 desc 仍保留(后台 pay_user_vip_desc 继续生效)。
assert.match(vipJs, /catalog\.desc/);
assert.match(paymentRuntime, /'desc'\s+=> \$desc,/);
});
test('积分与钱包余额合并为一次 LedgerService 读取', () => {
assert.match(ledger, /public function points_and_balance\(int \$user_id\): array/);
// 仅收敛 get_catalog:购买/履约路径的余额读取与本文无关,保持原样。
const getCatalog = sliceMethod(catalogSource, 'public function get_catalog', 'private function find_catalog_package');
assert.match(getCatalog, /points_and_balance\(\$user_id\)/);
assert.match(getCatalog, /'user_points'\s+=> \$balances\['points'\]/);
assert.match(getCatalog, /'wallet_balance'\s+=> \$balances\['balance'\]/);
assert.doesNotMatch(getCatalog, /LedgerService::get_instance\(\)->balance\(/);
assert.doesNotMatch(getCatalog, /balance_decimal/);
});
test('访问统计写库移出渲染关键路径', () => {
assert.match(stats, /add_action\('wp_head', \[self::get_instance\(\), 'track_visit'\], 1\)/);
assert.match(stats, /add_action\('shutdown'/);
assert.match(stats, /public function record_stats\(string \$option_key, bool \$is_unique\): void/);
// wp_head 阶段只做判定与 setcookie,不再读写 option。
const trackVisit = stats.slice(stats.indexOf('public function track_visit'), stats.indexOf('public function record_stats'));
assert.doesNotMatch(trackVisit, /update_option|get_option/);
assert.match(trackVisit, /setcookie\(\$visitor_cookie/);
// 落库与月初清理收敛到 record_stats。
const recordStats = stats.slice(stats.indexOf('public function record_stats'));
assert.match(recordStats, /update_option\(\$option_key, \$stats, false\)/);
assert.match(recordStats, /cleanup_old_stats\(\)/);
});
test('header-userbox 样式仅登录用户入队', () => {
assert.match(scripts, /if \(is_user_logged_in\(\)\) \{\s*\$enqueue_module\('header-userbox'\);/);
});