Files
wordpress-theme-aksrt/tests/widget-system/report-ban-contract.test.cjs
T
kate522 f9bc0f69c4 fix(ban): align user center ban notice with adjacent meta info
用户中心页头治理提示与相邻元信息(用户名/邮箱/城市)排版不一致:
- 高度不一致:原实现为 badg 胶囊 + 箭头作为 <a> 的内联兄弟节点,箭头
  游离在胶囊之外且按基线对齐,整行高度被 pill 撑高;
- 字号不一致:badg-sm 的 0.8em 使提示文字小于相邻项的 13px;
- 箭头位置:参考主题 zib_get_user_ban_info_link 的箭头位于按钮内部。

现复用 uc-header__meta-item(inline-flex 居中、12px 图标、13px 字号),
与相邻元信息完全同构:状态图标 + 状态文案(c-red/c-yellow)+ 内部箭头;
链接默认色经作用域规则回归行内墨色。契约测试锁定新结构,361 项全通过。
2026-09-22 18:13:58 +08:00

453 lines
30 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.
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.resolve(__dirname, '..', '..')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
const userController = read('src/Admin/UserController.php')
const reportOps = read('src/Admin/UserControllerReportOperations.php')
const restrictionOps = read('src/Admin/UserControllerRestrictionOperations.php')
const restriction = read('src/User/Restriction.php')
const reportCenter = read('src/Messages/ReportCenter.php')
const usersMenus = read('src/Admin/Native/UsersMenus.php')
const mailNotifications = read('src/Settings/MailNotifications.php')
const home = read('src/User/Home.php')
const commentItem = read('inc/helpers/comments/item.php')
const functions = read('functions.php')
const uninstall = read('uninstall.php')
const userBanHelper = read('inc/helpers/user-ban.php')
const userBanJs = read('assets/js/user-ban.js')
const authorHero = read('templates/parts/user/author-hero.php')
const ucHeader = read('inc/helpers/user-center-renderers/profile-header.php')
const commentsHelper = read('inc/helpers/aksrt-comments.php')
const appealPage = read('templates/pages/page-ban-appeal.php')
const identityHelper = read('inc/helpers/aksrt-identity.php')
const avatarHelper = read('inc/helpers/aksrt-avatar.php')
const singleComponents = read('inc/helpers/aksrt-single-components.php')
const banAppeal = read('src/User/BanAppeal.php')
const mainCss = read('assets/css/main.css')
const mainMinCss = read('assets/css/main.min.css')
test('report REST surface is canonical: submit + users/reports only', () => {
assert.match(userController, /'\/report\/submit'/)
assert.match(userController, /'\/users\/reports'/)
assert.match(userController, /'\/users\/reports\/\(\?P<id>\\d\+\)'/)
assert.match(userController, /'\/users\/reports\/\(\?P<id>\\d\+\)\/process'/)
for (const dead of ["'/report/list'", "'/report/update-status'", "'/report/counts'"]) {
assert.ok(!userController.includes(dead), `alias route should be removed: ${dead}`)
}
assert.ok(!reportOps.includes('public function update_report_status'), 'dead update_report_status method')
assert.ok(!reportOps.includes('public function get_report_counts'), 'dead get_report_counts method')
})
test('report center has no legacy ajax handlers or option migration', () => {
assert.ok(!reportCenter.includes('ajax_update_status'))
assert.ok(!reportCenter.includes('ajax_list'))
assert.ok(!reportCenter.includes('ajax_counts'))
assert.ok(!reportCenter.includes('migrate_from_option'))
assert.ok(!reportCenter.includes('maybe_migrate'))
assert.ok(!reportCenter.includes("get_option('aksrt_reports'"))
assert.ok(!functions.includes('ReportCenter::get_instance()->init()'), 'ReportCenter init() is gone')
assert.match(reportCenter, /public function has_pending_report/)
assert.match(reportOps, /has_pending_report\(\$reporter_id, \$target_type, \$target_id\)/)
})
test('report processing delegates actions to the shared ReportCenter entry', () => {
assert.match(reportOps, /ReportCenter::get_instance\(\)->apply_action\(\$target_type, \$target_id, \$action_taken, \$reason\)/)
assert.match(reportCenter, /public function apply_action\(/)
assert.match(reportCenter, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
assert.match(reportCenter, /lift_user_restriction\(\$target_id\)/)
assert.match(reportCenter, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
assert.ok(!reportCenter.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
for (const deadAction of ["'value' => 'warned'", "'value' => 'muted'", "'value' => 'restricted'", "'value' => 'banned'"]) {
assert.ok(!reportOps.includes(deadAction), `action value must map to Restriction types: ${deadAction}`)
}
for (const action of ["'value' => 'warn'", "'value' => 'mute'", "'value' => 'suspend'", "'value' => 'ban'", "'value' => 'clear'"]) {
assert.ok(reportOps.includes(action), `missing action option: ${action}`)
}
assert.match(reportOps, /get_user_restriction_state/)
assert.ok(!usersMenus.includes('$svc->apply_action('), 'native page follows the reference theme flow (status-only update)')
})
test('restriction class supports lifting and kicks sessions on ban', () => {
assert.match(restriction, /public function lift_user_restriction\(int \$user_id/)
assert.match(restriction, /public function lift_user_restriction\(int \$user_id, array \$types = \[self::TYPE_BAN, self::TYPE_SUSPEND, self::TYPE_MUTE\]\): int/)
assert.match(restriction, /WP_Session_Tokens::get_instance\(\$user_id\)->destroy_all\(\)/)
assert.ok(!restriction.includes('sync_user_restriction_meta'), 'write-only meta sync is dead code')
assert.ok(!restriction.includes('_aksrt_is_banned'))
})
test('ban admin REST endpoints stay in sync with the SPA client', () => {
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrictions'/)
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrict'/)
// The self-built SPA ban-manager page is removed: ban enforcement lives on
// the native reference-theme page, and the SPA hosts the settings schema.
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersBan.vue')), 'self-built SPA ban page must stay deleted')
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersReport.vue')), 'self-built SPA report queue must stay deleted')
})
test('ban/report settings page mirrors the reference theme schema', () => {
const schemaPhp = read('src/Admin/Schema/SchemaUsers.php')
const schemaJs = read('assets/admin/src/schema/sections/users/report.js')
const sections = read('assets/admin/src/constants/sections/users.js')
// SPA 侧边栏“用户互动 → 禁封/举报”挂载为 schema 设置页(非跳转页)
assert.match(sections, /schemaPage\(\{\s*\n\s*key: 'users-ban',/)
assert.match(sections, /label: '禁封\/举报'/)
assert.match(sections, /section: 'report'/)
assert.ok(!sections.includes("component: 'NativeOpsRedirect'"), 'ban page must not be a redirect stub')
// 字段键 1:1 对齐参考主题:禁封/举报开关、预置原因、申诉说明、申诉资料、图片举报、举报说明
for (const key of ['user_ban_s', 'user_report_s', 'ban_preset_reason', 'ban_appeal_desc', 'ban_appeal_keys', 'user_report_img_s', 'user_report_desc']) {
assert.ok(schemaPhp.includes(`'${key}'`), `PHP schema missing reference key: ${key}`)
assert.ok(schemaJs.includes(key), `SPA schema missing reference key: ${key}`)
}
// 参考主题文案与 group 结构
for (const label of ['禁封功能', '举报功能', '预置原因', '申诉说明', '申诉所需资料', '举报可上传图片', '举报说明']) {
assert.ok(schemaJs.includes(`label: '${label}'`), `SPA schema missing reference label: ${label}`)
}
assert.match(schemaJs, /type: 'group'/)
assert.match(schemaJs, /添加原因/)
assert.match(schemaJs, /添加申诉资料/)
// 运行时读取兼容 group 数组 [{t}] 并保留旧格式回退
const runtime = read('src/Settings/UserRuntime.php')
assert.match(runtime, /ban_settings\(\): array/)
assert.match(runtime, /report_settings\(\): array/)
assert.match(runtime, /user_report_s/)
// 缺键回退 schema 默认值(参考主题 _pz 语义):运行时经 read_domain 读取,
// 裸 get_all() 会让未保存过分区的站点拿不到 ban_preset_reason 默认值。
assert.match(runtime, /read_domain\('users', 'report'\)/)
// 预置原因 slug 不得由 sanitize_key(标签) 直接派生:中文标签会被清洗成
// 空串,前端按 slug 非空过滤后整张原因列表会被丢弃。ASCII 键优先,
// 否则用标签内容的稳定 md5 短哈希。
assert.match(runtime, /md5\(\$text\)/)
// 死链清理:user_report_categories 从不是真实存储键,解析器必须删除。
assert.ok(!runtime.includes('parse_report_categories'), 'dead parse_report_categories must stay removed')
assert.ok(!runtime.includes('user_report_categories'), 'dead user_report_categories key must stay removed')
const appeal = read('src/User/BanAppeal.php')
assert.match(appeal, /ban_appeal_keys/)
assert.match(appeal, /ban_appeal_desc/)
})
test('ban appeal approval actually lifts restrictions', () => {
assert.match(usersMenus, /lift_user_restriction\(\$user_id\)/)
assert.ok(!usersMenus.includes("SET status = 'lifted'"), 'restriction table has no status column')
assert.ok(!usersMenus.includes('clear_user_bans'))
assert.ok(!usersMenus.includes('lift_ban'))
})
test('profile governance notice reads from the restriction table', () => {
assert.match(home, /->get_display_state\(\$user_id\)/)
assert.match(home, /Restriction::state_labels\(\)\[\$state\]/)
assert.match(home, /\\AKSRT\\User\\Restriction::get_instance\(\)/)
assert.ok(!home.includes('aksrt_user_ban_state'))
// 提示框内容契约:state|label|text,label 对齐参考主题公开主页禁封徽章文案。
assert.match(home, /'state'\s*=> \$state/)
assert.match(home, /'label'\s*=>/)
assert.match(home, /'text'\s*=> \(string\) \$settings\['report'\]\['ban_notice_text'\]/)
// 模板必须消费真实契约键:tone/message 双键曾导致资料卡下方提示框渲染为空。
assert.match(authorHero, /\$notice\['state'\]/)
assert.match(authorHero, /\$notice\['text'\]/)
assert.match(authorHero, /\$notice\['label'\]/)
assert.ok(!authorHero.includes("$notice['message']"), 'template must not read the removed message key')
assert.ok(!authorHero.includes("$notice['tone']"), 'template must not read the removed tone key')
})
test('governance display state is a single source shared by every prompt surface', () => {
// 展示态与文案集中在 Restriction(资料卡/用户中心/评论拒绝态/昵称徽章共用)。
for (const state of ['STATE_BANNED', 'STATE_RESTRICTED', 'STATE_MUTED']) {
assert.match(restriction, new RegExp(`const ${state}\\s+=`), `missing display state: ${state}`)
}
assert.match(restriction, /public function get_display_state\(int \$user_id\): string/)
assert.match(restriction, /public static function state_labels\(\): array/)
for (const label of ["__('违规已封号', 'aksrt')", "__('小黑屋禁封中', 'aksrt')", "__('禁言中', 'aksrt')"]) {
assert.ok(restriction.includes(label), `missing state label: ${label}`)
}
assert.match(restriction, /is_banned\(\$user_id\)[\s\S]*?is_suspended\(\$user_id\)[\s\S]*?is_restricted\(\$user_id, self::TYPE_MUTE\)/)
// 请求级缓存:评论列表逐条渲染昵称徽章不得反复查库。
assert.match(userBanHelper, /static \$cache = \[\]/)
})
test('ban prompt surface mirrors the reference theme entry points', () => {
// 用户中心页头(参考主题 user_page_header_desc / zib_get_user_ban_info_link)。
assert.match(userBanHelper, /function aksrt_get_user_ban_notice_html/)
assert.match(userBanHelper, /function aksrt_get_ban_appeal_page_url/)
assert.match(ucHeader, /aksrt_get_user_ban_notice_html/)
// 用户中心提示排版与相邻元信息一致:复用 uc-header__meta-item 结构,
// 不用 badg-sm(0.8em 破坏字号一致性),箭头置于提示内部。
const noticeFn = userBanHelper.slice(userBanHelper.indexOf('function aksrt_get_user_ban_notice_html'))
const noticeBody = noticeFn.slice(0, noticeFn.indexOf('\n}'))
assert.match(noticeBody, /\$class = 'uc-header__meta-item aksrt-user-ban-notice';/)
assert.ok(!noticeBody.includes('badg-sm'), 'user center notice must not use badg-sm (shrinks font vs meta row)')
assert.match(noticeBody, /fa-angle-right/)
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
assert.match(userBanHelper, /function aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /暂时无法发表评论/)
// 昵称徽章 + 头像角标(参考主题 user_name_badge / user_avatar_badge)。
assert.match(userBanHelper, /function aksrt_get_user_ban_name_badge/)
assert.match(userBanHelper, /function aksrt_get_user_ban_avatar_badge/)
assert.match(identityHelper, /aksrt_get_user_ban_name_badge\(\$user_id\)/)
assert.match(avatarHelper, /aksrt_get_user_ban_avatar_badge\(\$user_id\)/)
// 禁封申诉页完整状态信息(参考主题 zib_get_ban_info_modal)。
assert.match(appealPage, /get_active_suspend\(\$user_id\)/)
for (const row of ['状态', '开始时间', '结束时间', '原因', '说明']) {
assert.match(appealPage, new RegExp(`__\\('${row}', 'aksrt'\\)`), `appeal page missing status row: ${row}`)
}
assert.match(appealPage, /esc_html_e\('申诉权限', 'aksrt'\)/)
assert.match(appealPage, /禁封中\(禁止登录\)/)
assert.match(appealPage, /本次禁封您将不能发起申诉/)
// 禁封/小黑屋均可申诉(参考主题 zib_user_is_ban 双类型语义)。
assert.match(banAppeal, /STATE_BANNED, \\AKSRT\\User\\Restriction::STATE_RESTRICTED/)
// 禁封站内信附申诉入口(参考主题 zib_updata_user_ban_msg 申诉指引)。
assert.match(restrictionOps, /aksrt_get_ban_appeal_page_url/)
// 头像角标样式:源文件与入队的 min 产物同步。
for (const css of [mainCss, mainMinCss]) {
assert.ok(css.includes('aksrt-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
}
})
test('report entry hides targets already banned or suspended', () => {
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
assert.match(userBanHelper, /function aksrt_is_user_ban_restricted/)
assert.match(commentItem, /aksrt_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /aksrt_is_user_ban_restricted\(\$post_author_id\)/)
assert.match(authorHero, /aksrt_is_user_ban_restricted\(\$user_id\)/)
})
test('admin report notification links to the real page', () => {
assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban'/)
assert.ok(!mailNotifications.includes('page=aksrt_reports&'), 'dead admin page link')
assert.ok(!mailNotifications.includes('tab=reports'), 'merged ticket page has no reports tab')
// 处理说明必须随 aksrt_report_status_changed 传入邮件载荷(此前硬编码空串,
// 举报人收到的“说明:”永远为空)。
assert.match(reportCenter, /do_action\('aksrt_report_status_changed', \$report_id[\s\S]*?new_status, \$note_clean\)/)
assert.match(mailNotifications, /function report_status_changed\(int \$report_id, int \$reporter_id, string \$status, string \$note = ''\)/)
assert.match(mailNotifications, /'note'\s*=>\s*\$note !== '' \? \$note :/)
// 举报通知必须入站内消息系统:提交→管理员(直达工单列表)、
// 处理结果→举报人(与结果邮件同一时机、同一说明)。
assert.match(reportCenter, /notify_admins_new_report\(/)
assert.match(reportCenter, /notify_reporter_status_changed\(/)
assert.match(reportCenter, /Message::get_instance\(\)->send\(/)
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&type=report&status=pending/)
})
test('native ban page mirrors the reference theme layout', () => {
const tickets = read('src/Admin/Native/UsersMenusTickets.php')
// 菜单与页面标题对齐参考主题(page title=处理举报及禁封申诉,menu title=举报&禁封申诉)
assert.ok(usersMenus.includes(`__('处理举报及禁封申诉', 'aksrt'),\n __('举报&禁封申诉', 'aksrt')`), 'submenu titles should match the reference theme')
assert.ok(usersMenus.includes('用户禁封申诉及用户举报管理'), 'page heading should match the reference theme')
// 类型筛选按钮组 + 状态 subsubsub + 搜索
for (const label of [`__('禁封申诉', 'aksrt')`, `__('用户举报', 'aksrt')`, `__('待处理', 'aksrt')`, `__('已批准', 'aksrt')`, `__('已拒绝', 'aksrt')`, `__('搜索记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing reference-theme label: ${label}`)
}
// 表头列与参考主题一致:状态/类型/提交用户/详情/提交时间/更新时间(带排序)
for (const head of ['状态', '类型', '提交用户', '详情', '提交时间', '更新时间']) {
assert.ok(tickets.includes(`__('${head}', 'aksrt')`), `missing table head: ${head}`)
}
assert.match(tickets, /orderby-but/)
assert.match(tickets, /dashicons-arrow-up/)
// 行渲染与处理表单对齐参考主题
for (const label of [`__('立即处理', 'aksrt')`, `__('正在处理', 'aksrt')`, `__('被举报用户', 'aksrt')`, `__('被举报用户禁封信息', 'aksrt')`, `__('申诉用户', 'aksrt')`, `__('禁封信息', 'aksrt')`, `__('申诉信息', 'aksrt')`, `__('处理方式', 'aksrt')`, `__('处理留言', 'aksrt')`, `__('申诉通过(解封该用户)', 'aksrt')`, `__('申诉拒绝(保持原禁封状态)', 'aksrt')`, `__('此处内容会反馈给举报人', 'aksrt')`, `__('确认提交', 'aksrt')`, `__('用户状态正常', 'aksrt')`, `__('当前用户已解封', 'aksrt')`, `__('此用户已解封', 'aksrt')`, `__('暂无申请记录', 'aksrt')`]) {
assert.ok(tickets.includes(label), `missing process form label: ${label}`)
}
// 处理表单必须携带隐藏 action 字段:admin-post.php 靠它触发
// admin_post_{action} 钩子,缺失时 WordPress 不输出任何内容(白屏)。
// value 按工单类型分发(report → update_report,appeal → process_appeal)。
assert.match(tickets, /name="action" value="' \. esc_attr\(\$kind === 'report' \? 'aksrt_users_update_report' : 'aksrt_users_process_appeal'\)/)
assert.match(usersMenus, /admin_post_aksrt_users_update_report/)
assert.match(usersMenus, /admin_post_aksrt_users_process_appeal/)
// 内联处理视图(参考主题方向):表单以合法的 colspan=6 单元格随待处理行
// 预渲染;展开/取消走 JS 行↔表单互换(无整页刷新),信息只出现一次,
// 不再裸嵌 tbody 被 foster-parent 错位。
assert.match(tickets, /<td colspan="6">';\s*\$this->render_ticket_process_form/)
assert.match(tickets, /data-aksrt-ticket-toggle="/)
assert.match(tickets, /data-aksrt-ticket-cancel="/)
assert.match(tickets, /data-aksrt-ticket-row="/)
assert.match(tickets, /e\.preventDefault\(\);/)
// 展开的处理表单提供“取消”按钮(确认提交右侧)。
assert.match(tickets, /class="button button-primary process-submit"/)
assert.match(tickets, /<a class="button" data-aksrt-ticket-cancel="' \. esc_attr\(\$row\['kind'\] \. '-' \. \(string\) \$row\['id'\]\) . '" href="' \. esc_url\(remove_query_arg\(\['action', 'id'\]\)\) . '">' \. esc_html__\('取消', 'aksrt'\)/)
// 分页对齐参考主题 zibpay_admin_pagenavi(每页 20 条)
assert.match(tickets, /TICKETS_PER_PAGE = 20/)
assert.match(tickets, /ticket_pagenavi\(int \$total_count, int \$per_page\)/)
for (const label of ['前往第一页', '前往上一页', '前往下一页', '前往最后一页', 'displaying-num', 'paging-input', 'total-pages']) {
assert.ok(tickets.includes(label), `missing pagenavi part: ${label}`)
}
// 旧的三 tab 结构已收敛;举报处理表单不包含处罚下拉(对齐参考主题)
assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
// 禁封限制 Tab 已移除(参考主题无此后台 Tab):禁封改走前台封禁弹窗。
assert.ok(!usersMenus.includes('tab=restrictions'), 'restrictions tab is gone')
assert.ok(!usersMenus.includes('render_restrictions_tab'), 'restrictions tab renderer is gone')
assert.ok(!usersMenus.includes('handle_add_restriction'), 'backend ban form handler is gone')
assert.ok(!usersMenus.includes('handle_remove_restriction'), 'backend unban form handler is gone')
assert.ok(!usersMenus.includes('render_reports_tab'))
assert.ok(!usersMenus.includes('render_appeals_tab'))
assert.match(usersMenus, /use UsersMenusTickets;/)
assert.ok(!tickets.includes('name="action_taken"'), 'report process form should be status-only like the reference theme')
// 工单处理页「点击此处」跳转前台用户公开主页(封面三点菜单 → 封禁弹窗)。
assert.match(tickets, /aksrt_get_user_home_url\(\$target_id\)/)
assert.ok(!tickets.includes('tab=restrictions'), 'ticket ban link must not point at the removed backend tab')
})
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
const scripts = read('src/Core/ScriptsPart1c.php')
// 预置原因真实数据源是 ban_preset_reason(UserRuntime::ban_settings),
// 不再读取 report_settings 中不存在的 categories 键(短路已修)。
assert.match(scripts, /UserRuntime::ban_settings\(\)\['reasons'\]/)
assert.ok(!scripts.includes("$report['categories']"), 'dead report.categories source must be removed')
// 图片举证开关与举报说明随 aksrtData 注入。
assert.match(scripts, /imgEnabled/)
assert.match(scripts, /report_settings\(\)/)
const reportJs = read('assets/js/report.js')
// Zibll 同构标记:edit-header 警示头、原因徽章 active 焦点色、其它原因、
// 图片举证入口(后台开启时)、举报说明注入、but-average modal-buts 底栏。
for (const part of ['edit-header', 'toggle-radius mr10 c-red', 'data-aksrt-report-reason', 'form-but-radio', '其它原因', '违规链接', '详细描述', '添加图片举证', 'but-average modal-buts']) {
assert.ok(reportJs.includes(part), `report.js missing reference-theme part: ${part}`)
}
assert.match(reportJs, /report\/upload-image/)
assert.ok(!reportJs.includes('is-active'), 'chips must toggle the reference .active class, not a custom one')
// 举报图片上传端点存在,复用评论上传回调、独立权限(登录 + 举报 + 图片开关)。
assert.match(userController, /'\/report\/upload-image'/)
// 几何 1:1:桌面端 600px(参考主题 Bootstrap 默认模态宽)。
const css = read('assets/css/frontend/modal-runtime.css')
assert.match(css, /\.aksrt-report-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /600px/)
// 加号图标单一来源:main.css 的 .preview .add:before/:after(inset:0 + margin:auto),
// modal-runtime 不得重复定义(否则过约束冲突,加号偏移只剩“L”形)。
assert.ok(!/form-upload \.preview \.add::?(before|after)/.test(css), 'duplicated add plus rules must stay out of modal-runtime.css')
})
test('comments expose a report entry for other users when reporting is enabled', () => {
// 举报位于“回复”右侧三点下拉菜单内(参考主题同位置),载荷走 wp_json_encode。
assert.match(commentItem, /data-aksrt-report="/)
assert.match(commentItem, /function aksrt_get_comment_report_html/)
assert.match(commentItem, /comment-report-link/)
assert.match(commentItem, /report'\]\['enabled'\]/)
assert.match(commentItem, /\$author_id === \$reporter_id/)
// 下拉菜单(置顶/编辑/批准/删除 + 禁封或举报)末项走禁封优先、否则举报。
assert.match(commentItem, /function aksrt_get_comments_action_dropdown_html[\s\S]*?\$lists \.= aksrt_get_comment_ban_or_report_item/)
// 禁封项(管理员可见):参考主题 comments_action_lists 钩子语义,渲染于 user-ban.php。
assert.match(userBanHelper, /comment-ban-link/)
assert.match(userBanHelper, /data-aksrt-ban="/)
// 举报不作为 comt-meta 独立按钮直接渲染。
assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/)
})
test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal 1:1', () => {
// 禁封方式/禁封时间/预置原因徽章/其它原因/禁封说明/删除全部内容/禁止用户申诉,
// 以及已禁封用户的状态信息与解封留言表单(弹窗经 REST /user-ban/modal 输出)。
for (const label of ['拉入小黑屋', '禁封用户', '禁封方式', '禁封时间', '选择禁封原因', '其它原因', '禁封说明', '删除全部内容', '禁止用户申诉', '请填写禁止申诉说明', '解封用户', '移出小黑屋', '禁封中(禁止登录)', '小黑屋', '给用户的留言', '开始时间', '结束时间', '原因', '说明', '禁封记录', '确认禁封']) {
assert.ok(userBanHelper.includes(label), `ban modal missing reference label: ${label}`)
}
// 时长选项 1/7/14/30/100/永久(默认 7 天);类型映射:小黑屋→suspend,禁封→ban。
assert.match(userBanHelper, /1 => __\('1天', 'aksrt'\)/)
assert.match(userBanHelper, /7 => __\('7天', 'aksrt'\)/)
assert.match(userBanHelper, /100 => __\('100天', 'aksrt'\)/)
assert.match(userBanHelper, /7 === \$days \? ' checked="checked"' : ''/)
// 预置原因来自 UserRuntime::ban_settings(与后台同一数据源)。
assert.match(userBanHelper, /UserRuntime::ban_settings\(\)\['reasons'\]/)
// 勾选禁止申诉必须填写说明(参考主题同校验),落用户 meta 供申诉入口拦截。
assert.match(userBanHelper, /_aksrt_no_appeal|no_appeal/)
const appeal = read('src/User/BanAppeal.php')
assert.match(appeal, /_aksrt_no_appeal/)
// 弹窗入口按钮契约:[data-aksrt-ban] 委托(封面按钮 / 评论菜单项 / 文章作者菜单项)。
assert.match(userBanHelper, /data-aksrt-ban="/)
assert.match(userBanHelper, /function aksrt_get_user_cover_menu_action_html/)
assert.match(userBanHelper, /function aksrt_get_comment_ban_or_report_item/)
assert.match(userBanHelper, /function aksrt_get_post_author_ban_item/)
// 权限模型(参考主题 zib_current_user_can('set_user_ban') 语义):
// manage_options + 非本人 + 非超级管理员 + 功能开关。
assert.match(userBanHelper, /current_user_can\('manage_options'\)/)
assert.match(userBanHelper, /\$current_id === \$user_id/)
assert.match(userBanHelper, /is_super_admin\(\$user_id\)/)
assert.match(userBanHelper, /user_ban_s/)
// REST 路由:modal 拉取 + set 提交(permission_callback 校验)。
assert.match(userController, /'\/user-ban\/modal'/)
assert.match(userController, /'\/user-ban\/set'/)
// 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...))。
assert.match(userBanHelper, /modal-colorful-header colorful-bg c-yellow/)
// 提交后整页刷新(禁封 reload:true)/ 仅关闭弹窗(解封 hide_modal:true)。
assert.match(userBanJs, /window\.location\.reload\(\)/)
assert.match(userBanJs, /AksrtModalRuntime\.close\('submit'\)/)
assert.match(userBanJs, /data-aksrt-ban-submit/)
assert.match(userBanJs, /data-aksrt-unban-submit/)
// 桌面 600px(参考主题未传 data-class → Bootstrap .modal-dialog 默认宽)
// + min-height 360px(data-height=360);移动端底部抽屉。
assert.match(userBanJs, /MODAL_HEIGHT = 360/)
assert.match(userBanJs, /mobileBottom: true/)
const css = read('assets/css/frontend/modal-runtime.css')
assert.match(css, /\.aksrt-user-ban-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /600px/)
// textarea autoHeight 1:1(参考主题 maxHeight 锁高 + 边框补偿)。
assert.match(userBanJs, /getAttribute\('maxHeight'\)/)
// 三个入口模板接线:封面三点菜单 / 评论区三点菜单 / 文章作者 metabox 三点菜单。
const authorHero = read('templates/parts/user/author-hero.php')
assert.match(authorHero, /aksrt_get_user_cover_menu_action_html\(\$user_id\)/)
assert.match(commentItem, /aksrt_get_comment_ban_or_report_item\(\$comment\)/)
const singleComponents = read('inc/helpers/aksrt-single-components.php')
assert.match(singleComponents, /aksrt_get_post_author_ban_item\(/)
// 脚本入队(singular + profile,与 report.js 同组)。
const scripts = read('src/Core/ScriptsPart1b.php')
assert.match(scripts, /'aksrt-user-ban'/)
})
test('uninstall cleans up every ban/report meta key written by the theme', () => {
for (const key of ['_aksrt_active_restrictions', '_aksrt_is_banned', '_aksrt_no_appeal', 'aksrt_user_ban_state']) {
assert.ok(uninstall.includes(`'${key}'`), `missing uninstall cleanup: ${key}`)
}
})
test('restriction REST operations pass expiry through to the service', () => {
assert.match(restrictionOps, /\$params\['expires_at'\] \?\? ''/)
assert.match(restrictionOps, /Restriction::get_instance\(\)->restrict\(/)
})
test('suspend (小黑屋) enforcement blocks publishing end to end', () => {
// 服务层提供 is_suspended / get_active_suspend(对齐参考主题:小黑屋
// 失去所有发布权限——发帖、评论等;此前 suspend 只在公开主页显示提示)。
assert.match(restriction, /public function is_suspended\(int \$user_id\): bool/)
assert.match(restriction, /public function get_active_suspend\(int \$user_id\): \?array/)
// 前台发帖入口(FrontSubmitSave::persist)拦截发帖/草稿/编辑。
const frontSave = read('src/Core/FrontSubmitSave.php')
assert.match(frontSave, /Restriction::get_instance\(\)->is_suspended\(\$user_id\)/)
assert.match(frontSave, /您已被关入小黑屋/)
// 评论提交(pre_comment_on_post)同时覆盖禁言(mute)与小黑屋(suspend)。
assert.match(restriction, /enforce_comment_mute[\s\S]*?is_suspended\(\$user_id\)/)
assert.match(restriction, /您已被关入小黑屋,暂时无法评论/)
})
test('report modal close icon and textarea autoHeight match the reference theme', () => {
const reportJs = read('assets/js/report.js')
// 关闭按钮走 SVG 雪碧图 icon-close(与参考主题 .ic-close 同构,fa-times 退役)。
assert.match(reportJs, /<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"><\/use><\/svg>/)
assert.ok(!reportJs.includes('fa-times'), 'close icon must use the SVG sprite, not FontAwesome')
// textarea 自动增高(参考主题 autoHeight 行为),隐藏中的“其它原因”跳过首次计算。
assert.match(reportJs, /function bindAutoHeight\(/)
assert.match(reportJs, /querySelectorAll\('textarea\.form-control'\)\.forEach\(bindAutoHeight\)/)
assert.match(reportJs, /offsetHeight > 0\) grow\(\)/)
})
test('wechat template messages cover report and account restriction types', () => {
const wechat = read('src/Payment/WeChatNotify.php')
assert.match(wechat, /const NOTIFY_TYPES = \['comment_reply', 'system_notice', 'order_paid', 'credits_change', 'report', 'system'\]/)
assert.match(wechat, /in_array\(\$type, self::NOTIFY_TYPES, true\)/)
// 订阅白名单与状态输出复用同一常量,不允许再出现硬编码子集。
assert.ok(!wechat.includes("['comment_reply', 'system_notice', 'order_paid', 'credits_change']"), 'type list must come from NOTIFY_TYPES')
// 站内通知缺省模板字段时用 title/description 兜底合成。
assert.match(wechat, /\$data\['description'\] \?\? ''/)
})