'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 exists = (file) => fs.existsSync(path.join(root, file)); test('comment area styles are fully owned (hard cut from vendor main.css)', () => { const layout = read('assets/css/frontend/comments/layout.css'); const mobile = read('assets/css/frontend/comments/mobile-item.css'); // 1:1 移植的关键布局选择器必须存在于自有文件。 const layoutSelectors = [ '#comments .comment-box .comments-closed', '#comments #postcomments .comt-ctrl .but', '#comment {', '.comment-signarea {', '#postcomments .comment-footer {', '#postcomments .avatar-img {', '#postcomments .children {', '#postcomments .comt-meta > span:not(:empty)', '#postcomments .comment-approve-link.approve {', '#postcomments .comment-approve-link.unapprove {', '#postcomments .comt-meta > span .comment-trash-link {', '.comment-content {', '#postcomments .comment-content img {', '#comments .virtual-input {', '.commentlist .no-more {', ]; for (const selector of layoutSelectors) { assert.ok(layout.includes(selector), `layout.css must contain ${selector}`); } // 桌面隐藏必须带 #comments 前缀(id 特异性),任何后加载的 // .flex{display:flex} 单类工具规则都无法在宽屏把假输入条顶出来。 assert.equal(layout.includes('\n.virtual-input {'), false, 'desktop hide must be id-scoped, not bare .virtual-input'); // 移动端弹层 / 虚拟输入 / 640px 覆盖。 const mobileSelectors = [ '@media (max-width: 767px)', '@media (max-width: 640px)', '#comments .virtual-input {', '.drop-fixed-sm .dropdown-menu,', '.mobile-fixed {', '#respond.mobile-fixed {', '.drop-fixed-sm .dropdown-menu::before {', '#comments .comt-box {', '#postcomments .commentlist > .comment > .list-inline .comment-footer {', ]; for (const selector of mobileSelectors) { assert.ok(mobile.includes(selector), `mobile-item.css must contain ${selector}`); } // 自有文件不得包含表情/快捷回复样式。 for (const [file, source] of [['layout.css', layout], ['mobile-item.css', mobile]]) { for (const token of ['smilie', 'dropdown-quick-often', 'quick-reply-item', 'quick-reply-action']) { assert.equal(source.includes(token), false, `${file} must not contain ${token}`); } } // 构建产物 comments.css 聚合了自有规则。 const built = read('assets/css/build/comments.css'); for (const selector of ['#postcomments .comt-meta > span:not(:empty)', '.mobile-fixed {', '#comments .virtual-input {', '#comments .comt-title {']) { assert.ok(built.includes(selector), `build/comments.css must contain ${selector}`); } }); test('vendor main.css no longer carries comment-area rules (no dual path)', () => { for (const file of ['assets/css/main.css', 'assets/css/main.min.css']) { const source = read(file); for (const token of [ '#postcomments', '.commentlist', '.comt-', '.comment-signarea', '.comment-filter', '.mobile-fixed', '.drop-fixed-sm', '#respond', '#comments', ]) { assert.equal(source.includes(token), false, `${file} must not contain ${token}`); } // 小部件与共享规则仍留在全站表。 assert.ok(source.includes('.comment-widget-item'), `${file} must keep .comment-widget-item`); assert.ok(source.includes('.comment-mini-lists'), `${file} must keep .comment-mini-lists`); assert.ok(source.includes('.content-after-box'), `${file} must keep .content-after-box`); assert.ok(source.includes('.theme-pagination.pagenav'), `${file} must keep .theme-pagination.pagenav`); } }); test('quick reply is removed across the owned comment stack', () => { const files = [ 'assets/admin/src/schema/sections/post-list/comment.js', 'assets/css/main.css', 'assets/css/build/comments.css', 'assets/js/comment-tools.js', 'inc/helpers/aksrt-comments.php', 'inc/helpers/comments/form-parts.php', 'src/Admin/Schema/SchemaPostList.php', 'src/Core/ScriptsPart1c.php', 'src/Settings/PostDisplay.php', ]; const tokens = [ 'comment_quick_s', 'comment_quick_often', 'quick_phrases', 'parse_comment_quick_phrases', 'quick-reply-item', 'dropdown-quick-often', 'input-quick', 'commentQuickPhrases', '快捷回复', ]; for (const file of files) { const source = read(file); for (const token of tokens) { assert.equal(source.includes(token), false, `${file} must not contain ${token}`); } } }); test('dead vendor comment archives stay deleted', () => { for (const file of [ 'assets/js/runtime/comment.js', 'assets/js/runtime/comment.min.js', 'assets/js/runtime/input-expand.js', 'assets/js/runtime/input-expand.min.js', ]) { assert.equal(exists(file), false, `${file} must not exist`); } const loader = read('assets/js/runtime/loader.js'); assert.doesNotMatch(loader, /['"](?:comment|input-expand)['"]\s*:/, 'legacy comment modules must have no loader mapping'); assert.doesNotMatch(read('assets/js/runtime/main.js'), /stubVendorCommentModule|unbindVendorAjaxLoop/); }); test('every comment-rendering singular loads the owned build bundle', () => { // 旧的静态聚合文件已删除:它缺 layout.css 且 @import 无版本号, // 曾导致页面/资源页手机端 .mobile-fixed 抽屉规则缺失、表单直接内联显示。 assert.equal(exists('assets/css/frontend/comments.css'), false, 'stale frontend/comments.css aggregator must not exist'); const part1b = read('src/Core/ScriptsPart1b.php'); // 统一入口:任何渲染评论区的 singular 都加载 build 聚合。 assert.ok( part1b.includes('is_singular() && !empty($singular_assets[\'needs_comments\'])'), 'ScriptsPart1b must enqueue the comments bundle for every comment-rendering singular' ); assert.ok(part1b.includes('assets/css/build/'), 'comments enqueue must point at the build bundle'); // 旧的两处按需入口必须移除(否则资源页/页面回到缺规则的聚合文件)。 assert.equal(part1b.includes("$enqueue_module('comments')"), false, 'stale aggregator enqueue must be gone'); }); test('comment markup keeps the 1:1 legacy DOM contract', () => { const item = read('inc/helpers/comments/item.php'); // 点赞按钮(legacy: zib_get_comment_like — svg + text + count)。 assert.ok(item.includes(""), 'like button must render before '); assert.ok(item.includes('aksrt_get_comment_icon(\'like\''), 'like button must use the sprite icon'); assert.ok(item.includes('aksrt_get_comment_icon(\'topping\')'), 'topping badge must use the sprite icon'); assert.ok(item.includes('aksrt_get_comment_icon(\'approve\''), 'approve action must use the sprite icon'); assert.ok(item.includes('aksrt_get_comment_icon(\'menu_2\')'), 'action dropdown must use the sprite icon'); assert.ok(item.includes('data-toggle="tooltip"'), 'time/reply tooltip parity'); assert.equal(item.includes('fa fa-thumbs-up'), false, 'legacy fa like icon must be gone'); const comments = read('inc/helpers/aksrt-comments.php'); assert.ok(comments.includes('post_password_required() || !comments_open()'), 'closed comments must render nothing'); assert.ok(comments.includes('user_url_enabled'), 'guest url field toggle must be wired'); assert.ok(comments.includes("name=\"url\""), 'guest url input must render'); assert.ok(comments.includes("aksrt_get_comment_icon('signup', 'icon mr10')"), 'signarea signup must use the sprite icon'); assert.ok(comments.includes("aksrt_get_vip_icon($current_user->ID, 'em12 mr3')"), 'comt-title must render the direct VIP image with reference-theme spacing'); assert.ok(comments.includes("aksrt_get_avatar_html($current_user->ID, 96, ['class' => 'avatar'])"), 'logged-in avatar must use the canonical avatar helper at the reference source size'); assert.ok(comments.includes("aksrt_get_avatar_html(0, 96, ['class' => 'avatar'])"), 'guest avatar must use the canonical default-avatar helper'); assert.equal(comments.includes('aksrt_render_vip_badge'), false, 'legacy VIP wrapper helper must not remain in the comment form'); assert.equal(comments.includes('aksrt_render_user_identity_chips'), false, 'identity chips must not crowd the narrow comt-title'); const vip = read('inc/helpers/aksrt-vip.php'); assert.ok(vip.includes("trim('img-icon ' . $class)"), 'VIP helper must emit the reference img-icon contract'); assert.equal(vip.includes('function aksrt_render_vip_badge'), false, 'legacy wrapped VIP helper must be removed'); assert.equal(vip.includes(' !empty($settings['comment_link'])"), 'toolbar link toggle'); assert.ok(settings.includes("'user_url' => !empty($settings['comment_user_url_s'])"), 'user url toggle'); });