'use strict'; /** * SPA「工单中心」接线契约: * 1) /users/ticket 是 customPage,组件 UsersTicket 已注册并能被 router 解析; * 2) 页面内嵌 SchemaPage 承载 users/ticket 分区,schema 表单零改动复用; * 3) 三块字典 CRUD 走 TicketRestAdmin(jio/v1/admin)的封装模块; * 4) 双向深链:SPA → 原生工单列表,原生 → SPA 工单中心; * 5) 配置 Tab 的残留(tab=config / UsersMenusTicketsConfig)不得回流。 */ 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('the ticket page is a custom page backed by UsersTicket', () => { const loaders = read('assets/admin/src/router/component-loaders/users.js'); assert.ok( loaders.includes("UsersTicket: () => import('@/pages/users/UsersTicket.vue')"), 'UsersTicket loader missing' ); const constants = read('assets/admin/src/constants/sections/users.js'); // 入口必须是 customPage 且显式指定组件,否则 router 会回落到 SchemaPage。 assert.match(constants, /customPage\(\{[\s\S]*?component: 'UsersTicket'/); 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'); }); test('UsersTicket embeds the schema page and the three CRUD panels', () => { const page = read('assets/admin/src/pages/users/UsersTicket.vue'); // 基础配置复用 SchemaPage,域/分区显式传入,避免依赖路由 meta。 assert.match(page, / { const module = read('assets/admin/src/api/modules/tickets.js'); // TicketRestAdmin 挂在 jio/v1/admin,客户端 baseUrl 只到 jio/v1, // 因此必须走 /jio/ 全路径形式。 assert.match(module, /'\/jio\/v1\/admin\/tickets'/); for (const method of [ 'listTicketStatuses', 'createTicketStatus', 'updateTicketStatus', 'deleteTicketStatus', 'listTicketTypes', 'createTicketType', 'updateTicketType', 'deleteTicketType', 'listTicketPrereplies', 'createTicketPrereply', 'updateTicketPrereply', 'deleteTicketPrereply', 'reorderTicketPrereplies', ]) { assert.ok(module.includes(`${method}(`), `api method ${method} missing`); } const index = read('assets/admin/src/api/modules/index.js'); assert.ok(index.includes("import { ticketsApiMethods } from './tickets.js'"), 'module not imported'); assert.ok(index.includes('...ticketsApiMethods,'), 'module not spread into apiMethods'); }); test('panels enforce the backend guards instead of trusting the client', () => { const status = read('assets/admin/src/pages/users/partials/ticket/TicketStatusPanel.vue'); // 默认状态的编辑/删除入口在前端禁用,但后端 TicketConfigRest 仍有 409 兜底。 assert.match(status, /disabled: row\.is_default/); // 颜色必须落在 TicketStatusService::sanitize_color() 接受的范围内。 assert.match(status, /import \{ toHex \} from '@\/utils\/color\.js'/); assert.match(status, / row\.id\)/); assert.match(prereply, /reorderTicketPrereplies\(ids\)/); }); test('no config tab residue survives in the SPA or the native page', () => { const router = read('assets/admin/src/router/index.js'); const constants = read('assets/admin/src/constants/sections/users.js'); const schema = read('assets/admin/src/schema/sections/users/ticket.js'); for (const [file, source] of [['router', router], ['constants', constants], ['ticket schema', schema]]) { assert.ok(!source.includes('tab=config'), `${file} still references tab=config`); } // 原生页不再有配置 Tab 的渲染 trait。 assert.ok( !fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTicketsConfig.php')), 'native config trait must be deleted' ); });