Files
wordpress-theme-aksrt/tests/widget-system/auth-source-contract.test.cjs
T
kate522 00ae62f62b fix(auth): close the modal regressions and keep the header component off /sign/
Three defects slipped into the sign-card rebuild:

The hidden-glyph rule was dropped. `.fa` sets display:inline-block, which
outranks the UA [hidden] rule, so every hidden glyph stayed visible — most
visibly a spinner rendered beside the login button at rest. `.sign [hidden]`
restores the override.

Closing account login produced an empty shell. The whole sign-in pane was
gated on `!$close_signin`, and the social block carried the same gate, so a
site with account login off but registration on still opened on an empty
panel with no social entry. The pane is now rendered in three shapes: open
registration falls through to the signup tab, a QR entry falls through to
the QR tab, social-only renders a notice plus the social bar, and when
nothing is available it states that login is closed.

The modal close button sat in normal flow and pushed the card content down;
`#u_sign .sign > .close` pins it back to the top-right corner.

The /sign/ route is `index.php?jio_sign=1`, so WordPress reports it as home.
The header multifunction component resolves page type by query flags and the
home branch won, so the component appeared on the auth page whenever "首页"
was in its scope — even though the sign page template itself was already
excluded. `jio_header_slide_current_page_type()` now bails out on `jio_sign`
before any of the is_* checks.

The breakpoint overrides that were fighting the shared sheet between 641px
and 991px are narrowed to the one offset that actually needed adjusting.

Verification: the modal was rendered through a PHP stub, served with the real
theme stylesheets, and driven by the real auth.js in a headless browser —
tab switching, signin-mode switching, password reveal, close button and
geometry at 1440/1100/900/760/500/390px. The header harness gained a
`jio_sign` case asserting no markup, and 860 widget/navigation tests pass.
2026-11-04 16:51:41 +08:00

