'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- 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/); assert.match(userboxCss, /\.aksrt-header-userbox\s*\{[^}]*line-height:\s*1\.42857143/s); assert.match(userboxCss, /\.aksrt-header-userbox\s+\.muted-box\.padding-6\s*\{[^}]*background-color:\s*var\(--this-bg,\s*var\(--color-line-soft\)\)[^}]*border-radius:\s*4px[^}]*padding:\s*6px/s); assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s); }); 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'); const buttonsCss = read('assets/css/core/buttons.css'); const userboxCss = read('assets/css/frontend/header-userbox.css'); // Stats row is wired with the reference wrapper and carries 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.match(identity, /tag-posts/); assert.match(identity, /fa fas fa-file-alt/); 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 em14 c-blue-2/); assert.match(header, /fa fas fa-coins em14 c-yellow/); // Message icon uses the reference markup. assert.match(header, /msg-news-icon abs-right/); assert.match(header, /toggle-radius msg-icon/); assert.match(header, //); // 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.match(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;/s); 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, /but block c-yellow is-done/); assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/); assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/); // The reference selector is a descendant, so the VIP chip inside its hover-show // wrapper receives the same .but geometry (font-size, padding, margin) as the // publish chip instead of falling back to the bare .but defaults. assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s); assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{/); assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/); assert.match(header, /
' \. \$checkin \. '<\/div>/); assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/); // The reference renders fa-calendar-check-o, an outline glyph; FA5 keeps that // look through the regular weight rather than the solid one. assert.match(header, /fa far fa-calendar-check/); assert.doesNotMatch(header, /fas fa-calendar-check/); // .but already gives its icon a 0.25em margin, so the popover entry carries no // margin class; the user-card badge keeps its own because it is not a .but. assert.match(header, /