Files
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

188 lines
9.3 KiB
JavaScript
Raw Permalink 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.
'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\(|<details|jio-native-toolbar/);
const { table, empty } = harness();
assert.match(table, /data-jio-msg-action="delete" disabled/);
assert.match(table, /data-jio-msg-selection/);
assert.match(table, /name="period" value="today"/);
assert.match(empty, /当前筛选条件下没有消息/);
});
test('message row actions live only in a dedicated rightmost operations column', () => {
const { table, empty } = harness();
assert.match(table, /<th scope="col" class="column-time">发送时间<\/th><th scope="col" class="column-actions">操作<\/th><\/tr>/);
const rows = [...table.matchAll(/<tr class="is-(?:unread|read)">([\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], /<button|data-jio-msg-view|data-jio-msg-action/);
assert.match(cells[5], /class="column-time"/);
assert.match(cells[6], /class="column-actions"/);
assert.match(cells[6], /data-jio-msg-view=/);
assert.match(cells[6], /data-jio-msg-action="delete"/);
});
assert.match(empty, /colspan="7"/);
const css = read('assets/css/admin/messages.css');
assert.match(css, /\.jio-messages-admin \.jio-messages-admin__table \.column-actions\s*\{[^}]*padding-left:\s*20px;[^}]*padding-right:\s*20px;[^}]*text-align:\s*center;/);
assert.match(css, /\.jio-msg-row-actions\s*\{[^}]*justify-content:\s*center;/);
});
test('details preserve full content, escape HTML and unsafe links, and never mark read', () => {
const { table, cache_calls } = harness();
assert.match(table, /<template id="jio-msg-detail-8">/);
assert.match(table, /完整内容(?:完整内容){99}/);
assert.match(table, /&lt;script&gt;x&lt;\/script&gt;/);
assert.doesNotMatch(table, /<script>|href="javascript:/);
assert.match(table, /2026-10-30 11:30/);
assert.match(table, /查看详情不会改变收件人的已读状态/);
assert.equal((table.match(/data-jio-msg-action="mark_read" data-id=/g) || []).length, 1);
assert.deepEqual(cache_calls, [[7]]);
});
test('role choices display counts and disable only empty or oversized roles', () => {
const { compose } = harness();
assert.match(compose, /value="subscriber" data-count="3"[^>]*>订阅者 · 3 人/);
assert.match(compose, /value="editor" data-count="501"[^>]* disabled/);
assert.match(compose, /value="author" data-count="0"[^>]* disabled/);
assert.match(compose, /value="contributor" data-count="500"[^>]*>贡献者 · 500 人/);
assert.doesNotMatch(compose, /value="contributor"[^>]* disabled/);
assert.match(compose, /data-count-limit="191"/);
assert.match(compose, /data-count-limit="2000"/);
assert.match(compose, /aria-describedby="jio-msg-content-count"/);
assert.match(compose, /data-jio-msg-pane="role" hidden/);
});
test('overview links, status tabs, filter forms and mutations preserve period filtering', () => {
const data = harness();
assert.equal(data.filters.period, 'today');
assert.equal(data.bad_period.period, '');
assert.equal(data.query_filters.period, 'today');
assert.match(data.overview, /period=today/);
assert.match(data.overview, /status=unread/);
assert.match(data.filter_form, /value="today" selected/);
assert.match(data.views, /period=today/);
assert.equal(data.single_delete.redirect.period, 'today');
assert.equal(data.single_delete.redirect.status, 'unread');
assert.equal(data.single_delete.redirect.paged, '3');
});
test('today counts and filtered lists share prepared site-timezone bounds including DST', () => {
const { sql, dst_sql } = harness();
assert.equal(sql.length, 4);
sql.forEach(({ sql, params }) => {
assert.match(sql, /created_at >= %s AND created_at < %s/);
assert.ok(params.includes('2026-10-29 16:00:00'));
assert.ok(params.includes('2026-10-30 16:00:00'));
});
assert.doesNotMatch(sql[2].sql, /is_read = 0 AND/);
assert.ok(sql[0].params.includes('%a\\%\\_%'));
assert.deepEqual(dst_sql[0].params, ['2026-11-01 04:00:00', '2026-11-02 05:00:00']);
});
test('role broadcasts require confirmation of the current role and count on the server', () => {
const { send, cap_checked, nonce_checked } = harness();
['unconfirmed', 'changed_count', 'changed_role', 'too_large', 'long_title'].forEach((key) => {
assert.equal(send[key].sent, 0, key);
assert.equal(send[key].redirect.jio_msg, 'send_failed', key);
});
assert.equal(send.confirmed.sent, 3);
assert.equal(send.users.sent, 1);
assert.equal(cap_checked, 8);
assert.equal(nonce_checked.length, 8);
});
test('asset types use Chinese labels in filters, row badges and details', () => {
const { asset_types, filter_form, asset_table } = harness();
assert.deepEqual(asset_types, {
asset_balance_increase: ['余额增加', 'success'],
asset_balance_decrease: ['余额减少', 'danger'],
asset_points_increase: ['积分增加', 'success'],
asset_points_decrease: ['积分减少', 'danger'],
});
for (const [type, [label]] of Object.entries(asset_types)) {
assert.ok(filter_form.includes(`<option value="${type}">${label}</option>`));
}
assert.match(asset_table, /class="jio-msg-badge is-success"[^>]*>余额增加<\/a>/);
assert.match(asset_table, /<dt>消息类型<\/dt><dd>余额增加<\/dd>/);
});
test('single row actions cannot accidentally operate on the checked bulk selection', () => {
const { single_delete } = harness();
assert.deepEqual(single_delete.sql[0].params, [8]);
assert.match(single_delete.sql[0].sql, /DELETE FROM .* WHERE id IN/);
});