'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, /
]+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 sprite markup lives in the unified helper aksrt_renderer_svg_icon delegates to. assert.match(read('inc/helpers/aksrt-icons.php'), /' \. aksrt_render_graphic_card_post\(\$post\)/); assert.match(related, /
  • \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, /
    /); assert.match(render, /
    /); assert.doesNotMatch(render, /user-btns/); assert.doesNotMatch(render, /发布文章.*用户中心/s); assert.match(identity, /tag-posts/); assert.match(identity, /tag-comment/); assert.match(identity, /tag-follow/); assert.match(identity, /tag-like/); assert.match(identity, /tag-view/); assert.match(identity, /get_follower_count/); assert.match(avatar, /class="avatar-badge"/); assert.match(userCard, /swiper-container more-posts swiper-scroll/); assert.match(userCard, /'class'\s+=>\s+'em09', 'cut'\s+=>\s+32/); assert.doesNotMatch(userCard, /aksrt-more-post-card/); assert.match(css, /^\.zib-widget \{/m); assert.doesNotMatch(css, /relates-thumb \.graphic\.style-3 \.graphic-img/); assert.doesNotMatch(authorActions, /\.user-card-author|\.user-card \.author|user-card-posts/); assert.match(identity, /function aksrt_get_user_follow\(/); assert.match(identity, /data-aksrt-follow-label/); assert.match(identity, /aksrt_get_user_follow\('focus-color ml10 follow flex0', \$user_id\)/); assert.match(authorBar, /aksrt_get_user_follow\('px12-sm ml10 follow but c-red', \$author_id\)/); assert.doesNotMatch(authorBar, /Follow::get_instance|is_following|show_actions|show_bio|extra_class|meta_items/); assert.doesNotMatch(single, /aksrt_single_render_author_bar/); assert.match(base, /\.user-card\.author \.user-name > name\s*\{[^}]*overflow:\s*visible;/s); assert.match(base, /\.user-card\.author \.user-name > name > \.follow\s*\{[^}]*flex-shrink:\s*0;/s); assert.match(follow, /querySelectorAll\('\[data-aksrt-follow="' \+ userId/); assert.match(follow, /matchingButtons\.forEach/); assert.match(follow, /\[data-aksrt-follow-label\]/); assert.doesNotMatch(follow, /childNodes|textNode|Fallback when markup/); assert.doesNotMatch(settings, /'authordesc_s'\s*=>/); assert.doesNotMatch(frontendTags, /array_key_exists\('authordesc_s'/); assert.doesNotMatch(authorActions, /article-author-bar__|author-box__/); }); test('AJAX pagination can recover, caps auto loading and rehydrates appended media', () => { const php = read('inc/helpers/template-tags/pagination-permissions.php'); const homepage = read('inc/helpers/homepage/content.php'); const admin = read('src/Admin/Admin.php'); const js = read('assets/js/core/pagination.js'); const ajaxList = read('assets/js/core/ajax-list.js'); const media = read('assets/js/core/misc.js'); assert.match(php, /class="text-center theme-pagination ajax-pag"/); assert.match(php, /class="next-page order-ajax-next ias-btn/); assert.doesNotMatch(php, /next-page ajax-next/); assert.match(php, /lazyload-action="ias"/); assert.match(js, /lazybeforeunveil/); // 1:1 对齐参考主题 zib_paging():.ajax-pag 内只渲染翻页控件,不预渲染 loader。 // 预渲染的 loader 会被 runtime post_ajax() 复用并丢弃动态骨架,导致排序切换 // 永远是写死 2 条 list 骨架。骨架改由 pagination.js 在请求时按视口创建。 assert.doesNotMatch(php, /post_ajax_loader/); assert.match(js, /posts-item card/); assert.match(js, /posts-item list flex/); // 排序菜单与 tab 触发器归 legacy runtime 管线,本引擎必须避让,否则双请求。 assert.match(js, /closest\("\.ajax-option, \.post_ajax_trigger"\)/); assert.match(php, /data-page-var/); assert.match(admin, /'author', 'author_name'/); assert.match(admin, /'category__in'.*'tag__in'/s); assert.match(admin, /\$query_vars\['paged'\] = \$page/); assert.match(js, /failed = false;\s*loadNextPage\(fromAuto\)/); assert.match(js, /successfulAutoLoads >= iasMax/); assert.match(js, /post_ajax\.ed/); assert.match(js, /window\.auto_fun\(\)/); assert.match(js, /classList\.add\("lazyload"\)/); assert.match(homepage, /aksrt_render_pagination\(\$query,[\s\S]*?'query_context'\s*=>\s*\$contract\['query_context'\]/); assert.doesNotMatch(homepage, /href="javascript:;" class="ajax-next aksrt-load-more-btn"/); assert.match(ajaxList, /if \(link\.closest\("\[data-aksrt-pagination\], \.aksrt-comment-pagination"\)\) return;/); assert.match(media, /thumb-dplayer-playing/); assert.doesNotMatch(media, /new Swiper|data-post-thumb-slider/); assert.match(read('assets/js/runtime/main.min.js'), /new-swiper/); assert.match(media, /data-post-thumb-video/); assert.match(media, /window\.Grade\(container/); assert.match(media, /linear-gradient\(135deg/); assert.match(media, /aksrtGradientState/); assert.match(read('assets/js/runtime/main.js'), /tbquire\(\['grade'\]/); }); test('front-end carousel CSS is conditional on carousel block content', () => { const hooks = read('inc/core-hooks.php'); assert.match(hooks, /has_block\('aksrt\/carousel', \$post\)/); assert.match(hooks, /if \(!\$has_carousel\) return;[\s\S]*?'aksrt-gutenberg-blocks-front'/); }); test('the pinned visual baseline records all source checksums', () => { const baseline = JSON.parse(read('tests/visual/zibll-9.0-article-baseline.json')); assert.equal(baseline.theme, 'Zibll'); assert.equal(baseline.version, '9.0'); const files = Object.values(baseline.files); assert.ok(files.length >= 6); for (const file of files) { assert.match(file.sha256, /^[a-f0-9]{64}$/); } }); test('every scene reserves a sidebar column only when its widget stack has content', () => { const layout = read('inc/helpers/template-tags/site-layout.php'); const single = read('inc/helpers/aksrt-single.php'); const aside = read('inc/helpers/aksrt-single-aside.php'); const homepage = read('inc/helpers/homepage/content.php'); const sidebarTemplate = read('sidebar.php'); const archiveHelpers = read('inc/helpers/aksrt-archive.php'); // Single visibility decision: an empty widget stack collapses every scene // (home/category/tag/search/page/archive) to no-sidebar, not just singles. assert.match(layout, /if \(!aksrt_has_current_sidebar_widgets\(\)\) \{\s*return false;/); assert.doesNotMatch(layout, /is_single\(\) && !aksrt_has_current_sidebar_widgets/); // The resolver must answer home_sidebar before the is_page() branch so a // static front page is checked against the zones the homepage renders. const resolver = layout.match(/function aksrt_resolve_sidebar_id\(\): string \{[\s\S]*?\n\}/); assert.ok(resolver, 'aksrt_resolve_sidebar_id should exist'); assert.ok( resolver[0].indexOf('is_home() || is_front_page()') < resolver[0].indexOf('is_page()'), 'home/front_page branch must precede the is_page() branch' ); assert.match(single, /\$show_aside = get_post_type\(\$post_id\) === 'post'[\s\S]*?&& aksrt_is_show_sidebar\(\);/); assert.match(aside, /get_post_type\(\$post_id\) !== 'post' \|\| empty\(\$ctx\['show_aside'\]\)/); // Homepage aside renders under the same single decision; no dual gate. assert.match(homepage, /function aksrt_homepage_render_sidebar\(\): void \{\s*if \(!aksrt_is_show_sidebar\(\)\) \{\s*return;/); assert.doesNotMatch(homepage, /aksrt_homepage_has_sidebar/); // Generic sidebar template early-returns instead of rendering an empty column. assert.match(sidebarTemplate, /if \(!function_exists\('aksrt_is_show_sidebar'\) \|\| !aksrt_is_show_sidebar\(\)\) \{\s*return;/); assert.doesNotMatch(sidebarTemplate, /legacy parity/); // Archive helper takes only the page-level toggle; no dead params. assert.match(archiveHelpers, /function aksrt_archive_has_sidebar\(bool \$enabled = true\): bool/); assert.doesNotMatch(archiveHelpers, /preferred|wp_is_mobile/); }); test('article visual matrix pins every breakpoint, state and strict threshold', () => { const matrix = JSON.parse(read('tests/visual/article-visual-matrix-v1.json')); assert.deepEqual(matrix.viewports.map((item) => item.width), [1440, 1024, 768, 767, 641, 640, 390]); assert.deepEqual(matrix.colorSchemes, ['light', 'dark']); assert.deepEqual(matrix.layouts, ['sidebar', 'no-sidebar']); assert.deepEqual(matrix.listModes, ['thumb', 'text', 'thumb_if_has', 'card']); assert.deepEqual(matrix.styles, ['default', 'style2', 'style3']); assert.ok(matrix.routes.some((route) => route.id === 'author-a')); assert.ok(matrix.routes.some((route) => route.states.includes('infinite'))); assert.ok(matrix.postScenarios.includes('no-image')); assert.ok(matrix.postScenarios.includes('multi')); assert.ok(matrix.postScenarios.includes('video')); assert.ok(matrix.postScenarios.includes('slides')); assert.equal(matrix.acceptance.dimensionDeltaPxMax, 1); assert.equal(matrix.acceptance.pixelDifferencePercentMax, 1.5); }); test('profile routes load versioned leaf styles and settings dialogs use the shared runtime', () => { const scripts = read('src/Core/ScriptsPart1b.php'); const settingsAccount = read('inc/helpers/user-center-renderers/settings-account.php'); const settingsBindings = read('inc/helpers/user-center-renderers/settings-bindings.php'); const userCenterJs = read('assets/js/routes/user-center.js'); const settingsCss = read('assets/css/routes/user-center/settings.css'); const profileListsCss = read('assets/css/routes/profile/lists.css'); const userHomeCss = read('assets/css/routes/user-home.css'); const profileLayoutCss = read('assets/css/routes/profile/layout.css'); const frontendLayoutCss = read('assets/css/frontend/layout.css'); const publicBundleCss = read('assets/css/build/public.css'); const userCenterShellCss = read('assets/css/routes/user-center/shell.css'); const userCenterCreditsCss = read('assets/css/routes/user-center/credits.css'); const profileCredits = read('inc/helpers/user-center-renderers/profile-credits.php'); const profileOrders = read('inc/helpers/user-center-renderers/profile-orders.php'); const ordersCss = read('assets/css/routes/orders.css'); const orderCenterJs = read('assets/js/order-center.js'); assert.ok(scripts.includes("$profile_parts = ['layout', 'lists'];")); for (const part of ['user-center/shell', 'user-center/credits', 'user-center/vip', 'user-center/level', 'user-center/settings', 'user-center/revenue', 'user-center/revenue-earnings', 'user-center/responsive']) { assert.ok(scripts.includes(`'${part}'`), `missing direct route leaf ${part}`); } // 死 leaf 已删除:独立的 revenue.css 只服务已下线的旧收益页标记。 assert.doesNotMatch(scripts, /'revenue',\n/); assert.doesNotMatch(scripts, /assets\/css\/routes\/revenue\.css/); assert.match(scripts, /assets\/css\/routes\/profile\/' \. \$part \. '\.css/); assert.match(scripts, /assets\/css\/routes\/' \. \$part \. '\.css/); assert.match(scripts, /filemtime\(\$part_path\)/); assert.doesNotMatch(scripts, /assets\/css\/routes\/profile\.css/); assert.doesNotMatch(scripts, /assets\/css\/routes\/user-center\.css/); assert.match(settingsAccount, /data-aksrt-settings-modal="<\?php echo esc_attr\(\$modal\); \?>"/); assert.match(settingsAccount, /\$render_row\([^;]*'email'/); assert.match(settingsAccount, /\$render_row\([^;]*'phone'/); assert.match(settingsAccount, /\$render_row\([^;]*'password'/); assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/); assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/); assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/); const settingsSubmitButtons = settingsAccount.match(/