fix(frontend): align top navigation with reference theme
This commit is contained in:
1 parent
1c501ff713
commit
9b27508a5b
12 files changed
+234
-607
No files matched your search
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
function renderHeaderParts() {
|
||||
const php = String.raw`
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
$GLOBALS['aksrt_test_user_id'] = 0;
|
||||
function esc_attr($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
|
||||
function esc_html($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
|
||||
function esc_attr__($value, $domain = null) { return esc_attr($value); }
|
||||
function esc_html__($value, $domain = null) { return esc_html($value); }
|
||||
function esc_url($value) { return $value; }
|
||||
function esc_url_raw($value) { return $value; }
|
||||
function home_url($path = '/') { return 'https://example.test' . $path; }
|
||||
function get_bloginfo($field = '') { return 'AKSRT'; }
|
||||
function get_current_user_id() { return (int) $GLOBALS['aksrt_test_user_id']; }
|
||||
function get_option($name, $default = false) { return $name === 'users_can_register'; }
|
||||
function aksrt_render_header_menu_buttons() { return '<a class="newadd-btns">发布</a>'; }
|
||||
function aksrt_get_translate_language_btn($class = '') { return '<a class="' . esc_attr($class) . '">语言</a>'; }
|
||||
function aksrt_header_user_box_html($ctx) { return '<div class="sub-user-box">账户</div>'; }
|
||||
function aksrt_get_avatar_html($userId, $size, $args = []) { return '<img class="avatar" width="' . (int) $size . '">'; }
|
||||
function has_nav_menu($location) { return false; }
|
||||
function is_super_admin() { return false; }
|
||||
function is_singular($type = '') { return false; }
|
||||
require __DIR__ . '/inc/helpers/header/aksrt-header-navbar.php';
|
||||
require __DIR__ . '/inc/helpers/header/aksrt-header-mobile.php';
|
||||
|
||||
$results = [];
|
||||
foreach (['1', '2', '3'] as $layout) {
|
||||
foreach ([0, 7] as $userId) {
|
||||
$GLOBALS['aksrt_test_user_id'] = $userId;
|
||||
$ctx = [
|
||||
'layout' => $layout,
|
||||
'user_id' => $userId,
|
||||
'theme_positions' => ['pc_nav', 'm_menu'],
|
||||
'message_icon_positions' => [],
|
||||
'messages_url' => '',
|
||||
'unread_count' => 0,
|
||||
'mobile_layout' => 'center',
|
||||
'brand_name' => 'AKSRT',
|
||||
'logo_html' => '',
|
||||
];
|
||||
ob_start(); aksrt_menu_button($ctx); $buttons = ob_get_clean();
|
||||
$results['desktop'][$layout][$userId] = $buttons;
|
||||
}
|
||||
}
|
||||
foreach (['center', 'left'] as $mobileLayout) {
|
||||
$ctx = [
|
||||
'mobile_layout' => $mobileLayout,
|
||||
'mobile_align' => 'left',
|
||||
'theme_positions' => [],
|
||||
'brand_name' => 'AKSRT',
|
||||
'logo_html' => '',
|
||||
'user_id' => 0,
|
||||
];
|
||||
ob_start(); aksrt_navbar_header($ctx); $results['mobile'][$mobileLayout]['header'] = ob_get_clean();
|
||||
ob_start(); aksrt_nav_mobile_content($ctx); $results['mobile'][$mobileLayout]['drawer'] = ob_get_clean();
|
||||
}
|
||||
echo json_encode($results, JSON_UNESCAPED_SLASHES);
|
||||
`;
|
||||
const result = spawnSync('php', ['-r', php], { cwd: root, encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
test('rendered desktop layouts retain reference control order for guests and members', () => {
|
||||
const output = renderHeaderParts();
|
||||
for (const layout of ['1', '2', '3']) {
|
||||
for (const userId of ['0', '7']) {
|
||||
const html = output.desktop[layout][userId];
|
||||
assert.match(html, /class="but" aria-label=/, `layout ${layout} menu fold`);
|
||||
assert.match(html, /class="list-inline splitters relative"/, `layout ${layout} account list`);
|
||||
assert.doesNotMatch(html, /aksrt-navbar-account|aksrt-nav-avatar/);
|
||||
if (layout === '2' && userId === '0') assert.match(html, /class="signin-loader"/);
|
||||
else assert.match(html, /class="aksrt-nav-icon[^"]*"|class="navbar-avatar"/);
|
||||
const right = html.indexOf('list-inline splitters relative');
|
||||
const actions = html.indexOf('navbar-but');
|
||||
if (layout === '3' && actions !== -1) assert.ok(right < actions, 'layout 3 puts account group before actions');
|
||||
if (layout !== '3' && actions !== -1) assert.ok(actions < right, `layout ${layout} puts actions before account group`);
|
||||
}
|
||||
}
|
||||
assert.match(output.desktop['2']['0'], /signin-loader/);
|
||||
assert.match(output.desktop['2']['7'], /class="navbar-avatar"/);
|
||||
assert.match(output.desktop['1']['0'], /#icon-user/);
|
||||
assert.match(output.desktop['3']['7'], /#icon-user/);
|
||||
});
|
||||
|
||||
test('mobile search appears in the centered header or left drawer according to layout', () => {
|
||||
const output = renderHeaderParts();
|
||||
assert.match(output.mobile.center.header, /class="navbar-toggle main-search-btn"/);
|
||||
assert.doesNotMatch(output.mobile.center.drawer, /main-search-btn/);
|
||||
assert.doesNotMatch(output.mobile.left.header, /main-search-btn/);
|
||||
assert.match(output.mobile.left.drawer, /class="toggle-radius main-search-btn"/);
|
||||
});
|
||||
|
||||
test('header source restores the reference typography and Bootstrap control dimensions', () => {
|
||||
const css = read('assets/css/frontend/header-userbox.css');
|
||||
const mainCss = read('assets/css/main.css');
|
||||
assert.match(css, /\.header,\s*\.mobile-header,\s*\.navbar-search\s*\{[^}]*font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif;[^}]*font-size:\s*14px;[^}]*font-weight:\s*400;[^}]*line-height:\s*1\.42857143;[^}]*-webkit-font-smoothing:\s*auto;[^}]*text-rendering:\s*auto;/s);
|
||||
assert.match(css, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
|
||||
assert.match(mainCss, /\.navbar-avatar\s*\{[^}]*margin-top:\s*3px;[^}]*width:\s*29px;[^}]*height:\s*29px;/s);
|
||||
assert.doesNotMatch(css, /\.aksrt-navbar-account|\.aksrt-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.navbar-search(?:\.show)?\s*\{[^}]*transform:/s);
|
||||
});
|
||||
@@ -166,7 +166,8 @@ test('guest account member VIP and publish contracts share reference header DOM'
|
||||
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(header, /fa far fa-toggle-theme/);
|
||||
assert.doesNotMatch(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/);
|
||||
@@ -180,21 +181,14 @@ test('guest account member VIP and publish contracts share reference header DOM'
|
||||
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 top-nav midline chip rule leaves the popover social buttons alone', () => {
|
||||
test('the top navigation keeps the reference Bootstrap control geometry', () => {
|
||||
const userboxCss = read('assets/css/frontend/header-userbox.css');
|
||||
const publicCss = read('assets/css/build/public.css');
|
||||
|
||||
// The account-row chips, the theme toggle and the language toggle all want a
|
||||
// 29px midline circle. The popover's quick-login buttons are .toggle-radius
|
||||
// too and also live inside .navbar-form, but they must keep the
|
||||
// .social_loginbar geometry from main.css. flex-basis outranks width in a
|
||||
// flex container and the bare rule is (0,3,0) against (0,2,0), so an
|
||||
// unscoped selector collapses the buttons to 29px and zeroes their margins.
|
||||
assert.match(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)\s*\{[^}]*flex:\s*0 0 29px;[^}]*margin:\s*0;/s);
|
||||
assert.doesNotMatch(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius\s*\{/);
|
||||
// header-userbox.css is also folded into the public bundle, so the exemption
|
||||
// has to survive the build.
|
||||
assert.match(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)/);
|
||||
assert.match(userboxCss, /\.header,\s*\.mobile-header,\s*\.navbar-search\s*\{[^}]*font-family:\s*['"]Helvetica Neue['"],\s*Helvetica,\s*Arial,\s*sans-serif;[^}]*font-weight:\s*400;[^}]*line-height:\s*1\.42857143;/s);
|
||||
assert.match(userboxCss, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s);
|
||||
assert.doesNotMatch(userboxCss, /\.aksrt-navbar-account|\.aksrt-nav-avatar|\.navbar-form \.toggle-radius:not|navbar-form\.flex\.ac\s*\{[^}]*min-height|\.aksrt-navbar-language\s*\{[^}]*margin:/);
|
||||
assert.doesNotMatch(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\)/);
|
||||
});
|
||||
|
||||
test('route and feature modules do not restyle the legacy top-nav chips', () => {
|
||||
@@ -278,14 +272,15 @@ test('the account popover rows match the reference user box structure', () => {
|
||||
|
||||
// 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(header, /aksrt_get_user_checkin_details_link\([\s\S]*?'but block c-yellow'/);
|
||||
assert.match(header, /checkin-details-link/);
|
||||
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.
|
||||
// The hover wrapper uses the reference inline-block utility so the absolutely
|
||||
// positioned VIP card keeps its original static position.
|
||||
assert.match(read('inc/helpers/aksrt-header-menu-buttons.php'), /class="hover-show inline-block"/);
|
||||
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(read('assets/css/frontend/header-userbox.css'), /\.navbar-top \.navbar-but > \.hover-show/);
|
||||
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
|
||||
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
|
||||
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
|
||||
|
||||
@@ -169,7 +169,8 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
|
||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
|
||||
// The hover wrapper must stay a plain block container so the legacy static
|
||||
// position of .sub-vip-card matches the reference theme.
|
||||
assert.match(headerCss, /\.navbar-top \.navbar-but > \.hover-show\s*\{[^}]*display:\s*inline-block;/s)
|
||||
assert.match(headerMenuButtons, /class="hover-show inline-block"/)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-top \.navbar-but > \.hover-show/)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
|
||||
})
|
||||
@@ -350,20 +351,19 @@ test('user profile and author widgets render the reference user-card contract',
|
||||
test('desktop header actions share one row without generic button masks', () => {
|
||||
assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
|
||||
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
|
||||
assert.match(headerNavbar, /\$fold \. \$button \. \$right_but/)
|
||||
assert.match(headerNavbar, /\$fold \. \('3' === \$layout \? \$right_but \. \$button : \$button \. \$right_but\)/)
|
||||
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
|
||||
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
|
||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*display:\s*inline-flex;[^}]*align-items:\s*center;/s)
|
||||
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
|
||||
// Bootstrap's base .navbar-form rule keeps margin-top: 8px at >=768px (the
|
||||
// override only zeroes the horizontal margins), so every right-cluster group
|
||||
// starts 8px below the top of .navbar-collapse. A 34px cross-size therefore
|
||||
// puts each group's midline on the navbar's 25px line, where the 50px menu row
|
||||
// and the logo already sit. A 50px min-height double-counts that 8px: the
|
||||
// group's content drops 8px below the line and the navbar grows from 50px to
|
||||
// 58px, which also breaks the body.nav-fixed offset.
|
||||
assert.doesNotMatch(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*min-height/s)
|
||||
assert.match(headerCss, /\.navbar-top \.navbar-form\.flex\.ac\s*\{[^}]*min-height:\s*34px;/s)
|
||||
assert.match(headerNavbar, /<ul class="list-inline splitters relative">/)
|
||||
assert.doesNotMatch(headerNavbar, /aksrt-navbar-account|aksrt-nav-avatar/)
|
||||
assert.match(headerNavbar, /class="navbar-avatar"/)
|
||||
assert.match(legacyMainCss, /\.navbar-avatar\s*\{[^}]*margin-top:\s*3px;[^}]*width:\s*29px;[^}]*height:\s*29px;/s)
|
||||
assert.match(headerNavbar, /aksrt_get_translate_language_btn\('ml10 pull-right'\)/)
|
||||
assert.match(headerNavbar, /<a data-toggle-class data-target="\.nav\.navbar-nav" href="javascript:;" class="but"/)
|
||||
assert.match(headerNavbar, /<svg class="icon" viewBox="50 0 924 924"/)
|
||||
assert.match(headerNavbar, /<svg class="icon" aria-hidden="true"><use xlink:href="#icon-search"/)
|
||||
assert.doesNotMatch(headerCss, /\.aksrt-navbar-account|\.aksrt-nav-avatar|navbar-form\.flex\.ac\s*\{[^}]*min-height/)
|
||||
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon\s*\{[^}]*display:\s*inline-block;[^}]*padding:\s*6px 12px;[^}]*font-size:\s*14px;[^}]*line-height:\s*1\.42857143;[^}]*border:\s*1px solid transparent;[^}]*border-radius:\s*4px;/s)
|
||||
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
|
||||
// the header module may restate their padding, margin, radius or icon size.
|
||||
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
|
||||
@@ -371,15 +371,14 @@ test('desktop header actions share one row without generic button masks', () =>
|
||||
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||
// No merged flex container: the three groups keep their own .navbar-form
|
||||
// padding, which is what produces the reference inter-cluster spacing.
|
||||
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions/)
|
||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
|
||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
|
||||
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions|\.aksrt-navbar-account|\.aksrt-nav-avatar/)
|
||||
})
|
||||
|
||||
test('mobile search stays inside the scroll-hiding top header', () => {
|
||||
test('mobile search follows the reference center and left header placements', () => {
|
||||
assert.match(headerNavbar, /class="navbar-toggle main-search-btn"/)
|
||||
assert.doesNotMatch(headerNavbar, /mobile_layout/)
|
||||
assert.doesNotMatch(headerMobile, /main-search-btn|\$m_layout|aksrt_navbar_header/)
|
||||
assert.match(headerNavbar, /mobile_layout/)
|
||||
assert.match(headerNavbar, /'center' === \(\$ctx\['mobile_layout'\] \?\? 'center'\)/)
|
||||
assert.match(headerMobile, /main-search-btn|toggle-radius/)
|
||||
assert.match(headerMobile, /aksrt_nav_mobile_content\(\$ctx\)/)
|
||||
assert.doesNotMatch(headerRender, /'fixed'\s*=>/)
|
||||
assert.doesNotMatch(legacyMainCss, /\.mobile-navbar \.navbar-(?:header|brand)/)
|
||||
@@ -394,14 +393,12 @@ test('mobile search stays inside the scroll-hiding top header', () => {
|
||||
assert.match(legacyMainCss, /\.scroll-down\.nav-fixed \.header\.scroll-down-hide\s*\{[^}]*transform:\s*translate3d\(0, -105%, 0\);/s)
|
||||
})
|
||||
|
||||
test('search dropdown clears the fixed header and closes from the panel lower-right', () => {
|
||||
assert.match(headerCss, /\.navbar-search\s*\{[^}]*z-index:\s*1001;[^}]*transform:\s*translate3d\(0, -24px, 0\);[^}]*transition:\s*transform \.24s ease, opacity \.18s ease, visibility 0s linear \.24s;/s)
|
||||
assert.match(headerCss, /\.navbar-search\.show\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translate3d\(0, 0, 0\);/s)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-search\s*\{[^}]*height:\s*100%/s)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-search > \.container\s*\{[^}]*overflow-y:\s*auto/s)
|
||||
test('search panel retains the reference main.css surface and close spacing', () => {
|
||||
assert.match(legacyMainCss, /\.navbar-search\s*\{[^}]*visibility:\s*hidden;[^}]*transform:\s*translateY\(-101%\);[^}]*padding:\s*10px 30px;[^}]*padding-top:\s*80px;[^}]*transition:\s*all 0\.5s;/s)
|
||||
assert.match(legacyMainCss, /\.navbar-search\.show\s*\{[^}]*visibility:\s*unset;[^}]*transform:\s*translateY\(0\);/s)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-search(?:\.show)?\s*\{[^}]*transform:/s)
|
||||
assert.match(headerCss, /\.navbar-search \.line-form-input:focus,[\s\S]*?\.navbar-search \.line-form-input:focus-visible\s*\{[^}]*outline:\s*0;[^}]*box-shadow:\s*none;/s)
|
||||
assert.match(headerCss, /\.navbar-search \.search-close-box\s*\{[^}]*display:\s*flex;[^}]*justify-content:\s*flex-end;[^}]*margin:\s*16px auto 0;/s)
|
||||
assert.doesNotMatch(headerCss, /\.navbar-search \.search-close-box\s*\{[^}]*position:\s*fixed/s)
|
||||
assert.match(headerSearch, /class="mb20 search-close-box"/)
|
||||
assert.ok(headerSearch.indexOf('echo $remote') < headerSearch.indexOf('search-close-box'))
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user