Files
wordpress-theme-aksrt/tests/widget-system/report-ban-contract.test.cjs
T
kate522 7597972eea feat(user-ban): frontend ban modal replaces the backend restrictions tab
1:1 replicate the reference theme user-ban flow on the frontend:

- Ban/unban modal rendered by inc/helpers/user-ban.php, opened via
  AksrtModalRuntime.openRemote (600px desktop dialog, 360px initial
  height, mobile bottom drawer, c-yellow colorful unban header).
- Three entry points wired: user homepage cover three-dot menu,
  comment section menu, article author card menu (ban item takes
  priority over report, matching the reference hook order).
- REST endpoints /user-ban/modal and /user-ban/set under aksrt/v1,
  guarded by check_permission, persisted through User\Restriction.
- Submit semantics match zib_ajax_set_user_ban: ban reloads the page,
  unban only closes the modal.

Cleanup (no dual-track): the backend ban-restrictions tab, its form
handlers and flash keys are removed; ticket processing links to the
frontend user homepage as the reference theme does; admin/report and
appeal notification URLs move to type/status query args.

Verification: 337/337 widget-system contract tests, 41/41 admin and
front-submit tests, php -l clean on all changed PHP files.
2026-09-20 18:42:53 +08:00

370 lines
25 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')
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, /->is_banned\(\$user_id\)/)
assert.match(home, /\\AKSRT\\User\\Restriction::get_instance\(\)/)
assert.ok(!home.includes('aksrt_user_ban_state'))
})
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'\] \?\? ''/)
})