Files
wordpress-theme-aksrt/tests/widget-system/post-visual-contract.test.cjs
T
kate522andClaude Fable 5 38bbd917a9 fix(skeleton): align the homepage tab and sort loaders with the reference 1:1
首页「更多栏目」tab 与「排序方式」切换的骨架占位与参考主题 zibll 有实质差异,
根因不是几何宽度,而是三处结构/契约错位:

1. 骨架块自身结构与参考主题不同。card 分支用了 `<posts>` 元素 + `.post-graphic`
   包裹层(列表变体,带 margin-right:20px),而参考主题 card 骨架是
   `<div class="posts-item card">` 且 `.item-thumbnail` 是直接子元素。
   → card 骨架右侧多出一条真实卡片没有的 20px 沟槽。

2. 骨架块被错误地打上 `ajax-item`。参考主题的占位块没有该类。它会让
   `find('.ajax-item')` / ajax-list 的 `inject()` 把骨架当成真实条目。

3. 排序切换的骨架被预渲染节点顶替。aksrt_render_pagination() 在 .ajax-pag 内
   渲染了一个写死 2 条、写死 list 形状的 .post_ajax_loader;runtime post_ajax()
   会先在容器内 find('.post_ajax_loader'),找到就复用并丢弃调用方按视口动态
   拼出的骨架 → 切排序永远是 2 条 list 骨架,card 布局下也是、与视口宽度无关。
   参考主题 zib_paging() 根本没有 loader 兄弟节点。

改动:
- inc/helpers/posts-renderer/markup.php:aksrt_render_placeholder() 的
  card/list 分支改为与 zib_get_post_placeholder() 逐字等价的结构;去掉
  ajax-item 类、card 分支的 .post-graphic 包裹层,元素改用 <div>。
- inc/helpers/template-tags/pagination-permissions.php:删除预渲染 loader,
  与参考主题 zib_paging() 一致 —— 只渲染翻页控件。
- assets/js/core/pagination.js:找不到骨架节点时构造与 runtime/main.js 同款
  的文章骨架(卡片 8/6/4 条、列表 4/3 条,按视口与参考主题同阈值),
  不再退化为「转圈 + 文案」这个第二套加载语言;仅评论等非文章面保留该兜底。
- assets/js/core/pagination.js:为排序菜单(.ajax-option)与 tab 触发器
  (.post_ajax_trigger)增加避让 —— 它们归 legacy runtime 管线,
  否则一次点击发两个请求(与 ajax-list.js 的既有避让对称)。
- assets/js/runtime/main.js:post_ajax 的显隐由 fadeIn()/hide() 改为
  .is-loading 类切换。前者写 inline display,优先级高于类规则,一旦残留
  骨架永远打不开,正是骨架契约明令禁止的失败模式。

归档/分类筛选面原本另有一套独立加载语言(spinner 遮罩 + opacity 淡出),
本次一并统一到同一条 .post_ajax_loader + .is-loading 契约:
- inc/helpers/aksrt-archive.php:删除 show_loading 标记位与遮罩 DOM,
  改为在分页之后渲染文章骨架。
- assets/js/core/archive.js:showLoading/hideLoading 只切 .is-loading;
  fetchPosts() 清空内容时保留骨架与分页节点(与参考主题 success 只 hide
  不删的生命周期一致),原先会连带删掉骨架。
- assets/css/frontend/layout.css:删除 .archive__ajax-wrap.is-loading
  的 opacity 淡出与 spinner 遮罩规则。
- category.php / tag.php:删除已失效的 show_loading 实参。

验证:php -l 全部通过;php tools/audit-admin-contracts.php errors=0;
契约测试 post-visual(22) / asset-ledger(16) / message-center(8) /
lazyload(7) / frontend-lifecycle(13) / zibll-modern-widgets(10) /
page-display(18) / a11y 全绿;已重跑 build:public 刷新产物。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-04 03:04:02 +08:00

726 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 变暗反馈)。
assert.match(author, /'append_inner_html' => \$user_home_panel_loader/);
assert.match(author, /aksrt_render_placeholder\('list', max\(4, \(int\) \$per_page\)\)/);
assert.match(authorFavorites, /'append_inner_html' => \$user_home_panel_loader/);
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/);
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+false/);
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 keep dialogs hidden initially', () => {
const scripts = read('src/Core/ScriptsPart1b.php');
const accountModals = read('inc/helpers/user-center-renderers/profile-account-modals.php');
const settingsSecurity = read('inc/helpers/user-center-renderers/settings-security.php');
const userCenterJs = read('assets/js/routes/user-center.js');
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 profileModalsCss = read('assets/css/routes/profile/modals.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', 'modals'];"));
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(accountModals, /id="pwd-modal" class="aksrt-modal" hidden/);
assert.match(accountModals, /id="email-modal" class="aksrt-modal" hidden/);
assert.match(userCenterJs, /function closeModal\(m\)[\s\S]*?m\.hidden = true;/);
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\('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(settingsSecurity, /data-modal="email-modal"[\s\S]*?<\/button>\s*<\/div>\s*<div class="security-action-item">[\s\S]*?data-modal="phone-modal"/);
assert.match(accountModals, /id="phone-modal" class="aksrt-modal" hidden role="dialog"[\s\S]*?aksrt-modal__overlay" data-close-modal[\s\S]*?aksrt-modal__box/);
assert.match(accountModals, /id="phone-modal"[\s\S]*?class="modal-field modal-field--code"[\s\S]*?class="code-input-group"[\s\S]*?class="aksrt-modal__footer"/);
assert.doesNotMatch(accountModals, /id="phone-modal"[\s\S]*?aksrt-modal__(?:dialog|body|actions)/);
assert.match(profileModalsCss, /\.modal-field input\[type="tel"\]/);
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\)/);
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, /get_template_part\('templates\/parts\/user-home\/panel', \$panel_tab, \$ctx\)/);
// 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.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/);
assert.match(comments, /aksrt_render_user_home_pagination\(/);
assert.match(favorites, /aksrt_render_user_home_pagination\(/);
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\(\)/);
});
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(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 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/);
});