324 lines
17 KiB
JavaScript

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');
test('modal and sign page use one renderer and one state machine', () => {
const modal = read('templates/parts/modal-auth.php');
const page = read('templates/pages/page-sign.php');
const shared = read('templates/parts/auth-content.php');
const scripts = read('src/Core/ScriptsPart1c.php');
const runtime = read('assets/js/runtime/main.js') + read('assets/js/runtime/main.min.js');
const loader = read('assets/js/runtime/loader.js');
assert.match(modal, /templates\/parts\/auth-content\.php/);
assert.match(page, /templates\/parts\/auth-content\.php/);
assert.match(shared, /data-auth-panel="signin"/);
assert.match(shared, /data-auth-panel="signup"/);
assert.match(shared, /data-auth-panel="resetpassword"/);
assert.match(shared, /data-auth-panel="bind"/);
assert.match(shared, /data-auth-panel="oauth"/);
assert.match(scripts, /assets\/js\/auth\.js/);
assert.match(scripts, /'jio-auth',[\s\S]*?assets\/js\/auth\.js[\s\S]*?\['loader_js'\]/);
assert.doesNotMatch(scripts, /modal-auth\.js|sign-register\.js/);
assert.doesNotMatch(runtime, /signsubmit-loader|tbquire\(\['sign-register'\]\)|\.on\('click', '\.(?:signin|signup)-loader'/);
assert.doesNotMatch(loader, /sign-register/);
});
test('authentication shell reuses the shared sign-card CSS contract', () => {
// Card width, the 800px split dialog, the 55% card offset, the 32px social
// items and the QR box all live in the shared theme stylesheet now.
const mainCss = read('assets/css/main.css');
const signCss = read('assets/css/routes/sign.css');
assert.match(mainCss, /\.sign\s*\{[\s\S]*?max-width:\s*330px/);
assert.match(mainCss, /#u_sign \.modal-dialog\s*\{\s*width:\s*800px;/);
assert.match(mainCss, /\.sign-img \+ \.sign\s*\{[\s\S]*?margin-left:\s*55%/);
assert.match(mainCss, /\.social-login-item\s*\{[\s\S]*?width:\s*32px !important;[\s\S]*?height:\s*32px !important/);
assert.match(mainCss, /\.qrcode-signin-container\s*\{[\s\S]*?min-height:\s*254px/);
assert.match(mainCss, /\.signin-qrcode-img\s*\{[\s\S]*?width:\s*180px/);
assert.match(mainCss, /#u_sign \.modal-dialog\s*\{\s*width:\s*800px;/);
assert.match(signCss, /\.sign-page \.sign\s*\{\s*width:\s*350px/);
// Hidden glyphs must stay hidden even though .fa forces display:inline-block.
assert.match(signCss, /\.sign \[hidden\]\s*\{\s*display:\s*none !important;/);
});
test('authentication settings use reference defaults and canonical keys', () => {
const js = read('assets/admin/src/schema/sections/users/auth.js');
const php = read('src/Admin/Schema/SchemaUsers.php');
const all = js + '\n' + php;
assert.match(all, /user_sign_type['"]?\s*(?:=>|:)\s*['"]modal/);
assert.match(all, /user_signup_captch['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /sign_card_position['"]?\s*(?:=>|:)\s*['"]right/);
assert.match(all, /sign_show_logo['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /sign_modal_show_logo['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /invit_code_s/);
assert.match(all, /only_nopas/);
assert.match(all, /user_signup_no_repas[\s\S]*?dependsOn[\s\S]*?user_signup_captch/);
assert.doesNotMatch(all, /user_close_login|user_close_signup|nopas_first|password_first|nopas_only|referral_registration_enabled|referral_register_field/);
});
test('authentication galleries save attachment IDs and are consumed by both surfaces', () => {
const field = read('assets/admin/src/components/form/JioField.vue');
const gallery = read('assets/admin/src/components/form/JioGalleryField.vue');
const provider = read('src/Admin/SchemaProvider.php');
const reader = read('src/Settings/AuthReader.php');
const modal = read('templates/parts/modal-auth.php');
const page = read('templates/pages/page-sign.php');
const css = read('assets/css/routes/sign.css');
assert.match(field, /<JioGalleryField[\s\S]*?schema\.type === 'gallery'/);
assert.match(gallery, /multiple:\s*true/);
assert.match(gallery, /Number\.parseInt\(attachment\.id/);
assert.match(provider, /'gallery'\s*=>\s*'attachment_ids'/);
assert.match(provider, /case 'attachment_ids':[\s\S]*?array_map\('absint'/);
assert.match(reader, /auth_gallery_ids\('sign_modal_background'\)/);
assert.match(reader, /auth_gallery_ids\('sign_page_background'\)/);
assert.match(modal, /wp_get_attachment_image\(\$image_id, 'full'/);
assert.match(modal, /<div class="sign-img absolute hide-sm"><\?php echo \$image_html/);
assert.match(page, /wp_get_attachment_image_url\(\$background_id, 'full'\)/);
assert.match(page, /<div class="fixed lazyload" data-bg="<\?php echo esc_url\(\$background_url\)/);
assert.match(css, /#u_sign \.sign-img img\s*\{[\s\S]*?object-fit:\s*cover/);
assert.doesNotMatch(modal + page, /is_string\(\$images\)|explode\(',', \$images\)/);
});
test('only the shared authentication REST routes and /sign/ renderer are registered', () => {
const controller = read('src/Admin/UserController.php');
const router = read('src/Auth/Router.php');
const urls = read('inc/modules/user-parts/urls.php');
['/auth/code', '/auth/login', '/auth/register', '/auth/reset-password'].forEach((route) => {
assert.match(controller, new RegExp(route.replace('/', '\\/')));
});
assert.doesNotMatch(controller, /auth\/(?:modal-|phone-|send-sms-code|bind-phone)/);
assert.match(router, /'sign'\s*=>\s*'index\.php\?jio_sign=1'/);
assert.doesNotMatch(router, /'login'\s*=>|'register'\s*=>|'reset-password'\s*=>|'verify-email'\s*=>|'bind-account'\s*=>/);
assert.match(router, /jio_bypass/);
assert.match(router, /\$action === 'logout'[\s\S]*?jio_get_logout_url/);
assert.doesNotMatch(router, /is_user_logged_in\(\) && current_user_can\('manage_options'\)|wp_doing_ajax\(\)/);
assert.match(urls, /home_url\('\/sign\/'\)/);
});
test('legacy authentication templates and scripts are removed', () => {
[
'assets/js/modal-auth.js',
'assets/js/sign-register.js',
'assets/js/runtime/sign-register.js',
'assets/js/runtime/sign-register.min.js',
'assets/css/routes/auth.css',
'assets/css/frontend/enhancements/modal-motion.css',
'templates/pages/page-login.php',
'templates/pages/page-register.php',
'templates/pages/page-reset-password.php',
'templates/pages/page-bind-account.php',
'templates/pages/page-verify-email.php',
].forEach((file) => assert.equal(fs.existsSync(path.join(root, file)), false, file));
});
test('unified handlers enforce nonce, captcha, rate limits, configured channels, and generic errors', () => {
const handlers = read('src/Admin/UserControllerAuthPhoneOperations.php');
const service = read('src/Auth/Service.php');
const serviceA = read('src/Auth/ServicePart1a.php');
const serviceB = read('src/Auth/ServicePart1b.php');
assert.match(handlers, /auth_verify_rest_nonce/);
assert.match(handlers, /private function auth_require_captcha/);
assert.match(handlers, /get_error_code\(\)/);
assert.match(handlers, /get_error_message\(\)/);
assert.match(handlers, /auth_rate_limited/);
assert.match(handlers, /auth_channel_allowed/);
['auth_code', 'auth_login', 'auth_register', 'auth_reset_password'].forEach((method) => {
const start = handlers.indexOf(`public function ${method}(`);
const end = handlers.indexOf('\n\tpublic function ', start + 1);
const body = handlers.slice(start, end === -1 ? handlers.length : end);
assert.match(body, /auth_require_captcha/, method);
});
assert.match(handlers, /passwordless_login_type/);
assert.match(handlers, /signup_verification_type/);
assert.match(handlers, /reset_password_verification_type/);
assert.match(handlers, /账号或密码错误/);
assert.match(handlers, /账号或验证码错误/);
assert.match(service, /signin_phone_enabled/);
assert.doesNotMatch(service + serviceA + serviceB, /jio_require_captcha/);
assert.doesNotMatch(service + serviceA + serviceB, /_auth_code_verified|_defer_login/);
assert.doesNotMatch(service, /captcha_scene/);
assert.match(handlers, /public function user_bind_phone[\s\S]*?auth_require_captcha\('bind_phone'/);
assert.doesNotMatch(service, /login_by_phone_password|login_by_phone_code/);
});
test('OAuth bind and reset flows stay inside the shared page and four auth endpoints', () => {
const shared = read('templates/parts/auth-content.php');
const page = read('templates/pages/page-sign.php');
const handlers = read('src/Admin/UserControllerAuthPhoneOperations.php');
const serviceA = read('src/Auth/ServicePart1a.php');
const serviceB = read('src/Auth/ServicePart1b.php');
assert.match(page, /'obk'\s*=>\s*\$obk/);
assert.match(shared, /data-auth-panel="bind"[\s\S]*?data-auth-form="login"/);
assert.match(shared, /name="obk"/);
assert.match(handlers, /finalize_oauth_binding/);
assert.match(serviceA, /finalize_oauth_binding/);
assert.doesNotMatch(serviceA + serviceB, /bind_oauth_account|get_reset_context|request_password_reset|reset_password_with_token/);
});
test('shared renderer contracts every configured authentication state', () => {
const shared = read('templates/parts/auth-content.php');
const modal = read('templates/parts/modal-auth.php');
const client = read('assets/js/auth.js');
assert.match(shared, /data-signin-panel="nopas"/);
assert.match(shared, /data-signin-panel="pas"/);
assert.match(shared, /data-signin-mode="pas"/);
['login', 'register', 'resetpassword'].forEach((scene) => {
assert.match(shared, new RegExp(`data-send-code="${scene}"`));
});
assert.match(shared, /\$omit_confirm/);
assert.match(shared, /\$ref_mode !== 'hidden'/);
assert.match(shared, /\$ref_mode === 'required'/);
assert.match(shared, /data-invite-gate-panel/);
assert.match(shared, /data-invite-form-panel/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.match(shared, /render_social_buttons\(__\('社交账号登录'/);
assert.match(shared, /data-auth-qrcode/);
assert.match(modal, /data-auto=/);
assert.match(client, /jioAuthAutoUntil/);
assert.match(client, /data-password-show/);
assert.match(client, /pollQr/);
assert.match(client, /AbortController/);
assert.match(client, /refreshInFlight|inFlight/);
assert.match(client, /visibilitychange/);
assert.match(client, /Math\.pow\(2/);
assert.match(client, /QR_TIMEOUT/);
assert.match(client, /stopQr\(root\)/);
assert.match(client, /window\.notyf\(escapeHtml\(text\), type \|\| 'info'\)/);
assert.match(client, /notify\('请先完成人机验证', 'warning'\)/);
assert.match(client, /form\.noValidate = true/);
assert.match(client, /form\.checkValidity\(\)[\s\S]*?field\.validationMessage/);
assert.match(client, /window\.location\.assign\(authPageUrl/);
assert.doesNotMatch(client + shared, /showMessage|data-auth-message|sign-message/);
assert.match(read('src/Settings/CreditsRuntime.php'), /invit_code_s/);
assert.match(read('src/Settings/CreditsRuntime.php'), /\['close', 'open', 'must'\]/);
});
test('OAuth frontend is accessible, redirect-aware, and removes retired providers', () => {
const shared = read('templates/parts/auth-content.php');
const modal = read('templates/parts/modal-auth.php');
const client = read('assets/js/auth.js');
const userCenter = read('assets/js/routes/user-center.js');
const bindings = read('inc/helpers/user-center-renderers/settings-bindings.php');
const signCss = read('assets/css/routes/sign.css');
const mainCss = read('assets/css/main.css');
assert.match(modal, /role="dialog"[^>]*aria-modal="true"[^>]*aria-labelledby="jio-auth-modal-title"/);
assert.match(modal, /id="jio-auth-modal-title"/);
assert.match(client, /focusableModalItems/);
assert.match(client, /lastModalTrigger/);
assert.match(client, /restore\.focus\(\)/);
assert.match(shared, /role="tabpanel"[^>]*data-auth-panel="signin"/);
assert.match(shared, /role="tabpanel"[^>]*data-auth-panel="oauth"/);
assert.match(client, /setAttribute\('role', 'tab'\)/);
assert.match(client, /aria-controls/);
assert.match(client, /aria-selected/);
assert.match(shared, /data-password-show[^>]*aria-label=/);
assert.match(shared, /aria-pressed="false"/);
assert.match(client, /setAttribute\('aria-label', visible \? '隐藏密码' : '显示密码'\)/);
assert.match(client, /setAttribute\('aria-pressed', visible \? 'true' : 'false'\)/);
assert.match(shared, /data-auth-qr-status[^>]*role="status"[^>]*aria-live="polite"/);
assert.match(read('src/Auth/OAuth.php'), /data-provider="' \. esc_attr\(\$name\)/);
assert.match(read('src/Auth/OAuth.php'), /aria-label="' \. esc_attr\(sprintf/);
assert.match(client, /searchParams\.set\('redirect_to', redirect\)/);
assert.match(client, /oauth_action/);
assert.match(client, /oauth_provider/);
assert.match(client, /oauth_error/);
assert.match(client, /oauthErrorMessages/);
assert.doesNotMatch(client, /notify\([^\n;]*searchParams\.get\('oauth_error'/);
assert.match(userCenter, /errorMessages\[currentUrl\.searchParams\.get\('oauth_error'\)/);
assert.match(userCenter, /result\.status === 409/);
assert.match(userCenter, /d\.message \|\| \(d\.data && d\.data\.message\)/);
assert.match(bindings, /\$binding\['provider'\] \?\? \$binding\['key'\]/);
assert.match(bindings, /\$binding\['start_url'\] \?\? \$binding\['url'\]/);
assert.match(bindings, /data-provider=/);
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
assert.match(mainCss, /\.pay-box \.but\.baidu/);
});
test('agreement copy sits under each panel, not under the user card', () => {
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
// The sign-in column states the social agreement, the registration panel
// states its own, and the compact user card carries neither line.
assert.match(shared, /使用社交账号登录即表示同意/);
assert.match(shared, /扫码登录即表示同意/);
assert.match(shared, /注册即表示同意/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
});
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
const icons = read('inc/helpers/jio-icons.php');
const sprites = read('assets/js/runtime/svg-icon.js');
// 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*'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="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', () => {
const oauth = read('src/Auth/OAuth.php');
const header = read('inc/helpers/header/jio-header-navbar.php');
const links = read('inc/helpers/jio-links.php');
const legacyCss = read('assets/css/frontend/pages/base-1.css');
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
assert.match(oauth, /'<div class="social_loginbar">'/);
assert.match(oauth, /'social-login-item ' \. \$type/);
assert.match(oauth, /' button-lg' : ' toggle-radius'/);
assert.match(oauth, /rel="nofollow"/);
assert.match(oauth, /qrcode-signin/);
assert.match(oauth, /oauth_button_lg/);
// Callers append the block verbatim — no hand-rolled separator or wrapper.
assert.match(header, /render_social_buttons\(__\('快速登录', 'jio'\)\)/);
assert.doesNotMatch(header, /social-separator|social_loginbar/);
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'jio'\)\)/);
assert.doesNotMatch(links, /social_loginbar/);
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
// stylesheet are gone.
assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/);
assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/);
});
test('provider URLs stay raw in the data layer and are escaped once per surface', () => {
const oauth = read('src/Auth/OAuth.php');
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
const comments = read('inc/helpers/jio-comments.php');
// get_auth_providers_data() hands out raw URLs, exactly like its sibling
// get_frontend_bindings_data() does. esc_url() is idempotent on its own
// output, so escaping here as well was redundant rather than broken.
assert.doesNotMatch(oauth, /'url'\s*=>\s*esc_url\(\$url\)/);
assert.match(oauth, /'url'\s*=>\s*\$url,/);
// Every consumer still escapes on output, so the rendered href is unchanged.
assert.match(oauth, /href="' \. esc_url\(\$url\) \. '" class="' \. esc_attr\(\$class\)/);
assert.match(userRenderer, /href="' \. esc_url\(\$url\)/);
assert.match(oauth, /'<a rel="nofollow" href="' \. esc_url\(\$url\)/);
assert.match(comments, /<a href="<\?php echo esc_url\(\$url\); \?\>" class="but hollow social-login-item ml10">/);
});