Make --radius-avatar the single source of truth for frontend user avatar shape. A new primitive in components-ext.css (img.avatar / avatar-mini / avatar-md / avatar-lg / .avatar-img img) forces a circle on every avatar image, which covers the bare avatar-mini in the ban and report modals, the classless img inside the VIP modal's .avatar-img, and the post-card meta avatar that previously depended on post-card.css being enqueued. The user center header frame, the public user home hero and the friends page avatar switch to the same token because route CSS loads after the public bundle and would otherwise override the primitive. The VIP icon no longer rides the avatar corner. The user card widget, the after-article author card and the sign-in modal stop opting into aksrt_get_avatar_box()'s corner badge, and the icon renders first in the badge/level row instead, since aksrt_get_user_name already orders name -> VIP -> auth -> medal -> level. The sign-in modal appends it after the display name and keeps the legacy precedence where a ban badge takes the VIP slot. The user card widget name row now shrinks its display name so the extra icon cannot overflow a narrow sidebar. Verified with the new frontend-avatar-shape-contract test plus post-visual, user-card-runtime, circular-button-icon-margin, user-features and page-display contracts, and php -l on every changed PHP file. The public bundle was rebuilt with tools/build-public-assets.mjs.
882 lines
56 KiB
JavaScript
882 lines
56 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 articleFiles = [
|
||
'src/Settings/PostDisplay.php',
|
||
'src/Admin/Schema/PostList/SchemaPostListList.php',
|
||
'src/Admin/Schema/SchemaPostList.php',
|
||
'assets/admin/src/schema/sections/post-list/list.js',
|
||
'assets/admin/src/schema/sections/post-list/thumb.js',
|
||
'assets/admin/src/schema/sections/post-list/page.js',
|
||
'inc/helpers/posts-renderer/config.php',
|
||
'inc/helpers/posts-renderer/post-data.php',
|
||
'inc/helpers/posts-renderer/markup.php',
|
||
'inc/helpers/aksrt-archive.php',
|
||
'inc/helpers/aksrt-single-render.php',
|
||
];
|
||
|
||
test('the article stream exposes only the new typed renderer contract', () => {
|
||
const markup = read('inc/helpers/posts-renderer/markup.php');
|
||
const consumers = join([
|
||
'index.php',
|
||
'category.php',
|
||
'tag.php',
|
||
'search.php',
|
||
'archive-resources.php',
|
||
'src/Admin/Admin.php',
|
||
'src/Admin/GlobalController.php',
|
||
'src/Core/Shortcodes.php',
|
||
'src/WidgetSystem/PostRenderer.php',
|
||
'inc/helpers/homepage/content.php',
|
||
'templates/parts/user-home/panel-posts.php',
|
||
'templates/parts/user-home/panel-favorites.php',
|
||
]);
|
||
|
||
assert.match(markup, /function aksrt_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/);
|
||
assert.doesNotMatch(consumers, /function_exists\(['"]aksrt_render_post_list/);
|
||
assert.doesNotMatch(consumers, /aksrt_render_post_list\(\s*\$query\s*,/);
|
||
assert.doesNotMatch(consumers, /content-post-card|aksrt_homepage_render_post_card/);
|
||
|
||
// 搜索页的加载态必须是文章同构骨架,而不是遗留的转圈 spinner,
|
||
// 这样 runtime main.js 的 list 型 loader 形状才与搜索结果一致。
|
||
assert.match(read('search.php'), /post_ajax_loader search-loader"><\?php echo function_exists\('aksrt_render_placeholder'\) \? aksrt_render_placeholder\('list', 6\)/);
|
||
assert.doesNotMatch(read('search.php'), /class="loading mr10"/);
|
||
});
|
||
|
||
test('settings use legacy list modes, grouped options, and destructive related keys', () => {
|
||
const source = join(articleFiles);
|
||
const listSchema = read('assets/admin/src/schema/sections/post-list/list.js');
|
||
const pageSchema = read('assets/admin/src/schema/sections/post-list/page.js');
|
||
const legacyKeys = [
|
||
'post_card_style',
|
||
'list_list_style',
|
||
'list_card_style',
|
||
'grid-cms',
|
||
'large_thumb',
|
||
'multi_thumb',
|
||
'layout_override',
|
||
'homepage_card_style',
|
||
'homepage_multi_thumb',
|
||
'homepage_default_view',
|
||
'post_related_enabled',
|
||
'post_related_count',
|
||
'post_related_style',
|
||
'post_related_source',
|
||
'post_related_title',
|
||
];
|
||
|
||
assert.match(listSchema, /list_type: 'thumb'/);
|
||
for (const value of ['text', 'thumb', 'thumb_if_has', 'card']) {
|
||
assert.match(listSchema, new RegExp(`value: '${value}'`));
|
||
}
|
||
assert.match(listSchema, /list_list_option: \{ style: 'null', img_position: 'left', scale: 70 \}/);
|
||
assert.match(listSchema, /list_card_option: \{ style: 'null', scale: 70 \}/);
|
||
assert.match(pageSchema, /post_related_s: true/);
|
||
assert.match(pageSchema, /post_related_type: 'img'/);
|
||
assert.match(pageSchema, /related_title: '相关推荐'/);
|
||
assert.match(pageSchema, /post_related_n: 6/);
|
||
assert.match(pageSchema, /post_related_orderby: 'views'/);
|
||
for (const key of legacyKeys) {
|
||
assert.equal(source.includes(key), false, `legacy article key remains: ${key}`);
|
||
}
|
||
// bare `topics` is legal again: the related-posts query mirrors the legacy taxonomy OR.
|
||
assert.doesNotMatch(source, /list_card_topics|is_topics/);
|
||
});
|
||
|
||
test('forced mode wins and thumb_if_has ignores spare and lazy placeholders', () => {
|
||
const config = read('inc/helpers/posts-renderer/config.php');
|
||
const data = read('inc/helpers/posts-renderer/post-data.php');
|
||
|
||
const forced = config.indexOf("$is_forced = in_array($forced_type, ['list', 'card'], true)");
|
||
const globalCard = config.indexOf("$list_type === 'card'");
|
||
const contextCard = config.indexOf("$list['list_card_tag']");
|
||
const categoryCard = config.indexOf("$list['list_card_cat']");
|
||
assert.ok(forced >= 0 && forced < globalCard && globalCard < contextCard && contextCard < categoryCard);
|
||
assert.match(config, /\$is_card = \$forced_type === 'card' \|\| \(!\$is_forced/);
|
||
|
||
const hasReal = config.indexOf("$has_real = $primary !== '';");
|
||
const spareCall = config.lastIndexOf('aksrt_renderer_spare_image($thumb)');
|
||
const lazy = config.lastIndexOf('aksrt_get_lazy_thumb');
|
||
assert.ok(hasReal >= 0 && hasReal < spareCall && spareCall < lazy);
|
||
assert.match(data, /'thumb_if_has' && empty\(\$images\['has_real'\]\)/);
|
||
assert.match(data, /count\(\$gallery\) > 2/);
|
||
assert.match(data, /has_post_format\(\['image', 'gallery'\]/);
|
||
});
|
||
|
||
test('category card mode carries its term id through archive, AJAX and REST rendering', () => {
|
||
const resolver = read('inc/helpers/aksrt-archive.php');
|
||
const config = read('inc/helpers/posts-renderer/config.php');
|
||
const admin = read('src/Admin/Admin.php');
|
||
const global = read('src/Admin/GlobalController.php');
|
||
|
||
assert.match(resolver, /'term_id'\s*=>\s*0/);
|
||
assert.match(resolver, /foreach \(\['type', 'term_id'/);
|
||
assert.match(resolver, /\$context === 'category' && !\$has_top_level_term_id && !\$has_override_term_id/);
|
||
assert.match(config, /!\$is_forced && !\$is_card && !empty\(\$args\['is_category'\]\)/);
|
||
assert.match(config, /\$term_id = absint\(\$args\['term_id'\]\)/);
|
||
assert.doesNotMatch(config, /get_queried_object_id/);
|
||
assert.match(admin, /\$clean\['term_id'\]\s*=\s*absint\(\$config\['term_id'\]\)/);
|
||
assert.match(global, /aksrt_resolve_post_list_render_config\(\[\s*'context'\s*=>\s*\$archive_type,\s*'term_id'\s*=>\s*\$archive_type === 'category' \? \$term_id : 0,/);
|
||
assert.doesNotMatch(config, /\b(?:list_scale|card_scale|mobile_meta)\b/);
|
||
assert.doesNotMatch(resolver, /aksrt_archive_get_list_style_settings/);
|
||
});
|
||
|
||
test('image resolution counts only content images and applies the configured WordPress size', () => {
|
||
const config = read('inc/helpers/posts-renderer/config.php');
|
||
const data = read('inc/helpers/posts-renderer/post-data.php');
|
||
const thumbSchema = read('assets/admin/src/schema/sections/post-list/thumb.js');
|
||
const phpSchema = read('src/Admin/Schema/SchemaPostList.php');
|
||
|
||
assert.match(config, /function aksrt_renderer_content_images\(int \$post_id, string \$size\)/);
|
||
assert.match(config, /strip_shortcodes\(\(string\) get_post_field\('post_content'/);
|
||
assert.match(config, /\$slides = aksrt_renderer_curated_images\(\$post_id, \$size\);\s*\$gallery = aksrt_renderer_content_images\(\$post_id, \$size\);/);
|
||
assert.match(config, /get_post_thumbnail_id\(\$post_id\).*aksrt_renderer_sized_image\(\(string\) \$thumbnail_id, \$size\)/s);
|
||
assert.match(config, /aksrt_renderer_category_image\(\$post_id, \$size\)/);
|
||
assert.doesNotMatch(config, /aksrt_get_post_gallery_images|cover_images|featured_video_episode|_aksrt_featured_video|_video_url|_aksrt_thumbnail_url|_thumbnail_url/);
|
||
assert.doesNotMatch(config, /\['aksrt_category_cover_id',/);
|
||
assert.match(data, /'gallery_count'\s*=>\s*count\(\$content_images\)/);
|
||
assert.match(data, /\(int\) \$data\['gallery_count'\] - 4/);
|
||
assert.match(thumbSchema, /spareThumbnailDefault = \[\{ img: `\$\{themeRoot\}\/assets\/img\/thumbnail\.svg` \}\]/);
|
||
assert.match(phpSchema, /'spare_thumbnail'.*'default' => \[\['img' => get_template_directory_uri\(\) \. '\/assets\/img\/thumbnail\.svg'\]\]/);
|
||
});
|
||
|
||
test('list, card, media, badge and Mini DOM follow dedicated legacy structures', () => {
|
||
const markup = read('inc/helpers/posts-renderer/markup.php');
|
||
const data = read('inc/helpers/posts-renderer/post-data.php');
|
||
const card = markup.slice(markup.indexOf('function aksrt_renderer_card'), markup.indexOf('function aksrt_renderer_mini'));
|
||
const mini = markup.slice(markup.indexOf('function aksrt_renderer_mini'), markup.indexOf('function aksrt_render_post'));
|
||
|
||
assert.match(markup, /\['posts-item', 'list', 'ajax-item'\]/);
|
||
assert.match(markup, /\$classes\[\] = .*'flex xx'.*'flex'/s);
|
||
assert.match(markup, /<posts class=/);
|
||
assert.match(markup, /<div class="post-graphic/);
|
||
assert.match(markup, /<div class="item-body flex xx flex1 jsb/);
|
||
assert.doesNotMatch(card, /item-excerpt|renderer_excerpt_markup|thumb-items/);
|
||
assert.match(data, /<div class="item-thumbnail/);
|
||
assert.match(data, /relative aksrt-slider.*new-swiper item-thumbnail/s);
|
||
assert.match(data, /swiper-slide.*gradient-bg.*data-opacity="0\.2"/s);
|
||
assert.match(data, /\$fit = \$is_contain && !\$has_slides \? ' contain gradient-bg' : ''/);
|
||
assert.match(data, /video-thumb-box.*data-post-thumb-video/s);
|
||
assert.match(data, /class="thumb-items/);
|
||
assert.match(data, /empty\(\$cfg\['is_category'\]\).*in_array\('cat'/s);
|
||
assert.match(data, /empty\(\$cfg\['is_tag'\]\).*in_array\('tag'/s);
|
||
assert.doesNotMatch(data, /meta-mobile-show/);
|
||
|
||
assert.match(mini, /<div class="mr10"><div class="item-thumbnail"><a/);
|
||
assert.match(mini, /posts-mini-con flex xx flex1 jsb/);
|
||
assert.match(mini, /\['c-red', 'c-yellow', 'c-purple', 'c-blue', 'c-green'\]/);
|
||
assert.match(mini, /aksrt_renderer_pay_badge\(\$data, true\)/);
|
||
assert.match(data, /meta-pay badg badg-sm mr6/);
|
||
assert.doesNotMatch(mini, /posts-mini-row/);
|
||
|
||
assert.doesNotMatch(markup, /--posts-(?:list|card)-scale|<posts[^>]+style=/);
|
||
assert.match(data, /\['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red'\]/);
|
||
assert.match(data, /查看更多分类文章/);
|
||
assert.match(data, /usort\(\$tags,.*->count.*->count/s);
|
||
assert.match(data, /查看此标签更多文章/);
|
||
|
||
const commentMeta = data.indexOf("aksrt_renderer_svg_icon('comment')");
|
||
const viewMeta = data.indexOf("aksrt_renderer_svg_icon('view')", commentMeta);
|
||
const likeMeta = data.indexOf("aksrt_renderer_svg_icon('like')", viewMeta);
|
||
assert.ok(commentMeta >= 0 && commentMeta < viewMeta && viewMeta < likeMeta);
|
||
assert.match(data, /comments_open.*close_comments/s);
|
||
assert.match(data, /show_like.*post_type.*=== 'post'/s);
|
||
// The <use> sprite markup lives in the unified helper aksrt_renderer_svg_icon delegates to.
|
||
assert.match(read('inc/helpers/aksrt-icons.php'), /<use xlink:href="#icon-/);
|
||
});
|
||
|
||
test('post-card CSS owns the exact desktop and 767/640 responsive matrix', () => {
|
||
const css = read('assets/css/frontend/post-card.css');
|
||
const mobile767 = css.slice(css.indexOf('@media (max-width: 767px)'), css.indexOf('@media (max-width: 640px)'));
|
||
const mobile640 = css.slice(css.indexOf('@media (max-width: 640px)'));
|
||
|
||
assert.match(css, /\.posts-row\s*\{[^}]*margin-right:\s*-8px;[^}]*margin-left:\s*-8px;/s);
|
||
assert.match(css, /\.posts-item\.card\s*\{[^}]*width:\s*calc\(33\.333% - 16px\)/s);
|
||
assert.match(css, /\.fluid-widget-wrap \.posts-item\.card,[\s\S]*?\.site-layout-1 \.posts-item\.card\s*\{[^}]*width:\s*calc\(25% - 16px\)/);
|
||
assert.match(css, /\.site-layout-1 \.posts-item:not\(\.card\)\s*\{[^}]*width:\s*calc\(50% - 16px\)/s);
|
||
assert.match(mobile767, /\.posts-item\.card\s*\{[^}]*width:\s*calc\(50% - 10px\)/s);
|
||
assert.doesNotMatch(mobile767, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
|
||
assert.match(mobile640, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
|
||
assert.match(mobile640, /\.posts-item:not\(\.no-thumb\) \.meta-right item\s*\{[^}]*display:\s*none/s);
|
||
assert.doesNotMatch(css, /\.aksrt-(?!panel|slider)|post-card-cms|grid-cms/);
|
||
});
|
||
|
||
test('article metadata owns its flex and SVG contracts on every list route', () => {
|
||
const css = read('assets/css/frontend/post-card.css');
|
||
const scripts = read('src/Core/ScriptsPart1c.php');
|
||
const loader = read('assets/js/runtime/loader.js');
|
||
|
||
assert.doesNotMatch(scripts, /aksrt-svg-icon|svg-icon-helper/);
|
||
assert.match(loader, /'svg-icon'\s*:\s*'svg-icon'/);
|
||
assert.match(css, /\.posts-item\.flex,[\s\S]*?\.posts-mini \.flex\s*\{[^}]*display:\s*flex;/);
|
||
assert.match(css, /\.posts-item \.flex\.xx,[\s\S]*?\.posts-mini \.flex\.xx\s*\{[^}]*flex-direction:\s*column;/);
|
||
assert.match(css, /\.posts-item \.flex\.ac,[\s\S]*?\.posts-mini \.flex\.ac\s*\{[^}]*align-items:\s*center;/);
|
||
assert.match(css, /\.posts-item \.flex\.jsb,[\s\S]*?\.posts-mini \.flex\.jsb\s*\{[^}]*justify-content:\s*space-between;/);
|
||
assert.match(css, /\.posts-item \.flex1,[\s\S]*?\.posts-mini \.flex1\s*\{[^}]*flex:\s*1;[^}]*overflow:\s*hidden;/);
|
||
assert.match(css, /\.posts-item \.item-meta svg\.icon,[\s\S]*?\.posts-mini \.item-meta svg\.icon\s*\{[^}]*display:\s*inline-block;[^}]*margin-right:\s*2px;/);
|
||
});
|
||
|
||
test('related posts, author page and widgets use their dedicated renderers', () => {
|
||
const related = read('inc/helpers/aksrt-single-render.php');
|
||
const relatedCard = read('inc/helpers/aksrt-single-components.php');
|
||
const resourceRelated = read('templates/parts/resource-related.php');
|
||
const author = read('templates/parts/user-home/panel-posts.php');
|
||
const authorFavorites = read('templates/parts/user-home/panel-favorites.php');
|
||
const userHomeCss = read('assets/css/routes/user-home.css');
|
||
const widgets = read('src/WidgetSystem/PostRenderer.php');
|
||
|
||
assert.match(related, /\['img', 'list', 'text'\]/);
|
||
assert.match(related, /aksrt_renderer_mini\(\$data, \$mini_cfg\)/);
|
||
assert.match(related, /\<div class="swiper-slide mr10"\>' \. aksrt_render_graphic_card_post\(\$post\)/);
|
||
assert.match(related, /<li><a class="icon-circle"/);
|
||
assert.match(related, /\['category', 'topics', 'post_tag'\]/);
|
||
assert.match(related, /'terms'\s+=>\s+array_map\('intval', \(array\) \$term_ids\)/);
|
||
assert.match(related, /class=\"zib-widget\"/);
|
||
assert.doesNotMatch(related, /post_related_source/);
|
||
assert.doesNotMatch(related, /post__not_in/);
|
||
assert.match(related, /\$args\['meta_key'\]\s*=\s*'aksrt_post_views'/);
|
||
assert.match(relatedCard, /aksrt_renderer_resolve_images/);
|
||
assert.match(relatedCard, /'text2'\s+=>\s+esc_html\(aksrt_str_cut\(\$title, 0, \(int\) \$args\['cut'\], '\.\.\.'\)\)/);
|
||
assert.match(resourceRelated, /swiper-container swiper-scroll/);
|
||
assert.match(resourceRelated, /aksrt_render_graphic_card_post/);
|
||
assert.doesNotMatch(resourceRelated, /aksrt_render_post_list/);
|
||
assert.match(author, /aksrt_render_post_list/);
|
||
assert.doesNotMatch(author, /'type'\s*=>\s*'list'/);
|
||
assert.match(author, /'context'\s*=>\s*'author'/);
|
||
assert.match(author, /'no_author'\s*=>\s*true/);
|
||
assert.match(author, /'no_margin'\s*=>\s*true/);
|
||
assert.match(author, /'wrapper_class'\s*=>\s*'aksrt-user-home__post-list'/);
|
||
assert.match(author, /aksrt_render_user_home_pagination\(\(int\) \$author_posts->max_num_pages, \(int\) \$posts_page\)/);
|
||
assert.doesNotMatch(author, /aksrt_render_pagination|aksrt_render_profile_pagination|\$_GET|add_query_arg/);
|
||
// 面板 AJAX 翻页的骨架:与文章列表同一个 .post_ajax_loader 契约,
|
||
// 由 author.js 的 .is-loading 切换(不能再靠 opacity 变暗反馈)。
|
||
// 骨架形状必须跟随同一份渲染配置:写死 'list' 会让卡片模式下的占位
|
||
// 与真实卡片不同轴。
|
||
assert.match(author, /'append_inner_html' => \$user_home_panel_loader/);
|
||
assert.match(author, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
|
||
assert.doesNotMatch(author, /aksrt_render_placeholder\('(list|card)'/);
|
||
assert.match(authorFavorites, /'append_inner_html' => \$user_home_panel_loader/);
|
||
assert.match(authorFavorites, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
|
||
assert.doesNotMatch(authorFavorites, /aksrt_render_placeholder\('(list|card)'/);
|
||
assert.match(userHomeCss, /\.aksrt-user-home \.dashboard-panel \.post_ajax_loader \{/);
|
||
assert.match(userHomeCss, /\.aksrt-user-home \.dashboard-panel\.is-loading \.post_ajax_loader \{/);
|
||
assert.doesNotMatch(userHomeCss, /\.dashboard-panel\.is-loading \{[\s\S]{0,80}opacity: \.6/);
|
||
|
||
// 翻页 / 切 tab 是整页替换语义(author.js 最终就是 panel.innerHTML = html),
|
||
// 骨架必须独占内容区。若骨架只是追加在旧卡片下方,加载期会先看到
|
||
// 「第 1 页卡片 + 脚下一摞骨架」再换页。对齐参考主题 post_ajax() 的
|
||
// beforeSend:replace 时 $item.remove() + find(pag).remove()。
|
||
// 刻意不按 data-view 收窄:card 模式下真实卡片同样是直接子级 .posts-item,
|
||
// 也要一起收起,否则卡片面板旧内容不会让位。
|
||
assert.match(userHomeCss, /\.dashboard-panel\.is-loading \.aksrt-user-home__post-list > \.posts-item \{\s*display: none;/);
|
||
// 必须收「直接子级」而非后代:骨架内层同样带 .posts-item,
|
||
// 写成后代选择器会连骨架自己一起隐藏掉,占位也就永远出不来了。
|
||
assert.doesNotMatch(userHomeCss, /\.dashboard-panel\.is-loading \.aksrt-user-home__post-list \.posts-item \{/);
|
||
// 旧翻页控件高亮的是旧页码,同属被替换的旧页内容。
|
||
assert.match(userHomeCss, /\.dashboard-panel\.is-loading:has\(\.post_ajax_loader\) \.profile-pagination \{\s*display: none;/);
|
||
assert.match(widgets, /aksrt_render_mini_list/);
|
||
assert.match(widgets, /swiper-container swiper-scroll/);
|
||
});
|
||
|
||
test('after-box author card mirrors legacy user-card structure', () => {
|
||
const render = read('inc/helpers/aksrt-single-render.php');
|
||
const identity = read('inc/helpers/aksrt-identity.php');
|
||
const avatar = read('inc/helpers/aksrt-avatar.php');
|
||
const userCard = read('inc/helpers/aksrt-user-card.php');
|
||
const css = read('assets/css/frontend/single-post/related-toc.css');
|
||
const authorActions = read('assets/css/frontend/single-post/author-actions.css');
|
||
const base = read('assets/css/frontend/base.css');
|
||
const follow = read('assets/js/follow.js');
|
||
const authorBar = read('templates/parts/single-author-bar.php');
|
||
const single = read('inc/helpers/aksrt-single.php');
|
||
const settings = read('src/Settings/PostDisplay.php');
|
||
const frontendTags = read('inc/helpers/aksrt-frontend-tags.php');
|
||
|
||
assert.match(render, /avatar-img avatar-lg/);
|
||
assert.match(render, /'vip'\s+=>\s+true/);
|
||
assert.match(render, /'follow'\s+=>\s+true/);
|
||
assert.match(render, /<div class="author-tag mt10 mini-scrollbar">/);
|
||
assert.match(render, /<div class="user-desc mt10 muted-2-color em09">/);
|
||
assert.doesNotMatch(render, /user-btns/);
|
||
assert.doesNotMatch(render, /发布文章.*用户中心/s);
|
||
assert.match(identity, /tag-posts/);
|
||
assert.match(identity, /tag-comment/);
|
||
assert.match(identity, /tag-follow/);
|
||
assert.match(identity, /tag-like/);
|
||
assert.match(identity, /tag-view/);
|
||
assert.match(identity, /get_follower_count/);
|
||
assert.match(avatar, /class="avatar-badge"/);
|
||
assert.match(userCard, /swiper-container more-posts swiper-scroll/);
|
||
assert.match(userCard, /'class'\s+=>\s+'em09', 'cut'\s+=>\s+32/);
|
||
assert.doesNotMatch(userCard, /aksrt-more-post-card/);
|
||
assert.match(css, /^\.zib-widget \{/m);
|
||
assert.doesNotMatch(css, /relates-thumb \.graphic\.style-3 \.graphic-img/);
|
||
assert.doesNotMatch(authorActions, /\.user-card-author|\.user-card \.author|user-card-posts/);
|
||
assert.match(identity, /function aksrt_get_user_follow\(/);
|
||
assert.match(identity, /data-aksrt-follow-label/);
|
||
assert.match(identity, /aksrt_get_user_follow\('focus-color ml10 follow flex0', \$user_id\)/);
|
||
assert.match(authorBar, /aksrt_get_user_follow\('px12-sm ml10 follow but c-red', \$author_id\)/);
|
||
assert.doesNotMatch(authorBar, /Follow::get_instance|is_following|show_actions|show_bio|extra_class|meta_items/);
|
||
assert.doesNotMatch(single, /aksrt_single_render_author_bar/);
|
||
assert.match(base, /\.user-card\.author \.user-name > name\s*\{[^}]*overflow:\s*visible;/s);
|
||
assert.match(base, /\.user-card\.author \.user-name > name > \.follow\s*\{[^}]*flex-shrink:\s*0;/s);
|
||
assert.match(follow, /querySelectorAll\('\[data-aksrt-follow="' \+ userId/);
|
||
assert.match(follow, /matchingButtons\.forEach/);
|
||
assert.match(follow, /\[data-aksrt-follow-label\]/);
|
||
assert.doesNotMatch(follow, /childNodes|textNode|Fallback when markup/);
|
||
assert.doesNotMatch(settings, /'authordesc_s'\s*=>/);
|
||
assert.doesNotMatch(frontendTags, /array_key_exists\('authordesc_s'/);
|
||
assert.doesNotMatch(authorActions, /article-author-bar__|author-box__/);
|
||
});
|
||
|
||
test('AJAX pagination can recover, caps auto loading and rehydrates appended media', () => {
|
||
const php = read('inc/helpers/template-tags/pagination-permissions.php');
|
||
const homepage = read('inc/helpers/homepage/content.php');
|
||
const admin = read('src/Admin/Admin.php');
|
||
const js = read('assets/js/core/pagination.js');
|
||
const ajaxList = read('assets/js/core/ajax-list.js');
|
||
const media = read('assets/js/core/misc.js');
|
||
|
||
assert.match(php, /class="text-center theme-pagination ajax-pag"/);
|
||
assert.match(php, /class="next-page order-ajax-next ias-btn/);
|
||
assert.doesNotMatch(php, /next-page ajax-next/);
|
||
assert.match(php, /lazyload-action="ias"/);
|
||
assert.match(js, /lazybeforeunveil/);
|
||
// 1:1 对齐参考主题 zib_paging():.ajax-pag 内只渲染翻页控件,不预渲染 loader。
|
||
// 预渲染的 loader 会被 runtime post_ajax() 复用并丢弃动态骨架,导致排序切换
|
||
// 永远是写死 2 条 list 骨架。骨架改由 pagination.js 在请求时按视口创建。
|
||
assert.doesNotMatch(php, /post_ajax_loader/);
|
||
assert.match(js, /posts-item card/);
|
||
assert.match(js, /posts-item list flex/);
|
||
// 排序菜单与 tab 触发器归 legacy runtime 管线,本引擎必须避让,否则双请求。
|
||
assert.match(js, /closest\("\.ajax-option, \.post_ajax_trigger"\)/);
|
||
assert.match(php, /data-page-var/);
|
||
assert.match(admin, /'author', 'author_name'/);
|
||
assert.match(admin, /'category__in'.*'tag__in'/s);
|
||
assert.match(admin, /\$query_vars\['paged'\] = \$page/);
|
||
assert.match(js, /failed = false;\s*loadNextPage\(fromAuto\)/);
|
||
assert.match(js, /successfulAutoLoads >= iasMax/);
|
||
assert.match(js, /post_ajax\.ed/);
|
||
assert.match(js, /window\.auto_fun\(\)/);
|
||
assert.match(js, /classList\.add\("lazyload"\)/);
|
||
assert.match(homepage, /aksrt_render_pagination\(\$query,[\s\S]*?'query_context'\s*=>\s*\$contract\['query_context'\]/);
|
||
assert.doesNotMatch(homepage, /href="javascript:;" class="ajax-next aksrt-load-more-btn"/);
|
||
assert.match(ajaxList, /if \(link\.closest\("\[data-aksrt-pagination\], \.aksrt-comment-pagination"\)\) return;/);
|
||
assert.match(media, /thumb-dplayer-playing/);
|
||
assert.doesNotMatch(media, /new Swiper|data-post-thumb-slider/);
|
||
assert.match(read('assets/js/runtime/main.min.js'), /new-swiper/);
|
||
assert.match(media, /data-post-thumb-video/);
|
||
assert.match(media, /window\.Grade\(container/);
|
||
assert.match(media, /linear-gradient\(135deg/);
|
||
assert.match(media, /aksrtGradientState/);
|
||
assert.match(read('assets/js/runtime/main.js'), /tbquire\(\['grade'\]/);
|
||
});
|
||
|
||
test('front-end carousel CSS is conditional on carousel block content', () => {
|
||
const hooks = read('inc/core-hooks.php');
|
||
assert.match(hooks, /has_block\('aksrt\/carousel', \$post\)/);
|
||
assert.match(hooks, /if \(!\$has_carousel\) return;[\s\S]*?'aksrt-gutenberg-blocks-front'/);
|
||
});
|
||
|
||
test('the pinned visual baseline records all source checksums', () => {
|
||
const baseline = JSON.parse(read('tests/visual/zibll-9.0-article-baseline.json'));
|
||
assert.equal(baseline.theme, 'Zibll');
|
||
assert.equal(baseline.version, '9.0');
|
||
const files = Object.values(baseline.files);
|
||
assert.ok(files.length >= 6);
|
||
for (const file of files) {
|
||
assert.match(file.sha256, /^[a-f0-9]{64}$/);
|
||
}
|
||
});
|
||
|
||
test('every scene reserves a sidebar column only when its widget stack has content', () => {
|
||
const layout = read('inc/helpers/template-tags/site-layout.php');
|
||
const single = read('inc/helpers/aksrt-single.php');
|
||
const aside = read('inc/helpers/aksrt-single-aside.php');
|
||
const homepage = read('inc/helpers/homepage/content.php');
|
||
const sidebarTemplate = read('sidebar.php');
|
||
const archiveHelpers = read('inc/helpers/aksrt-archive.php');
|
||
|
||
// Single visibility decision: an empty widget stack collapses every scene
|
||
// (home/category/tag/search/page/archive) to no-sidebar, not just singles.
|
||
assert.match(layout, /if \(!aksrt_has_current_sidebar_widgets\(\)\) \{\s*return false;/);
|
||
assert.doesNotMatch(layout, /is_single\(\) && !aksrt_has_current_sidebar_widgets/);
|
||
|
||
// The resolver must answer home_sidebar before the is_page() branch so a
|
||
// static front page is checked against the zones the homepage renders.
|
||
const resolver = layout.match(/function aksrt_resolve_sidebar_id\(\): string \{[\s\S]*?\n\}/);
|
||
assert.ok(resolver, 'aksrt_resolve_sidebar_id should exist');
|
||
assert.ok(
|
||
resolver[0].indexOf('is_home() || is_front_page()') < resolver[0].indexOf('is_page()'),
|
||
'home/front_page branch must precede the is_page() branch'
|
||
);
|
||
|
||
assert.match(single, /\$show_aside = get_post_type\(\$post_id\) === 'post'[\s\S]*?&& aksrt_is_show_sidebar\(\);/);
|
||
assert.match(aside, /get_post_type\(\$post_id\) !== 'post' \|\| empty\(\$ctx\['show_aside'\]\)/);
|
||
|
||
// Homepage aside renders under the same single decision; no dual gate.
|
||
assert.match(homepage, /function aksrt_homepage_render_sidebar\(\): void \{\s*if \(!aksrt_is_show_sidebar\(\)\) \{\s*return;/);
|
||
assert.doesNotMatch(homepage, /aksrt_homepage_has_sidebar/);
|
||
|
||
// Generic sidebar template early-returns instead of rendering an empty column.
|
||
assert.match(sidebarTemplate, /if \(!function_exists\('aksrt_is_show_sidebar'\) \|\| !aksrt_is_show_sidebar\(\)\) \{\s*return;/);
|
||
assert.doesNotMatch(sidebarTemplate, /legacy parity/);
|
||
|
||
// Archive helper takes only the page-level toggle; no dead params.
|
||
assert.match(archiveHelpers, /function aksrt_archive_has_sidebar\(bool \$enabled = true\): bool/);
|
||
assert.doesNotMatch(archiveHelpers, /preferred|wp_is_mobile/);
|
||
});
|
||
|
||
test('article visual matrix pins every breakpoint, state and strict threshold', () => {
|
||
const matrix = JSON.parse(read('tests/visual/article-visual-matrix-v1.json'));
|
||
assert.deepEqual(matrix.viewports.map((item) => item.width), [1440, 1024, 768, 767, 641, 640, 390]);
|
||
assert.deepEqual(matrix.colorSchemes, ['light', 'dark']);
|
||
assert.deepEqual(matrix.layouts, ['sidebar', 'no-sidebar']);
|
||
assert.deepEqual(matrix.listModes, ['thumb', 'text', 'thumb_if_has', 'card']);
|
||
assert.deepEqual(matrix.styles, ['default', 'style2', 'style3']);
|
||
assert.ok(matrix.routes.some((route) => route.id === 'author-a'));
|
||
assert.ok(matrix.routes.some((route) => route.states.includes('infinite')));
|
||
assert.ok(matrix.postScenarios.includes('no-image'));
|
||
assert.ok(matrix.postScenarios.includes('multi'));
|
||
assert.ok(matrix.postScenarios.includes('video'));
|
||
assert.ok(matrix.postScenarios.includes('slides'));
|
||
assert.equal(matrix.acceptance.dimensionDeltaPxMax, 1);
|
||
assert.equal(matrix.acceptance.pixelDifferencePercentMax, 1.5);
|
||
});
|
||
|
||
test('profile routes load versioned leaf styles and settings dialogs use the shared runtime', () => {
|
||
const scripts = read('src/Core/ScriptsPart1b.php');
|
||
const settingsAccount = read('inc/helpers/user-center-renderers/settings-account.php');
|
||
const settingsBindings = read('inc/helpers/user-center-renderers/settings-bindings.php');
|
||
const userCenterJs = read('assets/js/routes/user-center.js');
|
||
const settingsCss = read('assets/css/routes/user-center/settings.css');
|
||
const profileListsCss = read('assets/css/routes/profile/lists.css');
|
||
const userHomeCss = read('assets/css/routes/user-home.css');
|
||
const profileLayoutCss = read('assets/css/routes/profile/layout.css');
|
||
const frontendLayoutCss = read('assets/css/frontend/layout.css');
|
||
const publicBundleCss = read('assets/css/build/public.css');
|
||
const userCenterShellCss = read('assets/css/routes/user-center/shell.css');
|
||
const userCenterCreditsCss = read('assets/css/routes/user-center/credits.css');
|
||
const profileCredits = read('inc/helpers/user-center-renderers/profile-credits.php');
|
||
const profileOrders = read('inc/helpers/user-center-renderers/profile-orders.php');
|
||
const ordersCss = read('assets/css/routes/orders.css');
|
||
const orderCenterJs = read('assets/js/order-center.js');
|
||
|
||
assert.ok(scripts.includes("$profile_parts = ['layout', 'lists'];"));
|
||
for (const part of ['user-center/shell', 'user-center/credits', 'user-center/vip', 'user-center/level', 'user-center/settings', 'user-center/revenue', 'user-center/revenue-earnings', 'user-center/responsive']) {
|
||
assert.ok(scripts.includes(`'${part}'`), `missing direct route leaf ${part}`);
|
||
}
|
||
// 死 leaf 已删除:独立的 revenue.css 只服务已下线的旧收益页标记。
|
||
assert.doesNotMatch(scripts, /'revenue',\n/);
|
||
assert.doesNotMatch(scripts, /assets\/css\/routes\/revenue\.css/);
|
||
assert.match(scripts, /assets\/css\/routes\/profile\/' \. \$part \. '\.css/);
|
||
assert.match(scripts, /assets\/css\/routes\/' \. \$part \. '\.css/);
|
||
assert.match(scripts, /filemtime\(\$part_path\)/);
|
||
assert.doesNotMatch(scripts, /assets\/css\/routes\/profile\.css/);
|
||
assert.doesNotMatch(scripts, /assets\/css\/routes\/user-center\.css/);
|
||
|
||
assert.match(settingsAccount, /data-aksrt-settings-modal="<\?php echo esc_attr\(\$modal\); \?>"/);
|
||
assert.match(settingsAccount, /\$render_row\([^;]*'email'/);
|
||
assert.match(settingsAccount, /\$render_row\([^;]*'phone'/);
|
||
assert.match(settingsAccount, /\$render_row\([^;]*'password'/);
|
||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/);
|
||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/);
|
||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/);
|
||
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-aksrt-settings-submit-label/);
|
||
assert.match(settingsAccount, /id="email-new-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||
assert.doesNotMatch(settingsAccount, /aksrt-settings-modal__footer|确认修改|确认更改|确认绑定/);
|
||
assert.ok(settingsAccount.indexOf('绑定邮箱') < settingsAccount.indexOf('绑定手机'));
|
||
assert.ok(settingsAccount.indexOf('绑定手机') < settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'"));
|
||
assert.ok(settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'") < settingsAccount.indexOf('账户密码'));
|
||
assert.ok(settingsBindings.indexOf('绑定社交账号') < settingsBindings.indexOf('aksrt-account-list--social'));
|
||
assert.match(settingsAccount, /!empty\(\$user_settings\['bind_phone'\]\)/);
|
||
assert.match(settingsBindings, /aksrt-account-list--social/);
|
||
assert.match(settingsBindings, /binding-unbind-btn/);
|
||
assert.match(userCenterJs, /runtime\.open\(\{/);
|
||
assert.match(userCenterJs, /className: 'aksrt-user-settings-modal modal-mini full-sm'/);
|
||
assert.match(userCenterJs, /mobileBottom: true/);
|
||
assert.match(userCenterJs, /onContent: function \(_, session\)/);
|
||
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
|
||
assert.match(userCenterJs, /resetCaptcha\(scene, form\)/);
|
||
assert.match(userCenterJs, /function setSettingsSubmitLabel\(button, text\)[\s\S]*?label\.textContent = text/);
|
||
assert.match(userCenterJs, /aksrt_change_password/);
|
||
assert.match(userCenterJs, /aksrt_change_email/);
|
||
assert.match(settingsAccount, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"/);
|
||
assert.match(settingsAccount, /\$email_verify_required/);
|
||
assert.match(settingsAccount, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"/);
|
||
assert.match(settingsAccount, /data-account-recovery-open[\s\S]*?data-account-password-pane="recovery"/);
|
||
assert.match(settingsAccount, /aksrt_render_captcha\('change_password'\)/);
|
||
assert.match(userCenterJs, /\/auth\/code/);
|
||
assert.match(userCenterJs, /\/auth\/reset-password/);
|
||
assert.match(userCenterJs, /\/user\/phone-verify/);
|
||
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|aksrt_send_email_code/);
|
||
assert.match(settingsCss, /\.aksrt-modal-runtime\.aksrt-user-settings-modal:not\(\.aksrt-modal-runtime--bottom\)[\s\S]*?width: 400px/);
|
||
assert.match(settingsCss, /\.aksrt-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
|
||
assert.match(settingsAccount, /class="colorful-make"/);
|
||
assert.doesNotMatch(userCenterJs, /function closeModal\(|\.aksrt-modal\.is-open|security-trigger|data-close-modal/);
|
||
for (const deletedPath of [
|
||
'inc/helpers/user-center-renderers/profile-account-modals.php',
|
||
'inc/helpers/user-center-renderers/settings-security.php',
|
||
'assets/css/routes/profile/modals.css',
|
||
]) assert.equal(fs.existsSync(path.join(root, deletedPath)), false, `stale settings modal file: ${deletedPath}`);
|
||
assert.doesNotMatch(profileListsCss, /aksrt-modal__icon|aksrt-modal__close|security-action/);
|
||
assert.match(userHomeCss, /\.aksrt-profile-details-modal\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
|
||
assert.match(userHomeCss, /\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
|
||
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\]\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\);/);
|
||
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] > \.posts-item\s*\{[^}]*width:\s*auto;[^}]*margin:\s*0 !important;/);
|
||
assert.match(userHomeCss, /@media \(max-width: 767px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\[data-view="list"\] \.posts-item\s*\{[^}]*padding:\s*10px 0;/);
|
||
const panelFavorites = read('templates/parts/user-home/panel-favorites.php');
|
||
assert.doesNotMatch(panelFavorites, /'type'\s*=>\s*'list'/);
|
||
assert.match(panelFavorites, /'context'\s*=>\s*'author'/);
|
||
assert.match(panelFavorites, /aksrt_render_user_home_pagination\(\(int\) \$favorites_query->max_num_pages, \(int\) \$favorites_page\)/);
|
||
assert.match(panelFavorites, /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$favorites_query\)/);
|
||
assert.match(panelFavorites, /aksrt_render_placeholder\(\(string\) \(\$user_home_render_config\['mode'\] \?\? 'list'\), max\(4, \(int\) \$per_page\)\)/);
|
||
assert.match(read('templates/parts/user-home/panel-posts.php'), /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list', 'append_inner_html' => \$user_home_panel_loader\]\), \$author_posts\)/);
|
||
assert.doesNotMatch(panelFavorites, /profile-list-item|\$_GET|add_query_arg/);
|
||
// Only the two-column user center spans the viewport; the single-column public
|
||
// profile keeps following the admin content max width.
|
||
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.aksrt-profile\s*\{[\s\S]*?max-width:\s*100%/);
|
||
assert.match(frontendLayoutCss, /\.site-main\.content-shell\.aksrt-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)[\s\S]*?padding-top:\s*0/);
|
||
// The front end enqueues build/public.min.css, not the frontend/ sources, so a
|
||
// source-only edit never reaches the page. The bundle must carry the contract.
|
||
assert.match(publicBundleCss, /\.site-main\.content-shell\.aksrt-profile\s*\{[\s\S]*?max-width:\s*100%/);
|
||
assert.match(publicBundleCss, /\.site-main\.content-shell\.aksrt-user-home\s*\{[\s\S]*?max-width:\s*var\(--container, 1440px\)/);
|
||
// .aksrt-profile on <main> is the user center only; other routes must not carry it.
|
||
assert.equal(read('templates/pages/page-user-home.php').includes('aksrt-profile"'), false);
|
||
assert.doesNotMatch(profileLayoutCss, /\.aksrt-profile\.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
|
||
assert.doesNotMatch(userCenterShellCss, /\.aksrt-user-center \.content-shell\s*,[\s\S]*?max-width:\s*var\(--container/);
|
||
assert.match(userCenterShellCss, /\.aksrt-profile\.content-shell\s*\{[\s\S]*?max-width:\s*none/);
|
||
assert.match(userCenterShellCss, /\.aksrt-profile\.user-center\s*\{[\s\S]*?display:\s*block;[\s\S]*?gap:\s*0/);
|
||
assert.match(userCenterShellCss, /\.aksrt-profile\.content-shell > \.content-wrap,[\s\S]*?\.aksrt-profile\.content-shell \.uc-header\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none/);
|
||
assert.match(profileLayoutCss, /\.aksrt-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
|
||
assert.match(profileLayoutCss, /#aksrt-bg-file-input\[hidden\],[\s\S]*?#aksrt-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/);
|
||
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?data-aksrt-sms-trigger/);
|
||
assert.match(settingsAccount, /class="modal-colorful-header colorful-bg jb-blue"/);
|
||
assert.match(settingsAccount, /class="modal-colorful-header colorful-bg jb-yellow"/);
|
||
assert.doesNotMatch(settingsAccount, /class="aksrt-modal"|aksrt-modal__overlay|aksrt-modal__box/);
|
||
assert.match(userCenterCreditsCss, /#panel-credits\.dashboard-panel\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
|
||
assert.match(userCenterCreditsCss, /\.uc-asset-tabs\s*\{[^}]*background:\s*var\(--surface-1,\s*#fff\);[^}]*border:[^;}]+;[^}]*box-shadow:[^;}]+;/);
|
||
assert.doesNotMatch(profileCredits, /credits-signin\.php|data-aksrt-signin-calendar/);
|
||
// 签到详情弹窗为参考主题 1:1 结构(src/Credits/Signin.php),日历已随
|
||
// 旧版 lite 弹窗一并移除;用户中心里的孤儿渲染器也已整包移除。
|
||
assert.equal(fs.existsSync(path.join(root, 'inc/helpers/user-center-renderers/credits-signin.php')), false);
|
||
assert.doesNotMatch(userCenterCreditsCss + read('assets/css/frontend/enhancements/components/custom-amount.css'), /uc-signin-calendar/);
|
||
const signinModalHtml = read('src/Credits/Signin.php');
|
||
assert.match(signinModalHtml, /modal-colorful-header colorful-bg c-blue/);
|
||
assert.match(signinModalHtml, /checkin-mini-box/);
|
||
assert.doesNotMatch(signinModalHtml, /data-aksrt-signin-calendar/);
|
||
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden/);
|
||
assert.doesNotMatch(profileOrders, /\$_GET|add_query_arg|name="order_(?:status|type|number|page)"/);
|
||
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden[\s\S]*?aksrt-orders-page__status-filters[\s\S]*?aksrt-orders-page__type-filters/);
|
||
assert.match(profileOrders, /data-aksrt-order-filter="status"[\s\S]*?data-aksrt-order-filter="type"/);
|
||
assert.match(profileOrders, /data-aksrt-order-page=/);
|
||
assert.doesNotMatch(profileOrders, /aksrt-orders-page__(?:filter-chip|page-chip|clear)[^>]*href=/);
|
||
assert.doesNotMatch(profileOrders, /aksrt-orders-page__tabs|aksrt-orders-page__tab(?:\s|<|"|')/);
|
||
assert.match(ordersCss, /\.aksrt-orders-page__filter-panel\[hidden\]\s*\{\s*display:\s*none\s*!important;/);
|
||
assert.match(ordersCss, /\.aksrt-orders-page__status-filters, \.aksrt-orders-page__type-filters/);
|
||
assert.doesNotMatch(ordersCss, /\.aksrt-orders-page__tabs|\.aksrt-orders-page__tab(?:\s|\.|:|\{)/);
|
||
assert.match(orderCenterJs, /if \(panel\) panel\.hidden = expanded;/);
|
||
assert.doesNotMatch(profileOrders, /can_open|entitlement_label|is-entitlement/);
|
||
assert.doesNotMatch(orderCenterJs, /can_open|entitlement_label|is-entitlement/);
|
||
assert.doesNotMatch(ordersCss, /is-entitlement/);
|
||
assert.match(orderCenterJs, /function loadView\(state, patch\)/);
|
||
assert.match(orderCenterJs, /data-aksrt-order-filter/);
|
||
assert.match(orderCenterJs, /data-aksrt-order-page/);
|
||
assert.doesNotMatch(orderCenterJs, /new URL\(window\.location\.href\)/);
|
||
// 订单详情弹窗滚动发生在 runtime dialog 本体:滚动条必须完全隐藏(含细滚动条),滚动行为不变。
|
||
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-modal-runtime__dialog\s*\{[^}]*scrollbar-width:\s*none/);
|
||
assert.match(ordersCss, /\.aksrt-order-detail-modal \.aksrt-modal-runtime__dialog::-webkit-scrollbar\s*\{\s*display:\s*none;\s*\}/);
|
||
assert.doesNotMatch(ordersCss, /aksrt-order-detail-modal[^{}]*mini-scrollbar/);
|
||
assert.doesNotMatch(userCenterJs, /\['order_status', 'order_type', 'order_number', 'order_page'\]/);
|
||
assert.match(userCenterJs, /initialTab === 'orders'[\s\S]*?window\.history\.replaceState/);
|
||
});
|
||
|
||
test('public user home stays param-free and swaps panels via REST fragments', () => {
|
||
const ctx = read('inc/helpers/aksrt-user-home.php');
|
||
const shell = read('templates/pages/page-user-home.php');
|
||
const follow = read('templates/parts/user-home/panel-follow.php');
|
||
const comments = read('templates/parts/user-home/panel-comments.php');
|
||
const favorites = read('templates/parts/user-home/panel-favorites.php');
|
||
const rest = read('src/User/Home.php');
|
||
const authorJs = read('assets/js/author.js');
|
||
|
||
// Context builder owns no request state; pages default to 1.
|
||
assert.doesNotMatch(ctx, /\$_GET/);
|
||
assert.match(ctx, /function aksrt_render_user_home_pagination\(int \$total_pages, int \$current\): void/);
|
||
assert.match(ctx, /data-uh-page/);
|
||
assert.match(ctx, /function aksrt_capture_user_home_panel\(string \$tab, array \$ctx\): string/);
|
||
|
||
// Shell mounts panel wrappers from the tab registry with a user id hook.
|
||
assert.match(shell, /data-user-id="<\?php echo \(int\) \$user_id; \?>">/);
|
||
assert.match(shell, /in_array\(\$panel_tab, \['favorites', 'follow'\], true\) && \$panel_tab !== \$active_tab/);
|
||
assert.match(shell, /data-panel-loaded="<\?php echo \$defer_panel \? 'false' : 'true'; \?>"/);
|
||
assert.match(shell, /get_template_part\('templates\/parts\/user-home\/panel', \$panel_tab, \$panel_context\)/);
|
||
|
||
// Follow panel: button-driven sub-tabs, pagination without URL building.
|
||
assert.match(follow, /data-uh-sub="followers"/);
|
||
assert.match(follow, /data-uh-sub="following"/);
|
||
assert.match(follow, /aksrt_render_user_home_pagination\(\$total_pages, \$follow_page\)/);
|
||
assert.match(ctx, /function aksrt_get_user_home_follow_placeholder\(\): string[\s\S]*?author-minicard radius8 flex ac uh-follow__placeholder[\s\S]*?placeholder k1 mb6[\s\S]*?placeholder s1/);
|
||
assert.match(ctx, /str_repeat\(\$item, 6\)/);
|
||
assert.match(follow, /aksrt_get_user_home_follow_placeholder\(\)/);
|
||
assert.match(follow, /data-uh-retry[\s\S]*?重试/);
|
||
assert.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/);
|
||
|
||
assert.match(comments, /data-uh-comments-next=/);
|
||
assert.match(favorites, /aksrt_render_user_home_pagination\(/);
|
||
assert.match(favorites, /if \(!empty\(\$placeholder_only\)\)[\s\S]*?aksrt_render_placeholder\(\$placeholder_mode, 4\)/);
|
||
assert.match(favorites, /data-uh-retry[\s\S]*?重试/);
|
||
assert.doesNotMatch(comments + favorites, /add_query_arg|\$_GET/);
|
||
|
||
// REST fragment endpoint serves public panel bodies.
|
||
assert.ok(rest.includes("'/user-home/panel/(?P<user_id>\\d+)/(?P<tab>[a-z0-9_-]+)'"));
|
||
assert.match(rest, /'permission_callback'\s*=>\s*'__return_true'/);
|
||
assert.match(rest, /aksrt_capture_user_home_panel\(\$tab, \$context\)/);
|
||
|
||
// Runtime never reads or writes URL params; fragments replace panel bodies.
|
||
assert.doesNotMatch(authorJs, /searchParams|pushState|replaceState/);
|
||
assert.match(authorJs, /\/user-home\/panel\/' \+ userId \+ '/);
|
||
assert.match(authorJs, /data-uh-sub/);
|
||
assert.match(authorJs, /post_ajax\.ed/);
|
||
assert.match(authorJs, /window\.auto_fun\(\)/);
|
||
assert.match(authorJs, /panel\.dataset\.panelLoaded === 'false'[\s\S]*?loadPanelBody\(panel, name, 1, 'followers'\)/);
|
||
assert.match(authorJs, /target\.closest\('\[data-uh-retry\]'\)[\s\S]*?loadPanelBody\(retryPanel, retryName, 1, 'followers'\)/);
|
||
assert.match(authorJs, /panel\.dataset\.panelLoaded = 'true'/);
|
||
assert.match(authorJs, /panel\.classList\.add\('is-load-error'\)/);
|
||
const userHomeCss = read('assets/css/routes/user-home.css');
|
||
assert.match(userHomeCss, /dashboard-panel\.is-loading \.uh-follow__list--placeholder\s*\{\s*display: block;/);
|
||
assert.match(userHomeCss, /dashboard-panel\.is-loading \.uh-follow__subtabs,[\s\S]*?dashboard-panel\.is-loading \.uh-follow__list:not\(\.uh-follow__list--placeholder\),[\s\S]*?dashboard-panel\.is-loading \.uh-pagination,[\s\S]*?dashboard-panel\.is-loading \.profile-empty\s*\{\s*display: none;/);
|
||
assert.match(userHomeCss, /dashboard-panel\.is-load-error \.uh-follow__list--placeholder\s*\{\s*display: block;/);
|
||
assert.match(userHomeCss, /dashboard-panel\.is-load-error \.post_ajax_loader\s*\{\s*display: block;/);
|
||
assert.match(userHomeCss, /dashboard-panel\.is-load-error \.uh-panel-load-error\s*\{\s*display: block;/);
|
||
|
||
// 翻页拿子分组只读**当前面板内**的 is-active 子页签,不读 panel.dataset:
|
||
// uhSub 是粉丝面板私有状态,写满所有面板会互相串味 —— 先点过粉丝子页签,
|
||
// 残留值会长在文章面板上,之后粉丝面板翻页读到的是别人的分组。
|
||
assert.match(authorJs, /panel\.querySelector\('\[data-uh-sub\]\.is-active'\)/);
|
||
assert.doesNotMatch(authorJs, /loadPanelBody\(panel, name, page, panel\.dataset\.uhSub/);
|
||
// uhPage / uhSub 只写粉丝面板;文章 / 收藏 / 评论面板没有子页签。
|
||
assert.match(authorJs, /if \(name === 'follow'\) \{[\s\S]{0,120}panel\.dataset\.uhSub = followSub;/);
|
||
assert.doesNotMatch(authorJs, /var followSub = sub \|\| panel\.dataset\.uhSub \|\| 'followers';/);
|
||
});
|
||
|
||
test('single articles enqueue only content and settings driven assets', () => {
|
||
const enqueue = read('src/Core/ScriptsPart1b.php');
|
||
const runtime = read('src/Core/ScriptsPart1c.php');
|
||
const builder = read('tools/build-public-assets.mjs');
|
||
const singleStart = enqueue.indexOf('// Normal articles: one small core bundle');
|
||
const staticPageStart = enqueue.indexOf('// 静态页面 + 用户中心', singleStart);
|
||
const singleBlock = enqueue.slice(singleStart, staticPageStart);
|
||
|
||
assert.ok(singleStart >= 0 && staticPageStart > singleStart);
|
||
assert.match(builder, /'single\.css': \[[\s\S]*?single-post\/shell\.css'[\s\S]*?single-post\/content\.css'[\s\S]*?single-post\/author-actions\.css'/);
|
||
assert.match(builder, /'comments\.css': \[[\s\S]*?comments\/shell\.css'[\s\S]*?comments\/mobile-item\.css'/);
|
||
assert.match(singleBlock, /build\/single\.min\.css/);
|
||
assert.match(singleBlock, /wp_register_style\([\s\S]*?'aksrt-single-post'[\s\S]*?array_merge\(\['aksrt-single-core'\], \$single_feature_handles\)/);
|
||
assert.doesNotMatch(singleBlock, /\$enqueue_module\('single-post'\)|\$enqueue_module\('comments'\)/);
|
||
|
||
for (const requirement of [
|
||
'has_code',
|
||
'needs_navigation',
|
||
'needs_cover',
|
||
'needs_related_toc',
|
||
'has_hidden_content',
|
||
]) {
|
||
assert.match(singleBlock, new RegExp(`'${requirement}'\\s*=>`));
|
||
}
|
||
|
||
for (const requirement of [
|
||
'needs_post_card',
|
||
'needs_comments',
|
||
'needs_sidebar',
|
||
'needs_breadcrumb',
|
||
]) {
|
||
assert.match(singleBlock, new RegExp(`singular_assets\\['${requirement}'\\]`));
|
||
}
|
||
|
||
assert.match(runtime, /wp:\(\?:code\|preformatted\|aksrt\\\/enlighter\)/);
|
||
assert.match(runtime, /preg_match_all\('\/<h\[1-4\]/);
|
||
assert.match(runtime, /data-aksrt-video/);
|
||
assert.match(runtime, /post_password_required\(\$post_id\)/);
|
||
assert.match(runtime, /comment_settings\['close_comments'\]/);
|
||
assert.match(runtime, /comment_settings\['require_login'\]/);
|
||
assert.match(runtime, /needs_reward'[\s\S]*?rewards_enabled/);
|
||
assert.match(runtime, /needs_yiyan'[\s\S]*?yiyan_header/);
|
||
assert.match(runtime, /needs_share'[\s\S]*?share_enabled/);
|
||
|
||
for (const requirement of [
|
||
'has_code',
|
||
'has_toc',
|
||
'has_video',
|
||
'needs_reward',
|
||
'needs_yiyan',
|
||
'needs_comments',
|
||
'needs_comment_tools',
|
||
'needs_page_template',
|
||
'needs_share',
|
||
]) {
|
||
assert.match(runtime, new RegExp(`singular_assets\\['${requirement}'\\]`));
|
||
}
|
||
});
|
||
|
||
test('single post more menu has one accessible runtime and an in-flow author anchor', () => {
|
||
const article = read('assets/js/core/article.js');
|
||
const components = read('inc/helpers/aksrt-single-components.php');
|
||
const single = read('inc/helpers/aksrt-single.php');
|
||
const singleRender = read('inc/helpers/aksrt-single-render.php');
|
||
const authorBar = read('templates/parts/single-author-bar.php');
|
||
const styles = read('assets/css/frontend/single-post/author-actions.css');
|
||
|
||
assert.match(article, /function initPostMetaMenus\(root\)/);
|
||
assert.match(article, /classList\.toggle\("is-open", open\)/);
|
||
assert.match(article, /setAttribute\("aria-expanded", open \? "true" : "false"\)/);
|
||
assert.match(article, /setAttribute\("aria-hidden", open \? "false" : "true"\)/);
|
||
assert.match(article, /event\.key === "Escape"/);
|
||
assert.match(article, /jQuery\(document\)\.off\(.*auto_fun/);
|
||
assert.doesNotMatch(components + article, /data-toggle=["']dropdown|\.dropdown\(/);
|
||
|
||
assert.match(components, /\$menu_id = 'post-drop-meta-menu-' \. \$post_id/);
|
||
assert.match(components, /aria-controls="' \. esc_attr\(\$menu_id\)/);
|
||
assert.match(components, /aria-haspopup="true"/);
|
||
assert.match(single, /aksrt_get_single_meta_box\(\$post_id\)/);
|
||
assert.doesNotMatch(components, /\$direction/);
|
||
assert.match(components, /\$count_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$count\) : number_format_i18n\(\$count\)/);
|
||
assert.match(components, /\$likes_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$likes\) : number_format_i18n\(\$likes\)/);
|
||
assert.match(components, /esc_html\(\(string\) \$count_label\)/);
|
||
assert.match(components, /esc_html\(\(string\) \$likes_label\)/);
|
||
assert.match(authorBar, /class="single-meta-row"/);
|
||
assert.doesNotMatch(authorBar, /single-metabox abs-right/);
|
||
assert.match(singleRender, /'meta_html'\s*=>\s*\(string\) \(\$ctx\['meta_box_html'\] \?\? ''\)/);
|
||
assert.doesNotMatch(singleRender, /cover-meta|str_replace\('single-metabox'/);
|
||
assert.match(styles, /\.single-meta-row > \.single-metabox\s*\{[^}]*position:\s*relative;[^}]*transform:\s*none;/s);
|
||
assert.match(styles, /\.post-drop-meta__menu\s*\{[^}]*position:\s*absolute;[^}]*right:\s*0;/s);
|
||
assert.match(styles, /\.article-header:has\(\.post-drop-meta\.is-open\)\s*\{[^}]*z-index:\s*30;/s);
|
||
});
|
||
|
||
test('single post like, favorite and comment counts abbreviate everywhere', () => {
|
||
const actionsRender = read('inc/helpers/aksrt-single-render.php');
|
||
const tabbar = read('inc/helpers/template-tags/pagination-permissions.php');
|
||
const article = read('assets/js/core/article.js');
|
||
|
||
assert.match(actionsRender, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
|
||
assert.match(actionsRender, /\$favorite_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
|
||
assert.match(actionsRender, /data-stat-count="likes" data-base-count="<\?php echo esc_attr\(\(string\) \$like_count\); \?>"><\?php echo esc_html\(\(string\) \$like_label\);/);
|
||
assert.match(actionsRender, /data-stat-count="favorites" data-base-count="<\?php echo esc_attr\(\(string\) \$favorite_count\); \?>"><\?php echo esc_html\(\(string\) \$favorite_label\);/);
|
||
|
||
assert.match(tabbar, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
|
||
assert.match(tabbar, /\$favorite_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$favorite_count\) : number_format_i18n\(\$favorite_count\)/);
|
||
assert.match(tabbar, /\$comment_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$comment_count\) : number_format_i18n\(\$comment_count\)/);
|
||
assert.match(tabbar, /echo esc_html\(\(string\) \$like_label\)/);
|
||
assert.match(tabbar, /echo esc_html\(\(string\) \$favorite_label\)/);
|
||
assert.match(tabbar, /echo esc_html\(\(string\) \$comment_label\)/);
|
||
|
||
assert.match(article, /function formatStatCount\(count\)/);
|
||
assert.match(article, /safeCount >= 10000[\s\S]*?"W\+"/);
|
||
assert.match(article, /safeCount >= 1000[\s\S]*?"K"/);
|
||
assert.match(article, /var label = formatStatCount\(count\);/);
|
||
});
|
||
|
||
test('abbreviated counts use uppercase unit suffixes everywhere', () => {
|
||
const frontendTags = read('inc/helpers/aksrt-frontend-tags.php');
|
||
const article = read('assets/js/core/article.js');
|
||
const commentAjax = read('assets/js/comment-ajax.js');
|
||
|
||
const formatNumber = frontendTags.match(/function aksrt_format_number\([\s\S]*?\nendif;/)[0];
|
||
assert.match(formatNumber, /round\(\$num \/ 10000, 1\) \. 'W\+'/);
|
||
assert.match(formatNumber, /round\(\$num \/ 1000, 1\) \. 'K'/);
|
||
assert.doesNotMatch(formatNumber, /\. 'k'/);
|
||
|
||
assert.doesNotMatch(article, /"k"/);
|
||
assert.match(article, /"K"/);
|
||
assert.match(commentAjax, /function formatCount\(count\)/);
|
||
assert.match(commentAjax, /safeCount >= 10000[\s\S]*?'W\+'/);
|
||
assert.match(commentAjax, /safeCount >= 1000[\s\S]*?'K'/);
|
||
assert.doesNotMatch(commentAjax, /'k'/);
|
||
});
|
||
|
||
test('article page sales counts and comment likes abbreviate', () => {
|
||
const pay = read('inc/helpers/aksrt-pay.php');
|
||
const downloadBox = read('templates/parts/resource-download-box.php');
|
||
const commentItem = read('inc/helpers/comments/item.php');
|
||
|
||
// 付费盒(文章页 + 侧栏小工具)「已售」徽章。
|
||
assert.match(pay, /\$sales_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$sales\) : number_format_i18n\(\$sales\)/);
|
||
assert.doesNotMatch(pay, /sprintf\(__\('已售 %s', 'aksrt'\), \$sales\)/);
|
||
|
||
// 资源下载盒「已售」徽章。
|
||
assert.match(downloadBox, /\$sales_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$display_count\) : number_format_i18n\(\$display_count\)/);
|
||
assert.match(downloadBox, /printf\(esc_html__\('已售 %s', 'aksrt'\), \$sales_label\)/);
|
||
assert.doesNotMatch(downloadBox, /number_format_i18n\(\$display_count\)\);/);
|
||
|
||
// 评论区点赞数。
|
||
assert.match(commentItem, /\$like_label = function_exists\('aksrt_format_number'\) \? aksrt_format_number\(\$like_count\) : number_format_i18n\(\$like_count\)/);
|
||
assert.match(commentItem, /esc_html\(\(string\) \$like_label\)/);
|
||
});
|
||
|
||
test('single post has no word-count or estimated-reading-time chain', () => {
|
||
const chain = join([
|
||
'inc/helpers/aksrt-single.php',
|
||
'inc/helpers/aksrt-single-render.php',
|
||
'inc/helpers/aksrt-frontend-tags.php',
|
||
'src/Settings/PostDisplay.php',
|
||
'src/Settings/FacadeBulk.php',
|
||
'src/Admin/Schema/SchemaPostList.php',
|
||
'assets/admin/src/schema/sections/post-list/feature.js',
|
||
'assets/css/frontend/single-post/shell.css',
|
||
]);
|
||
|
||
assert.doesNotMatch(chain, /aksrt_(?:post_word_count|reading_minutes)/);
|
||
assert.doesNotMatch(chain, /(?:feature_|post_)?word_count(?:_enabled)?/);
|
||
assert.doesNotMatch(chain, /(?:feature_|post_)?reading_time(?:_enabled)?/);
|
||
assert.doesNotMatch(chain, /meta_items_secondary|article-reading-meta/);
|
||
assert.doesNotMatch(chain, /字数统计|预计阅读时间|约 %s 字|约 %d 分钟/);
|
||
});
|
||
|
||
test('resource meta CSS uses one canonical direct stylesheet', () => {
|
||
const enqueue = read('src/Core/ScriptsPart1b.php');
|
||
const canonical = path.join(root, 'assets/css/frontend/resource-meta/part.css');
|
||
|
||
assert.ok(fs.existsSync(canonical));
|
||
assert.match(enqueue, /assets\/css\/frontend\/resource-meta\/part\.css/);
|
||
assert.doesNotMatch(enqueue, /\$enqueue_module\('resource-meta'\)/);
|
||
assert.equal(fs.existsSync(path.join(root, 'assets/css/frontend/resource-meta.css')), false);
|
||
|
||
for (let part = 1; part <= 5; part += 1) {
|
||
assert.equal(
|
||
fs.existsSync(path.join(root, `assets/css/frontend/resource-meta/part-${part}.css`)),
|
||
false
|
||
);
|
||
}
|
||
});
|
||
|
||
test('content max width is one contract from admin option to runtime var', () => {
|
||
const globalSchema = read('src/Admin/Schema/GlobalSchema.php');
|
||
const layoutSchema = read('assets/admin/src/schema/sections/global/layout.js');
|
||
const themeSearch = read('src/Settings/ThemeSearch.php');
|
||
const builder = read('src/Frontend/InlineStyleBuilder.php');
|
||
|
||
// Every width the settings UI offers must pass REST validation, or the save is
|
||
// rejected and the rendered width silently keeps the previous value.
|
||
const pxOptions = [...layoutSchema.matchAll(/value: (\d+) \}/g)].map((match) => Number(match[1]));
|
||
assert.ok(pxOptions.includes(1200));
|
||
assert.ok(pxOptions.includes(2200));
|
||
const schemaMax = Number(globalSchema.match(/'container_max_width'[\s\S]*?'max' => (\d+)/)[1]);
|
||
assert.equal(schemaMax, Math.max(...pxOptions));
|
||
assert.match(globalSchema.match(/'container_max_width'[^\n]*/)[0], /'min' => 0/);
|
||
|
||
assert.match(themeSearch, /max\(1200, min\(2200, \$width\)\)/);
|
||
assert.match(builder, /'--container'\s*=> \$content_width/);
|
||
// User pages are full-bleed now, so the aliased width tokens are dead.
|
||
assert.doesNotMatch(builder, /--content-max|content-no-sidebar-max/);
|
||
assert.doesNotMatch(read('assets/css/core/tokens.css'), /--content-max/);
|
||
});
|