翻页 / 切 tab 是整页替换语义(author.js 最终就是 panel.innerHTML = html), 但加载态只做了骨架的显示,旧卡片与旧页码仍留在屏上,于是先看到 「第 1 页卡片 + 脚下一摞骨架」,等接口返回才换页。 加载期收起 .aksrt-user-home__post-list 的直接子级 .posts-item 与旧翻页控件, 让骨架独占内容区;对齐参考主题 post_ajax() 的 beforeSend(replace 时 $item.remove() + find(pag).remove())。显隐仍走纯类切换,不写 inline display。 两点刻意的收窄: - 只收直接子级 .posts-item 而非后代:骨架内层同样带 .posts-item, 后代选择器会把骨架自己也藏掉,占位就永远出不来。 - 旧页码用 :has(.post_ajax_loader) 限定:关注 / 评论面板没有骨架, 无条件隐藏会让它们加载时只剩一片空白。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
744 lines
45 KiB
JavaScript
744 lines
45 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+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\(\(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(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/);
|
||
});
|