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>
191 lines
10 KiB
JavaScript
191 lines
10 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 「工单中心」配置契约:
|
||
* 1) PHP SchemaUsersTicket 分区与 SPA JS schema 的键、顺序、类型、默认值 1:1 对齐;
|
||
* 2) SPA 页面已注册(/users/ticket 可达),承载 schema 表单 + 三块字典 CRUD;
|
||
* 3) 运行时 TicketSettings 缺键回退 schema 默认,数值边界被夹取;
|
||
* 4) 前台入口三处均已注册:Center.php 面板定义 + sidebar-nav.php 侧栏项 +
|
||
* jio-user-center.php 的 built_in_tabs 与 view map。
|
||
*/
|
||
|
||
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 FIELD_ORDER = [
|
||
'ticket_s',
|
||
'ticket_num_prefix',
|
||
'ticket_notice_open',
|
||
'ticket_notice_text',
|
||
'ticket_attachment_s',
|
||
'ticket_attachment_max',
|
||
'ticket_order_link_s',
|
||
'ticket_urge_interval',
|
||
'ticket_title_max',
|
||
'ticket_content_max',
|
||
'ticket_reply_max',
|
||
];
|
||
|
||
const GROUPS = [
|
||
{ key: 'main', title: '工单功能' },
|
||
{ key: 'form', title: '提交设置' },
|
||
];
|
||
|
||
test('ticket schema keys match between PHP and SPA JS', () => {
|
||
const php = read('src/Admin/Schema/Users/SchemaUsersTicket.php');
|
||
const js = read('assets/admin/src/schema/sections/users/ticket.js');
|
||
|
||
for (const key of FIELD_ORDER) {
|
||
assert.ok(php.includes(`'key' => '${key}'`), `PHP schema missing ${key}`);
|
||
assert.ok(js.includes(`key: '${key}'`), `JS schema missing ${key}`);
|
||
}
|
||
|
||
// 顺序:两组各自的字段顺序必须一致(前端分组渲染依赖它)。
|
||
for (const group of GROUPS) {
|
||
const phpGroup = php.slice(php.indexOf(`'key' => '${group.key}'`));
|
||
const jsGroup = js.slice(js.indexOf(`key: '${group.key}'`));
|
||
const phpKeys = [...phpGroup.matchAll(/'key' => '([a-z_]+)'/g)].map((m) => m[1]);
|
||
const jsKeys = [...jsGroup.matchAll(/key: '([a-z_]+)'/g)].map((m) => m[1]);
|
||
assert.deepEqual(jsKeys, phpKeys, `group ${group.key} field order drifted`);
|
||
}
|
||
|
||
// 类型逐字段对齐。PHP 里 textarea 带 sanitize_type,取值要跳过它,
|
||
// 因此显式匹配 'type' => '...'(前后都是引号边界,不会撞上 sanitize_type)。
|
||
for (const key of FIELD_ORDER) {
|
||
const phpType = new RegExp(`'key' => '${key}'[^\\]]*'type' => '([a-z]+)'`).exec(php);
|
||
const jsType = new RegExp(`key: '${key}'[^}]*?\\btype: '([a-z]+)'`).exec(js);
|
||
assert.ok(phpType && jsType, `type not found for ${key}`);
|
||
assert.equal(jsType[1], phpType[1], `${key} type drifted`);
|
||
}
|
||
|
||
// sanitize_type 也必须一致:公告内容允许 HTML,两侧都要声明。
|
||
assert.ok(php.includes("'key' => 'ticket_notice_text', 'label' => '公告内容', 'type' => 'textarea', 'sanitize_type' => 'html'"),
|
||
'PHP notice text must declare sanitize_type html');
|
||
assert.ok(js.includes("key: 'ticket_notice_text', label: '公告内容', type: 'textarea', sanitize_type: 'html'"),
|
||
'JS notice text must declare sanitize_type html');
|
||
});
|
||
|
||
test('ticket schema is registered under users', () => {
|
||
const js = read('assets/admin/src/schema/sections/users/ticket.js');
|
||
assert.equal(js.includes("key: 'users.ticket'"), true, 'schema key must be users.ticket');
|
||
|
||
const index = read('assets/admin/src/schema/sections/users/index.js');
|
||
assert.ok(index.includes("import { ticketSchema } from './ticket.js'"), 'schema not imported');
|
||
assert.ok(/ticket:\s*ticketSchema/.test(index), 'schema not exported in usersSchemas');
|
||
|
||
const constants = read('assets/admin/src/constants/sections/users.js');
|
||
assert.ok(constants.includes("key: 'users-ticket'"), 'menu page key missing');
|
||
assert.ok(constants.includes("route: '/users/ticket'"), 'menu route missing');
|
||
assert.ok(constants.includes("section: 'ticket'"), 'menu section missing');
|
||
assert.ok(constants.includes("canonicalDomain: 'users'"), 'menu canonical domain missing');
|
||
|
||
// PHP 侧:分区由 SchemaUsers 组装,SchemaUsersTicket 提供内容。
|
||
const users = read('src/Admin/Schema/SchemaUsers.php');
|
||
assert.ok(users.includes("'ticket' => Users\\SchemaUsersTicket::build()"), 'PHP section not composed');
|
||
// 组装位置必须在 'sections' => [ ... ] 内部。SchemaProvider::get_sections() 只读
|
||
// $schema['sections'],分区挂在同级会让 /settings/schema/users/ticket 404
|
||
// (SPA 报「未知的设置分组」)。定位 sections 开括号后用配平扫描找闭括号,
|
||
// 确认 compose 调用落在区间内——单纯 includes() 查不出层级错误。
|
||
const sectionsAt = users.indexOf("'sections' => [");
|
||
assert.ok(sectionsAt !== -1, 'sections array not found');
|
||
const composeAt = users.indexOf("'ticket' => Users\\SchemaUsersTicket::build()");
|
||
assert.ok(composeAt > sectionsAt, 'ticket must be composed after sections opens');
|
||
let depth = 0;
|
||
let sectionsEnd = -1;
|
||
for (let i = users.indexOf('[', sectionsAt); i < users.length; i++) {
|
||
if (users[i] === '[') depth++;
|
||
else if (users[i] === ']') {
|
||
depth--;
|
||
if (depth === 0) {
|
||
sectionsEnd = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
assert.ok(sectionsEnd !== -1, 'unbalanced sections array');
|
||
assert.ok(composeAt < sectionsEnd, 'ticket section must be INSIDE the sections array');
|
||
const ticket = read('src/Admin/Schema/Users/SchemaUsersTicket.php');
|
||
assert.ok(ticket.includes("'key' => 'ticket'"), 'PHP section key missing');
|
||
assert.ok(ticket.includes("'option_key' => 'jio_settings'"), 'PHP option key missing');
|
||
});
|
||
|
||
test('TicketSettings falls back to schema defaults and clamps numerics', () => {
|
||
const settings = read('src/Ticket/TicketSettings.php');
|
||
|
||
// 只取方法体,避免 docblock 与注释里的同形文字参与断言。
|
||
const body = settings.slice(settings.indexOf('public static function all()'));
|
||
|
||
// 缺键回退:enabled 只在显式关闭时为 false(与 schema default true 对齐)。
|
||
assert.match(body, /!isset\(\$s\['ticket_s'\]\) \|\| !empty\(\$s\['ticket_s'\]\)/);
|
||
assert.match(body, /self::all\(\)\['ticket_num_prefix'\] \?\? 'T'/);
|
||
assert.match(body, /self::all\(\)\['ticket_notice_text'\] \?\? ''\)/);
|
||
assert.match(body, /self::all\(\)\['ticket_attachment_max'\] \?\? 5\)/);
|
||
assert.match(body, /self::all\(\)\['ticket_urge_interval'\] \?\? 15\)/);
|
||
|
||
// 数值夹取:附件上限 1..9,催单冷却 1..1440。
|
||
assert.match(body, /max\(1, min\(9, \(int\)/);
|
||
assert.match(body, /max\(1, min\(1440, \(int\)/);
|
||
|
||
// 字数上限:标题 10..200(VARCHAR(200)),描述 / 回复 100..16000(TEXT 65535 字节 / utf8mb4)。
|
||
assert.match(body, /max\(10, min\(200, \(int\) \(self::all\(\)\['ticket_title_max'\] \?\? 200\)\)\)/);
|
||
assert.match(body, /max\(100, min\(16000, \(int\) \(self::all\(\)\['ticket_content_max'\] \?\? 5000\)\)\)/);
|
||
assert.match(body, /max\(100, min\(16000, \(int\) \(self::all\(\)\['ticket_reply_max'\] \?\? 3000\)\)\)/);
|
||
|
||
// 公告文案走 kses(后台允许 HTML)。
|
||
assert.match(body, /wp_kses_post/);
|
||
// 分区名与 schema key 一致(常量在 all() 之前声明,故对整份文件断言)。
|
||
assert.match(settings, /const SECTION = 'ticket'/);
|
||
assert.match(body, /read_domain\('users', self::SECTION\)/);
|
||
});
|
||
|
||
test('ticket panel is registered across the user-center wiring', () => {
|
||
const center = read('src/User/Center.php');
|
||
assert.ok(center.includes("'tickets' => ["), 'panel definition missing');
|
||
assert.ok(center.includes("'label' => __('工单中心', 'jio')"), 'panel label missing');
|
||
assert.match(center, /'tickets'\s*=>\s*\[[\s\S]*?'priority' => 49/, 'panel priority should sit next to orders (48)');
|
||
// 开关关闭时整块移除。
|
||
assert.match(center, /\$ticket_on = class_exists\('\\Jio\\Ticket\\TicketSettings'\)/);
|
||
assert.match(center, /if \(!\$ticket_on\) \{\s*\n\s*\$order = array_values\(array_filter\(\$order, static fn\(string \$key\): bool => \$key !== 'tickets'\)\);/);
|
||
assert.match(center, /if \(\$key === 'tickets' && !\$ticket_on\) \{\s*\n\s*continue;/);
|
||
|
||
const sidebar = read('inc/helpers/user-center-renderers/sidebar-nav.php');
|
||
assert.ok(sidebar.includes("'label' => __('工单中心', 'jio')"), 'sidebar label missing');
|
||
assert.ok(sidebar.includes("'icon' => 'comment-color'"), 'sidebar icon should use the built-in comment sprite');
|
||
assert.ok(sidebar.includes("'tab' => 'tickets'"), 'sidebar tab key missing');
|
||
|
||
const helper = read('inc/helpers/jio-user-center.php');
|
||
assert.ok(helper.includes("'tickets'"), 'built_in_tabs missing tickets');
|
||
assert.match(helper, /'tickets'\s*=> 'tickets',/, 'view map missing tickets => tickets');
|
||
|
||
const renderer = read('inc/helpers/user-center-renderers/profile-tickets.php');
|
||
assert.ok(renderer.includes('data-jio-ticket-center'), 'panel root hook missing');
|
||
assert.ok(renderer.includes('data-jio-ticket-create'), 'create button hook missing');
|
||
assert.ok(renderer.includes("jio_render_captcha('ticket_create')"), 'create captcha mount missing');
|
||
assert.ok(renderer.includes("jio_render_captcha('ticket_reply')"), 'reply captcha mount missing');
|
||
});
|
||
|
||
test('ticket frontend assets are enqueued only when the panel exists', () => {
|
||
const scripts = read('src/Core/ScriptsPart1b.php');
|
||
assert.match(scripts, /\$has_tickets_panel = false;/, 'panel flag not initialised');
|
||
assert.match(scripts, /\$has_tickets_panel = isset\(\$dashboard_tabs\['tickets'\]\);/, 'panel flag not derived');
|
||
assert.ok(scripts.includes("'jio-tickets'"), 'ticket CSS handle missing');
|
||
assert.ok(scripts.includes("'jio-ticket-center'"), 'ticket JS handle missing');
|
||
assert.ok(scripts.includes('/assets/js/ticket-center.js'), 'ticket JS path missing');
|
||
assert.ok(scripts.includes('/assets/css/routes/tickets.css'), 'ticket CSS path missing');
|
||
// 两个入队都必须在 $has_tickets_panel 条件内。
|
||
const cssIndex = scripts.indexOf("'jio-tickets'");
|
||
const jsIndex = scripts.indexOf("'jio-ticket-center'");
|
||
assert.ok(scripts.lastIndexOf('if ($has_tickets_panel) {', cssIndex) > scripts.indexOf("'jio-orders'"),
|
||
'ticket CSS must be gated on its own panel flag');
|
||
assert.ok(jsIndex > cssIndex, 'ticket JS should be enqueued alongside its CSS');
|
||
|
||
const localize = read('src/Core/ScriptsPart1c.php');
|
||
assert.ok(localize.includes("'ticket' => (function () {"), 'localize key missing');
|
||
assert.ok(!localize.includes("'afterSale'"), 'stale afterSale localize must be gone');
|
||
});
|