- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP color sanitizer; replace sanitize_hex_color calls and per-class regexes (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService) - SchemaProvider / ControllerBase sanitize `color` fields as colors; alpha kept only when the schema sets showAlpha - Split SchemaProvider OAuth validation and option matching into sibling traits to stay under the 1000-line limit - Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField, FloatBtnCommonFields and TicketStatusPanel use it - Widget background color input now uses wpColorPicker - Remove dead AksCompositeField dimensions/spacing/background branches and the unused Gutenberg ColorPicker var - Add color-unification contract test; update harness requires and tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
6.2 KiB
JavaScript
126 lines
6.2 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 工单表结构契约:
|
||
* 1) 6 张 aksrt_ticket* 表全部由 DBMigratorPart2 建出,且被 migrate_ticket_tables() 调用;
|
||
* 2) SCHEMA_VERSION 已升到 3.1.0(dbDelta 只在版本变化时重跑);
|
||
* 3) aksrt_order_aftersale 已从 schema 与表名映射中彻底移除;
|
||
* 4) 状态与部门 seeds 幂等,默认状态被标记为不可改删。
|
||
*/
|
||
|
||
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 TABLES = [
|
||
'aksrt_ticket',
|
||
'aksrt_ticket_reply',
|
||
'aksrt_ticket_status',
|
||
'aksrt_ticket_type',
|
||
'aksrt_ticket_prereply',
|
||
'aksrt_ticket_note',
|
||
];
|
||
|
||
test('schema version is bumped so dbDelta re-runs', () => {
|
||
const migrator = read('src/Core/DBMigrator.php');
|
||
assert.match(migrator, /const SCHEMA_VERSION = '3\.1\.0';/, 'SCHEMA_VERSION must be 3.1.0');
|
||
assert.ok(migrator.includes('use DBMigratorTicket;'), 'DBMigratorTicket trait not composed');
|
||
// 版本注释要说明每次升版做了什么,否则后人看不懂升版原因。
|
||
assert.match(migrator, /3\.0\.0: 售后(order_aftersale)整体替换为工单系统/);
|
||
assert.match(migrator, /3\.1\.0: aksrt_ticket 新增 urge_count \/ last_urge_time/);
|
||
const ticket = read('src/Core/DBMigratorTicket.php');
|
||
assert.match(ticket, /urge_count INT UNSIGNED NOT NULL DEFAULT 0,/);
|
||
assert.match(ticket, /last_urge_time DATETIME DEFAULT NULL,/);
|
||
});
|
||
|
||
test('order_aftersale is gone from schema and the table map', () => {
|
||
const part2 = read('src/Core/DBMigratorPart2.php');
|
||
const part3 = read('src/Core/DBMigratorPart3.php');
|
||
|
||
assert.ok(!part2.includes('aksrt_order_aftersale'), 'order_aftersale CREATE must be removed');
|
||
assert.ok(!part3.includes("'order_aftersale'"), 'order_aftersale table map entry must be removed');
|
||
assert.ok(!part3.includes('aksrt_order_aftersale'), 'order_aftersale table name must be removed');
|
||
});
|
||
|
||
test('six ticket tables are declared and the migrator is invoked', () => {
|
||
const part2 = read('src/Core/DBMigratorPart2.php');
|
||
// Part2 只负责调用;DDL 集中在 DBMigratorTicket,避免 run() 继续膨胀。
|
||
assert.ok(part2.includes('self::migrate_ticket_tables($prefix, $charset_collate);'),
|
||
'migrate_ticket_tables() must be called from run()');
|
||
assert.ok(!part2.includes('aksrt_order_aftersale'), 'Part2 must not carry the old table');
|
||
|
||
const ticket = read('src/Core/DBMigratorTicket.php');
|
||
for (const table of TABLES) {
|
||
assert.ok(ticket.includes(`CREATE TABLE {$prefix}${table} (`), `${table} DDL missing in trait`);
|
||
}
|
||
assert.ok(ticket.includes('self::seed_ticket_statuses($prefix);'), 'status seeds not invoked');
|
||
assert.ok(ticket.includes('self::seed_ticket_types($prefix);'), 'type seeds not invoked');
|
||
});
|
||
|
||
test('ticket table map exposes all six names', () => {
|
||
const part3 = read('src/Core/DBMigratorPart3.php');
|
||
for (const name of ['ticket', 'ticket_reply', 'ticket_status', 'ticket_type', 'ticket_prereply', 'ticket_note']) {
|
||
assert.ok(part3.includes(`'${name}'`), `table map missing ${name}`);
|
||
assert.ok(part3.includes(`'aksrt_${name}'`), `table map missing aksrt_${name}`);
|
||
}
|
||
});
|
||
|
||
test('ticket columns cover the state machine and optional order link', () => {
|
||
const ticket = read('src/Core/DBMigratorTicket.php');
|
||
|
||
// 工单号对外唯一;状态是可自定义的 key;订单关联可空(默认空串)。
|
||
assert.match(ticket, /UNIQUE KEY uniq_ticket_num \(ticket_num\)/);
|
||
assert.match(ticket, /status VARCHAR\(24\) NOT NULL DEFAULT 'pending'/);
|
||
assert.match(ticket, /order_number VARCHAR\(32\) NOT NULL DEFAULT ''/);
|
||
assert.match(ticket, /KEY idx_order \(order_number\)/);
|
||
|
||
// 沟通记录独立成行,支持引用回复。
|
||
assert.match(ticket, /author_type VARCHAR\(16\) NOT NULL DEFAULT 'user'/);
|
||
assert.match(ticket, /quote_reply_id BIGINT UNSIGNED NOT NULL DEFAULT 0/);
|
||
|
||
// 状态表带完结/默认两个开关,默认状态不可改删由服务层兜住。
|
||
assert.match(ticket, /status_key VARCHAR\(24\) NOT NULL/);
|
||
assert.match(ticket, /UNIQUE KEY uniq_status_key \(status_key\)/);
|
||
assert.match(ticket, /is_closed TINYINT\(1\) NOT NULL DEFAULT 0/);
|
||
assert.match(ticket, /is_default TINYINT\(1\) NOT NULL DEFAULT 0/);
|
||
});
|
||
|
||
test('status and department seeds are idempotent', () => {
|
||
const ticket = read('src/Core/DBMigratorTicket.php');
|
||
|
||
// 按 key / name 查重后跳过,重复执行不会写重复行。
|
||
assert.match(ticket, /\$exists = \$wpdb->get_col\("SELECT status_key FROM \{\$table\}"\)/);
|
||
assert.match(ticket, /if \(in_array\(\$key, \$exists, true\)\) \{\s*\n\s*continue;/);
|
||
assert.match(ticket, /\$exists = \$wpdb->get_col\("SELECT name FROM \{\$table\}"\)/);
|
||
assert.match(ticket, /if \(in_array\(\$label, \$exists, true\)\) \{\s*\n\s*continue;/);
|
||
|
||
// 五个内置状态与五个部门。
|
||
for (const key of ['pending', 'handling', 'user_replied', 'admin_replied', 'closed']) {
|
||
assert.ok(ticket.includes(`['${key}',`), `status seed ${key} missing`);
|
||
}
|
||
for (const label of ['技术支持', '账号问题', '财务问题', '产品建议', '其他']) {
|
||
assert.ok(ticket.includes(`'${label}'`), `department seed ${label} missing`);
|
||
}
|
||
// 默认状态是「待接单」,且是完结态的补集。
|
||
assert.match(ticket, /\['pending', '待接单', '#f59e0b', 0, 1\]/);
|
||
assert.match(ticket, /\['closed', '已关闭', '#6b7280', 1, 0\]/);
|
||
});
|
||
|
||
test('TicketStatusService seeds mirror the migrator and guard the default row', () => {
|
||
const service = read('src/Ticket/TicketStatusService.php');
|
||
for (const key of ['pending', 'handling', 'user_replied', 'admin_replied', 'closed']) {
|
||
assert.ok(service.includes(`KEY_${key.toUpperCase()}`), `KEY_${key.toUpperCase()} missing`);
|
||
}
|
||
// 状态机只挡确定非法的组合,其余交给「目标状态必须存在」。
|
||
assert.match(service, /if \(\$from === self::KEY_PENDING && \$to === self::KEY_CLOSED\) \{\s*\n\s*return false;/);
|
||
assert.match(service, /if \(self::is_closed\(\$from\)\) \{\s*\n\s*return false;/);
|
||
// 颜色只接受 #rgb / #rrggbb。
|
||
assert.match(service, /'#6b7280'/);
|
||
assert.match(service, /\\AKSRT\\Core\\Color::sanitize\(\$color\)/);
|
||
assert.doesNotMatch(service, /preg_match\('\/\^#/);
|
||
});
|