顶导航点击小人弹出的用户信息浮层,从 AKSRT 定制结构改为与参考主题 zib_header_user_box() 逐行同构:头像链接包裹、钱包双瓷砖、会员条、签到、消息图标、统计徽章行全部改用参考主题遗留类,渲染交回 main.css(.sub-user-box 242px、.avatar-img 38px、.msg-news-icon、badge.top、.payvip-icon 深色渐变加 gem 水印、.vip-expdate-tag、.muted-box)。 头像改走 aksrt_get_avatar_box(用户ID, avatar-img, true, false),得到参考的 a 链接包裹 avatar-img 结构。消息图标改参考标记 msg-news-icon abs-right 加 toggle-radius msg-icon,未读徽章用 badge.top,avatar_dropdown 位置门控契约保留。管理端行在「后台管理」前插入「支付中心」,跳 admin.php?page=aksrt_pay,即参考主题「商城中心」槽位,图标 fa-credit-card。 钱包重写为参考双瓷砖:余额在前(c-blue-2 加 fa-money-bill-wave)、积分在后(c-yellow 加 fa-coins),分别按充值开关与积分总开关门控,两块皆关则不渲染,并保留 data-aksrt-wallet-value 与 data-aksrt-points-value 两个 JS 刷新钩子。会员条去掉 AKSRT 自创的 vipbg-v2 level-N 金色背景,改参考的 payvip-icon 加 badg jb-yellow vip-expdate-tag,到期文案按参考语义分为已过期、永久会员、裸日期三种。签到入口由 button 改为 a.but block c-yellow,并去掉随之失效的 compact 形参,data-signin 挂载与 wp_footer 验证码不变。统计行删掉文章徽章(本站不做帖子),图标由 SVG sprite 换 FA5+ 的 fa-comment-dots、fa-thumbs-up、fa-fire。 会员等级图标契约不动:aksrt_get_vip_icon 与 User\Level::get_badge_html 的调用与参数保持原样。 顺带清死代码:aksrt_header_user_badges_row 原本取到函数后直接返回空串,现在真正输出统计行;删掉 header-userbox.css 里整套 bespoke 规则与 vip-modal.css 的 vipbg-v2 死规则。 显式偏差:统计行链接保持扁平用户主页 URL(AKSRT 用户主页是干净 URL 加 JS tab,没有 query tab 路由);签到入口沿用积分签到总开关门控,不新增管理端可配置项。 public.css 与 public.min.css 用 tools/build-public-assets.mjs 重建:该 bundle 打包了 header-userbox.css 且作为依赖先于单模块入队,旧的 width 260px 会压过新源码、把卡片钉在 260px 而不是参考的 242px。 验证:node --test tests/**/*.test.cjs 573 项全通过;六个改动 PHP 文件 php -l 无语法错误;卡片用到的 FA5 token 逐个核对 5.15.4 全部命中。未做真实视觉效果验收。
334 lines
16 KiB
JavaScript
334 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
const join = (files) => files.map(read).join('\n');
|
|
const exists = (file) => fs.existsSync(path.join(root, file));
|
|
const listFiles = (directory, extensions) => {
|
|
const base = path.join(root, directory);
|
|
const files = [];
|
|
const walk = (current) => {
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
if (['build', 'node_modules', 'third-party'].includes(entry.name)) continue;
|
|
const absolute = path.join(current, entry.name);
|
|
if (entry.isDirectory()) walk(absolute);
|
|
else if (extensions.has(path.extname(entry.name))) files.push(path.relative(root, absolute));
|
|
}
|
|
};
|
|
walk(base);
|
|
return files;
|
|
};
|
|
|
|
const scripts = join([
|
|
'src/Core/ScriptsPart1b.php',
|
|
'src/Core/ScriptsPart1c.php',
|
|
]);
|
|
// The Font Awesome handle is registered inside aksrt_enqueue_fontawesome(), so the
|
|
// 5.15.4 asset contract has to follow that indirection rather than the enqueue call site.
|
|
const iconRuntime = join([
|
|
'src/Core/ScriptsPart1b.php',
|
|
'src/Core/ScriptsPart1c.php',
|
|
'inc/helpers/aksrt-icons.php',
|
|
]);
|
|
const header = join([
|
|
'header.php',
|
|
'inc/helpers/header/aksrt-header-render.php',
|
|
'inc/helpers/header/aksrt-header-navbar.php',
|
|
'inc/helpers/header/aksrt-header-mobile.php',
|
|
'inc/helpers/aksrt-header-menu-buttons.php',
|
|
'inc/helpers/aksrt-header-userbox.php',
|
|
]);
|
|
|
|
test('the public dependency graph uses WordPress jQuery with the local runtime stack', () => {
|
|
assert.match(scripts, /wp_enqueue_script\('jquery'\)/);
|
|
assert.doesNotMatch(scripts, /wp_deregister_script\('jquery'\)/);
|
|
assert.doesNotMatch(scripts, /jquery\.min\.js'[\s\S]*?'1\.9\.1'/);
|
|
assert.match(scripts, /bootstrap\.min\.css'[\s\S]*?'3\.4\.1'/);
|
|
assert.match(scripts, /aksrt_enqueue_fontawesome\(/);
|
|
assert.match(iconRuntime, /fontawesome5\/css\/all\.min\.css'[\s\S]*?'5\.15\.4'/);
|
|
assert.match(scripts, /main\.min\.css/);
|
|
assert.match(scripts, /loader\.js'[\s\S]*?\['jquery', 'bootstrap'\]/);
|
|
assert.match(read('assets/js/runtime/loader.js'), /tbquire\(\['main'\]\)/);
|
|
for (const file of [
|
|
'assets/js/runtime/libs/bootstrap.min.js',
|
|
'assets/css/bootstrap.min.css',
|
|
'assets/fontawesome5/css/all.min.css',
|
|
'assets/css/main.min.css',
|
|
'assets/js/runtime/loader.js',
|
|
'assets/js/runtime/main.min.js',
|
|
]) assert.equal(exists(file), true, `${file} must be local`);
|
|
|
|
assert.doesNotMatch(scripts, /bootstrap5|bootstrap-5|font-awesome\.min\.css|swiper(?:\.min)?\.(?:css|js)|aksrt-mini-touch/);
|
|
assert.equal(exists('assets/css/frontend/nav.css'), false);
|
|
assert.equal(exists('assets/js/core/nav.js'), false);
|
|
assert.equal(exists('assets/js/core/search.js'), false);
|
|
assert.equal(exists('assets/js/core/scroll.js'), false);
|
|
assert.equal(exists('assets/js/core/slider.js'), false);
|
|
assert.equal(exists('assets/js/mini-touch.js'), false);
|
|
});
|
|
|
|
test('payment entry points use only the AKSRT payment runtime', () => {
|
|
const loader = read('assets/js/runtime/loader.js');
|
|
const main = join([
|
|
'assets/js/runtime/main.js',
|
|
'assets/js/runtime/main.min.js',
|
|
]);
|
|
const paymentRuntime = join([
|
|
'assets/js/pay-vip.js',
|
|
'assets/js/cms-download.js',
|
|
'src/Core/ScriptsPart1b.php',
|
|
]);
|
|
|
|
assert.doesNotMatch(loader, /['"]pay['"]\s*:/);
|
|
assert.doesNotMatch(main, /tbquire\(\[['"]pay['"]\]\)/);
|
|
assert.doesNotMatch(main, /indexOf\(['"]zippay['"]\)/);
|
|
assert.match(paymentRuntime, /\.pay-vip/);
|
|
assert.match(paymentRuntime, /data-aksrt-pay-content/);
|
|
assert.match(paymentRuntime, /'aksrt-pay-vip'/);
|
|
assert.match(paymentRuntime, /'aksrt-cms-download'/);
|
|
});
|
|
|
|
test('Font Awesome 5 is the only icon contract across every runtime path', () => {
|
|
const extensions = new Set(['.php', '.js', '.vue']);
|
|
const runtimeFiles = [
|
|
...fs.readdirSync(root)
|
|
.filter((file) => path.extname(file) === '.php'),
|
|
...listFiles('src', extensions),
|
|
...listFiles('inc', extensions),
|
|
...listFiles('templates', extensions),
|
|
...listFiles('assets/js', extensions),
|
|
...listFiles('assets/admin/src', extensions),
|
|
];
|
|
const runtime = join(runtimeFiles);
|
|
const iconCss = join([
|
|
...listFiles('assets/css', new Set(['.css'])),
|
|
]);
|
|
const fa5 = read('assets/fontawesome5/css/all.min.css');
|
|
const iconTokens = new Set([...runtime.matchAll(/\bfa-[a-z0-9]+(?:-[a-z0-9]+)*\b/g)].map((match) => match[0]));
|
|
const utility = /^fa-(?:xs|sm|lg|[2-9]x|10x|fw|ul|li|border|pull-left|pull-right|spin|pulse|rotate|rotate-(?:90|180|270)|flip|flip-(?:horizontal|vertical|both)|stack|stack-1x|stack-2x|inverse|sr-only|sr-only-focusable)$/;
|
|
const custom = new Set([
|
|
'fa-chevron', 'fa-icon', 'fa-icon-picker', 'fa-icons', 'fa-input',
|
|
'fa-mask', 'fa-spin-hover', 'fa-toggle-theme', 'fa-xxx',
|
|
]);
|
|
// The theme ships its own SVG sprite family, so fa-zibsvg-<key> is a project
|
|
// icon rather than a Font Awesome glyph — allow the prefix instead of every key.
|
|
const projectFamily = /^fa-zibsvg-/;
|
|
|
|
assert.doesNotMatch(runtime, /\bfa fa-/);
|
|
assert.doesNotMatch(runtime, /FontAwesome|font-awesome\.min\.css|Font Awesome 4|4\.7\.0/);
|
|
assert.doesNotMatch(iconCss, /FontAwesome|font-awesome\.min\.css|Font Awesome 4|4\.7\.0/);
|
|
assert.doesNotMatch(iconCss, /font:\s*normal\s+normal\s+normal\s+[^;]+Font Awesome 5 Free/);
|
|
assert.equal(exists('assets/css/font-awesome.min.css'), false);
|
|
for (const token of iconTokens) {
|
|
if (utility.test(token) || custom.has(token) || projectFamily.test(token)) continue;
|
|
assert.ok(fa5.includes(`.${token}:before`), `${token} is not available in Font Awesome 5.15.4`);
|
|
}
|
|
});
|
|
|
|
test('header output owns all three layouts and reference state classes', () => {
|
|
assert.match(header, /header-layout-'\s*\.\s*\$layout/);
|
|
assert.match(header, /in_array\(\$layout, \[['"]1['"], ['"]2['"], ['"]3['"]\], true\)/);
|
|
assert.match(header, /navbar-top/);
|
|
assert.match(header, /container-header/);
|
|
assert.match(header, /nav_fixed/);
|
|
assert.match(header, /nav_scroll_down_hide/);
|
|
assert.match(header, /nav-fixed/);
|
|
assert.match(header, /show-slide/);
|
|
assert.match(header, /main-search-btn/);
|
|
assert.doesNotMatch(header, /data-bs-|navbar-expand|offcanvas/);
|
|
});
|
|
|
|
test('mobile navigation is one setting-driven left right or top drawer', () => {
|
|
assert.match(header, /mobile_navbar_align/);
|
|
for (const align of ['left', 'right', 'top']) {
|
|
assert.match(header, new RegExp(`'${align}'`));
|
|
}
|
|
assert.match(header, /mini-touch="mobile-nav"/);
|
|
assert.match(header, /touch-direction/);
|
|
assert.match(header, /mobile-navbar-show/);
|
|
assert.match(header, /class="mobile-navbar visible-xs-block scroll-y mini-scrollbar/);
|
|
assert.match(header, /class="fixed-body" data-toggle-class="mobile-navbar-show"/);
|
|
});
|
|
|
|
test('guest account member VIP and publish contracts share reference header DOM', () => {
|
|
const userboxCss = read('assets/css/frontend/header-userbox.css');
|
|
|
|
assert.match(header, /is_user_logged_in\(\)/);
|
|
assert.match(header, /sub-user-box aksrt-header-userbox/);
|
|
assert.match(header, /signin-loader|signin/);
|
|
assert.match(header, /get_payment_vip_settings/);
|
|
assert.match(header, /nav_user_newposts_btn/);
|
|
assert.match(header, /nav_newposts/);
|
|
assert.match(header, /navbar-form navbar-right navbar-but/);
|
|
assert.match(header, /pay-vip/);
|
|
assert.match(scripts, /\$enqueue_module\('header-userbox'\)/);
|
|
assert.match(userboxCss, /\.header-user-href > a[\s\S]*?max-height: none/);
|
|
assert.doesNotMatch(userboxCss, /user-dropdown|nav-user|html\.dark|theme-dark/);
|
|
assert.doesNotMatch(userboxCss, /--(?:main|muted|key)-|@import/);
|
|
// Account rows render through the shared legacy classes; the module keeps only
|
|
// the shell anchoring, the account href geometry and the top-nav bell badge.
|
|
assert.match(userboxCss, /\.aksrt-nav-notification \.aksrt-notification-badge/);
|
|
assert.doesNotMatch(userboxCss, /aksrt-header-wallet|aksrt-header-checkin|aksrt-header-payvip|\.aksrt-header-userbox \.aksrt-notification-badge/);
|
|
assert.doesNotMatch(userboxCss, /width:\s*260px/);
|
|
});
|
|
|
|
test('the account popover rows match the reference user box structure', () => {
|
|
const identity = read('inc/helpers/aksrt-identity.php');
|
|
const legacyMainCss = read('assets/css/main.css');
|
|
|
|
// Stats row is wired with the reference wrapper and drops the posts chip.
|
|
assert.match(header, /aksrt_header_user_badges_row\(\$user_id\)/);
|
|
assert.match(header, /class="em09 author-tag mb10 mt6 flex jc"/);
|
|
assert.doesNotMatch(identity, /tag-posts/);
|
|
assert.match(identity, /fa fas fa-comment-dots/);
|
|
assert.match(identity, /fa fas fa-thumbs-up/);
|
|
assert.match(identity, /fa fas fa-fire/);
|
|
assert.doesNotMatch(identity, /aksrt_renderer_svg_icon/);
|
|
|
|
// Admin row exposes the AKSRT payment center at the reference mall slot.
|
|
assert.match(header, /admin\.php\?page=aksrt_pay/);
|
|
assert.match(header, /支付中心/);
|
|
|
|
// Wallet tiles: balance first, muted-box geometry, FA5 icons.
|
|
assert.match(header, /class="flex ab jsb col-ml6"/);
|
|
assert.match(header, /muted-box padding-6 flex1 radius4/);
|
|
assert.match(header, /font-bold c-blue-2/);
|
|
assert.match(header, /font-bold c-yellow/);
|
|
assert.match(header, /fa fas fa-money-bill-wave/);
|
|
assert.match(header, /fa fas fa-coins/);
|
|
|
|
// Message icon uses the reference markup.
|
|
assert.match(header, /msg-news-icon abs-right/);
|
|
assert.match(header, /toggle-radius msg-icon/);
|
|
assert.match(header, /<badge class="top">/);
|
|
|
|
// VIP bar and CTA use the reference payvip geometry.
|
|
assert.match(header, /radius4 payvip-icon btn-block text-center/);
|
|
assert.match(header, /vip-expdate-tag/);
|
|
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
|
|
assert.doesNotMatch(header, /vipbg-v2/);
|
|
|
|
// Check-in entry is an anchor with the reference button classes.
|
|
assert.match(header, /but block c-yellow/);
|
|
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
|
|
|
|
// Card geometry falls back to the reference .sub-user-box width.
|
|
assert.match(legacyMainCss, /\.sub-user-box\s*\{\s*width:\s*242px;/s);
|
|
});
|
|
|
|
test('the only menu metadata and Walker contract is the legacy runtime', () => {
|
|
const walker = read('src/Frontend/NavWalker.php');
|
|
const menuAdmin = read('src/Admin/NavMenuOptions.php');
|
|
const decoration = read('inc/helpers/menu-decoration.php');
|
|
const source = walker + menuAdmin + decoration;
|
|
|
|
assert.match(source, /aksrt_menu_options/);
|
|
assert.doesNotMatch(source, /zib_menu_options/);
|
|
assert.match(walker, /start_lvl|start_el|end_el/);
|
|
assert.match(walker, /submenu_graphic_card/);
|
|
assert.match(walker, /submenu_multi_column_links/);
|
|
assert.match(walker, /senior-submenu/);
|
|
assert.match(walker, /swiper-container swiper-scroll/);
|
|
assert.match(walker, /swiper-wrapper/);
|
|
assert.match(walker, /show-nav-but|collapse|submenu_s/);
|
|
assert.match(menuAdmin, /graphic_card_opts_json/);
|
|
assert.match(menuAdmin, /multi_column_links_opts_json/);
|
|
});
|
|
|
|
test('search is footer-lazy and exposes only reference actions and request fields', () => {
|
|
const shell = read('inc/helpers/header/aksrt-header-menu-search.php');
|
|
const box = read('inc/helpers/search/aksrt-search-box.php');
|
|
const results = read('search.php');
|
|
const settings = read('src/Settings/ThemeSearch.php');
|
|
const source = shell + box + results + settings;
|
|
|
|
assert.match(shell, /add_action\('wp_footer', 'aksrt_footer_navbar_search'/);
|
|
assert.match(shell, /wp_ajax_menu_search/);
|
|
assert.match(shell, /wp_ajax_search_box/);
|
|
assert.match(shell, /wp_verify_nonce/);
|
|
assert.match(shell, /navbar-search/);
|
|
assert.match(source, /['"]trem['"]/);
|
|
assert.match(source, /['"]type['"]/);
|
|
assert.match(source, /['"]user['"]/);
|
|
assert.match(box, /history_search/);
|
|
assert.match(box, /array_slice\(\$history, 0, 30\)/);
|
|
assert.match(box, /search_keywords/);
|
|
assert.match(settings, /search_custom_key/);
|
|
assert.match(settings, /aksrt_search_terms\.name LIKE/);
|
|
assert.match(results, /search-tab-filter/);
|
|
assert.match(results, /aksrt_get_search_tab_nav_filter/);
|
|
assert.equal(exists('templates/parts/navbar-search.php'), false);
|
|
assert.equal(exists('templates/parts/search-panel.php'), false);
|
|
});
|
|
|
|
test('search settings preserve every original field ID and nested field type', () => {
|
|
const php = read('src/Admin/Schema/GlobalSchema.php');
|
|
const js = read('assets/admin/src/schema/sections/global/search.js');
|
|
const source = php + js;
|
|
for (const key of [
|
|
'search_placeholder', 'search_popular_key', 'search_popular_title',
|
|
'search_popular_sticky', 'search_popular_key_num', 'search_cat',
|
|
'search_cat_in', 'search_more_cat_obj', 'search_type', 'search_type_in',
|
|
'search_history', 'search_posts', '404_search_s', 'search_no_page',
|
|
'search_exclude_cats', 'search_custom_key_s', 'search_custom_key',
|
|
'search_facets', 'search_ms_s', 'search_ms_opt',
|
|
]) assert.ok(source.includes(key), `${key} missing`);
|
|
assert.match(js, /key: 'search_facets'[\s\S]*?type: 'fieldset'/);
|
|
assert.match(js, /key: 'search_ms_opt'[\s\S]*?type: 'fieldset'/);
|
|
assert.match(js, /key: 'synonyms'[\s\S]*?type: 'repeater'/);
|
|
});
|
|
|
|
test('header slider is the only top visual path and uses global plus page_config fields', () => {
|
|
const renderer = read('inc/helpers/homepage/header-components.php');
|
|
const globalSchema = read('src/Admin/Schema/SchemaPageDisplay.php');
|
|
const pageMeta = read('src/Admin/PageConfigMetaBox.php');
|
|
const defaults = read('src/Settings/DisplayPageGroups.php');
|
|
const source = renderer + globalSchema + pageMeta + defaults;
|
|
|
|
assert.match(renderer, /function aksrt_header_slide\(\): void/);
|
|
assert.match(renderer, /function aksrt_header_slide_is_show\(\): bool/);
|
|
assert.match(pageMeta, /page_config/);
|
|
for (const key of [
|
|
'header_slider_show', 'header_slider_show_type', 'header_slider_nav_color',
|
|
'header_slider', 'header_slider_option', 'header_slider_search_s',
|
|
'header_slider_search_option', 'header_slider_card_s',
|
|
'header_slider_card_option', 'header_slider_filter_blur',
|
|
]) assert.ok(source.includes(key), `${key} missing`);
|
|
assert.match(defaults, /slider-bg\.jpg/);
|
|
assert.match(defaults, /user_t\.jpg/);
|
|
assert.match(defaults, /\['pc_s', 'm_s'\]/);
|
|
assert.equal(exists('templates/parts/home-modules/module-hero.php'), false);
|
|
assert.equal(exists('assets/admin/src/pages/content/HeroManager.vue'), false);
|
|
assert.doesNotMatch(read('assets/admin/src/constants/sections/page-display.js'), /page-display-hero|HeroManager/);
|
|
assert.doesNotMatch(read('assets/admin/src/api/modules/page-display.js'), /getHeroes|createHero|updateHero|deleteHero/);
|
|
});
|
|
|
|
test('responsive contract pins the requested viewport matrix to reference breakpoints', () => {
|
|
const viewports = [375, 640, 768, 991, 1200, 1380, 1440];
|
|
const css = read('assets/css/main.min.css');
|
|
assert.deepEqual(viewports, [375, 640, 768, 991, 1200, 1380, 1440]);
|
|
for (const breakpoint of [375, 640, 768, 991, 1200, 1380]) {
|
|
assert.ok(css.includes(String(breakpoint)), `reference rule for ${breakpoint}px missing`);
|
|
}
|
|
assert.match(css, /@media\s*\(max-width:640px\)/);
|
|
assert.match(css, /@media\s*\(max-width:991px\)/);
|
|
assert.match(css, /@media\s*\(min-width:1200px\)/);
|
|
});
|
|
|
|
test('old navigation settings and runtime handles do not survive the hard switch', () => {
|
|
const source = join([
|
|
'src/Settings/Settings.php',
|
|
'src/Settings/FacadeBulk.php',
|
|
'src/Core/ScriptsPart1b.php',
|
|
'src/Core/ScriptsPart1c.php',
|
|
'assets/js/runtime/main.js',
|
|
]);
|
|
assert.doesNotMatch(source, /bootstrap5|aksrt-mini-touch|aksrt\.Drawer|aksrt\.minitouch|updateScrollBus/);
|
|
assert.doesNotMatch(source, /aksrt_nav_|legacy_nav|nav_compat/);
|
|
});
|