'use strict'; /** * 消息管理后台(users.php → jio_messages)契约: * 1) 页面由独立 trait 承载,UsersMenus 只做组合与路由注册; * 2) 写操作走 admin_post + nonce + 能力校验; * 3) SQL 全部在 NotificationAdminQuery 内且经 prepare; * 4) 分页用共享 AdminListPagination; * 5) 旧实现与死代码已删除。 */ 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'); test('messages page is composed from its own trait', () => { const menus = read('src/Admin/Native/UsersMenus.php'); assert.ok(menus.includes('use UsersMenusMessages;')); assert.ok(menus.includes("'jio_messages'")); assert.ok(menus.includes("add_action('admin_post_jio_users_send_message', [$this, 'handle_send_message'])")); assert.ok(menus.includes("add_action('admin_post_jio_users_bulk_messages', [$this, 'handle_bulk_messages'])")); assert.ok(menus.includes("add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets'])")); // 旧实现不得残留在壳类中。 assert.doesNotMatch(menus, /function render_messages_page|function handle_send_message|message_sent|message_failed/); assert.doesNotMatch(menus, /jio_notification/); }); test('write handlers check capability and nonce', () => { const page = read('src/Admin/Native/UsersMenusMessages.php'); assert.match(page, /function handle_send_message\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('jio_users_send_message'\);/); assert.match(page, /function handle_bulk_messages\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('jio_users_bulk_messages'\);/); const compose = read('src/Admin/Native/UsersMenusMessagesCompose.php'); const list = read('src/Admin/Native/UsersMenusMessagesList.php'); assert.ok(page.includes('use UsersMenusMessagesCompose;')); assert.ok(page.includes('use UsersMenusMessagesList;')); assert.ok(compose.includes("wp_nonce_field('jio_users_send_message')")); assert.ok(list.includes("wp_nonce_field('jio_users_bulk_messages')")); // 页面层不直接碰数据库,分页走共享组件。 assert.doesNotMatch(page + compose + list, /\$wpdb/); assert.ok(page.includes('AdminListPagination::render(')); }); test('admin query keeps SQL prepared and bulk size capped', () => { const query = read('src/Messages/NotificationAdminQuery.php'); assert.match(query, /DELETE FROM \{\$table\} WHERE id IN \(\{\$in\}\)", \$ids\)/); assert.ok(query.includes("array_fill(0, count($ids), '%d')")); assert.ok(query.includes('array_slice($ids, 0, self::MAX_BULK)')); assert.ok(query.includes('$wpdb->esc_like($s)')); }); test('dead notification helpers are removed', () => { assert.doesNotMatch(read('inc/modules/user-notifications.php'), /jio_notification_type_label/); assert.doesNotMatch(read('src/Messages/Message.php'), /get_all_notifications/); assert.ok(fs.existsSync(path.join(root, 'assets/css/admin/messages.css'))); }); let rendered; function harness() { if (!rendered) { const result = require('node:child_process').spawnSync('php', [path.join(__dirname, 'message-admin-harness.php')], { encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr + result.stdout); rendered = JSON.parse(result.stdout); } return rendered; } test('list toolbar is merged into the table card and inline confirmation is removed', () => { const page = read('src/Admin/Native/UsersMenusMessages.php'); const list = read('src/Admin/Native/UsersMenusMessagesList.php'); assert.ok(page.indexOf("NativeUi::card_open(__('站内通知'") < page.indexOf('$this->render_message_filter_form')); assert.doesNotMatch(page + list, /onclick=|confirm\(| { const { table, empty } = harness(); assert.match(table, /发送时间<\/th>操作<\/th><\/tr>/); const rows = [...table.matchAll(/([\s\S]*?)<\/tr>/g)]; assert.equal(rows.length, 2); rows.forEach(([, row]) => { const cells = [...row.matchAll(/<(?:th|td)\b[^>]*>[\s\S]*?<\/(?:th|td)>/g)].map(([cell]) => cell); assert.equal(cells.length, 7); assert.match(cells[1], /class="column-title"/); assert.doesNotMatch(cells[1], / { const { table, cache_calls } = harness(); assert.match(table, /