156 lines
9.1 KiB
JavaScript
156 lines
9.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
const [app, index, styles, taskService, server] = await Promise.all([
|
|
readFile(new URL('./app.js', import.meta.url), 'utf8'),
|
|
readFile(new URL('./index.html', import.meta.url), 'utf8'),
|
|
readFile(new URL('./styles.css', import.meta.url), 'utf8'),
|
|
readFile(new URL('../control-plane/src/task-service.ts', import.meta.url), 'utf8'),
|
|
readFile(new URL('../control-plane/src/server.ts', import.meta.url), 'utf8')
|
|
]);
|
|
|
|
test('dashboard summary cards are uniform display-only totals and leave filtering to the explicit controls', () => {
|
|
const summaryRenderer = app.slice(
|
|
app.indexOf('function renderOperationsDashboardSummary()'),
|
|
app.indexOf('function operationsDashboardRankScale')
|
|
);
|
|
assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/);
|
|
assert.doesNotMatch(app, /tone: 'primary'/);
|
|
assert.match(summaryRenderer, /const card = el\('article', `operations-dashboard-stat operations-dashboard-stat-\$\{item\.tone\}`\)/);
|
|
assert.doesNotMatch(summaryRenderer, /selectedStatus|isSelected|operationsStatus|aria-pressed/);
|
|
assert.doesNotMatch(app, /\$\('#operationsDashboardSummary'\)\?\.addEventListener\('click'/);
|
|
assert.match(index, /<option value="all">全部结果<\/option>/);
|
|
assert.match(app, /function resetOperationsDashboardFilters\(\)[\s\S]*?\$\('#operationsDashboardStatus'\)\.value = 'all';/);
|
|
assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/);
|
|
assert.doesNotMatch(styles, /\.operations-dashboard-stat\[data-operations-status\]/);
|
|
assert.doesNotMatch(styles, /\.operations-dashboard-stat\.is-active/);
|
|
assert.doesNotMatch(styles, /operations-dashboard-stat-primary/);
|
|
});
|
|
|
|
test('waiting-for-input work is folded into in-progress instead of exposing a follow-up state', () => {
|
|
assert.doesNotMatch(index, /<option value="attention">/);
|
|
assert.doesNotMatch(app, /label: '待跟进'/);
|
|
assert.match(app, /outcomeKind === 'attention'\) return '进行中'/);
|
|
assert.match(app, /Number\(summary\.active \|\| 0\) \+ Number\(summary\.attention \|\| 0\)/);
|
|
const activeSql = taskService.slice(
|
|
taskService.indexOf('const OPERATIONS_ACTIVE_SQL'),
|
|
taskService.indexOf('const OPERATIONS_COMPLETED_STATUSES')
|
|
);
|
|
assert.doesNotMatch(activeSql, /OPERATIONS_ATTENTION_SQL/);
|
|
});
|
|
|
|
test('dashboard route has a focused leadership header and phone-safe navigation shell', () => {
|
|
assert.match(index, /content="width=device-width, initial-scale=1, viewport-fit=cover"/);
|
|
assert.match(index, /class="operations-dashboard-page-head"[\s\S]*?<h1>平台运行看板<\/h1>/);
|
|
assert.match(index, /class="operations-dashboard-back-link" href="\/">返回操作台<\/a>/);
|
|
assert.match(
|
|
styles,
|
|
/body\.operations-dashboard-page-active \.top-nav,[\s\S]*?#statusDetailsPopover\s*{[\s\S]*?display: none !important;/
|
|
);
|
|
assert.match(
|
|
styles,
|
|
/@media \(max-width: 920px\)[\s\S]*?body\.operations-dashboard-page-active \.app-shell\s*{[\s\S]*?width: 100%;/
|
|
);
|
|
});
|
|
|
|
test('phone dashboard turns metrics and task rows into touch-friendly cards', () => {
|
|
assert.match(
|
|
styles,
|
|
/\.operations-dashboard-summary\s*{[\s\S]*?display: flex;[\s\S]*?overflow-x: auto;[\s\S]*?scroll-snap-type: x proximity;/
|
|
);
|
|
assert.match(styles, /\.operations-dashboard-business-row,[\s\S]*?min-height: 48px;/);
|
|
assert.match(styles, /\.operations-dashboard-table thead\s*{\s*display: none;/);
|
|
assert.match(styles, /content: attr\(data-label\);/);
|
|
assert.match(styles, /\.operations-dashboard-detail\s*{[\s\S]*?env\(safe-area-inset-top\)/);
|
|
for (const label of ['操作时间', '员工名称', '任务类型', '原始输入', '最终输出', '任务结果']) {
|
|
assert.match(app, new RegExp(`dataset\\.label = '${label}'`));
|
|
}
|
|
assert.match(app, /document\.body\.classList\.add\('operations-dashboard-detail-open'\)/);
|
|
assert.match(app, /document\.body\.classList\.remove\('operations-dashboard-detail-open'\)/);
|
|
});
|
|
|
|
test('table filters use a bounded single-connection read and default to 20 rows', () => {
|
|
const dashboardQuery = taskService.slice(
|
|
taskService.indexOf('async listOperationsDashboard('),
|
|
taskService.indexOf('async getOperationsDashboardTask(')
|
|
);
|
|
const dashboardRoute = server.slice(
|
|
server.indexOf("app.get('/api/operations-dashboard'"),
|
|
server.indexOf("app.get('/api/operations-dashboard/tasks/:taskId'")
|
|
);
|
|
assert.match(dashboardQuery, /businessRouteId === 'unclassified'[\s\S]*?t\.business_route_id IS NULL/);
|
|
assert.match(dashboardQuery, /t\.assigned_user_id IS NOT NULL[\s\S]*?t\.source IN \('manual', 'agentbus'\)/);
|
|
assert.match(dashboardQuery, /actorUserId[\s\S]*?t\.assigned_user_id = \$\$\{params\.length\}/);
|
|
assert.match(dashboardQuery, /LEFT JOIN users assignee ON assignee\.id = t\.assigned_user_id/);
|
|
assert.match(dashboardQuery, /t\.business_route_id = \$\$\{params\.length\} OR t\.business_route_id IS NULL/);
|
|
assert.match(dashboardQuery, /const taskSearchSelect = `[\s\S]*?t\.success_receipt = '\{\}'::jsonb[\s\S]*?END AS execution_result/);
|
|
assert.match(dashboardQuery, /withTransaction\(this\.config/);
|
|
assert.match(dashboardQuery, /SET TRANSACTION READ ONLY/);
|
|
assert.match(dashboardQuery, /SET LOCAL statement_timeout/);
|
|
assert.match(dashboardQuery, /SELECT \$\{search \? taskSearchSelect : taskCandidateSelect\}/);
|
|
assert.match(dashboardQuery, /CASE WHEN t\.business_route_id IS NULL THEN t\.original_text_ciphertext ELSE NULL END/);
|
|
const baseSelect = dashboardQuery.slice(
|
|
dashboardQuery.indexOf('const taskBaseSelect'),
|
|
dashboardQuery.indexOf('const taskLegacyOperationSelect')
|
|
);
|
|
const legacyOperationSelect = dashboardQuery.slice(
|
|
dashboardQuery.indexOf('const taskLegacyOperationSelect'),
|
|
dashboardQuery.indexOf('const taskDetailSelect')
|
|
);
|
|
const detailSelect = dashboardQuery.slice(
|
|
dashboardQuery.indexOf('const taskDetailSelect'),
|
|
dashboardQuery.indexOf('const taskSearchSelect')
|
|
);
|
|
const searchSelect = dashboardQuery.slice(
|
|
dashboardQuery.indexOf('const taskSearchSelect'),
|
|
dashboardQuery.indexOf('const taskCandidateSelect')
|
|
);
|
|
const candidateSelect = dashboardQuery.slice(
|
|
dashboardQuery.indexOf('const taskCandidateSelect'),
|
|
dashboardQuery.indexOf('try {', dashboardQuery.indexOf('const taskCandidateSelect'))
|
|
);
|
|
assert.doesNotMatch(baseSelect, /t\.operation/);
|
|
assert.match(legacyOperationSelect, /'action', t\.operation -> 'action'/);
|
|
assert.match(legacyOperationSelect, /'kind', t\.operation #> '\{data,existing_refs,kind\}'/);
|
|
assert.match(legacyOperationSelect, /'mode', t\.operation #> '\{data,arrangement,mode\}'/);
|
|
assert.match(detailSelect, /t\.operation, t\.original_text_ciphertext/);
|
|
assert.match(searchSelect, /t\.operation, t\.original_text_ciphertext/);
|
|
assert.doesNotMatch(candidateSelect, /t\.operation/);
|
|
assert.doesNotMatch(candidateSelect, /taskLegacyOperationSelect/);
|
|
assert.match(dashboardQuery, /filter\(\(\{ projection \}\) => !projection\.task\.business_route_id\)/);
|
|
assert.match(dashboardQuery, /'legacy_classification'/);
|
|
assert.match(dashboardQuery, /SELECT t\.id, \$\{taskLegacyOperationSelect\}/);
|
|
assert.match(dashboardQuery, /legacyOperationById/);
|
|
assert.match(dashboardQuery, /'search_messages'/);
|
|
assert.match(dashboardQuery, /filter\(\(\{ directSearchMatch \}\) => !directSearchMatch\)/);
|
|
assert.doesNotMatch(dashboardQuery, /const pool = getPool\(this\.config\)/);
|
|
assert.doesNotMatch(dashboardQuery, /Promise\.all/);
|
|
assert.doesNotMatch(dashboardQuery, /loadSearchRows|classificationResult/);
|
|
assert.match(dashboardQuery, /if \(pageMatches\.length\)[\s\S]*?loadDetailedRows\(pageMatches\.map/);
|
|
assert.match(dashboardQuery, /options\.limit \|\| 20/);
|
|
assert.match(app, /const OPERATIONS_DASHBOARD_PAGE_SIZE = 20/);
|
|
assert.match(server, /operationsDashboardQuerySchema[\s\S]*?limit:[^\n]+default\(20\)/);
|
|
assert.match(dashboardRoute, /new AbortController\(\)/);
|
|
assert.match(dashboardRoute, /request\.raw\.once\('aborted', abortRequest\)/);
|
|
assert.match(dashboardRoute, /signal: controller\.signal/);
|
|
assert.match(dashboardRoute, /request\.raw\.off\('aborted', abortRequest\)/);
|
|
assert.match(dashboardQuery, /operations_dashboard_query_timeout/);
|
|
|
|
const dashboardDetail = taskService.slice(taskService.indexOf('async getOperationsDashboardTask('));
|
|
assert.match(dashboardDetail, /t\.assigned_user_id IS NOT NULL/);
|
|
assert.match(dashboardDetail, /t\.source IN \('manual', 'agentbus'\)/);
|
|
assert.doesNotMatch(dashboardDetail, /t\.source = 'manual'/);
|
|
|
|
const sync = app.slice(
|
|
app.indexOf('async function syncOperationsDashboard()'),
|
|
app.indexOf('async function loadOperationsDashboardTask(')
|
|
);
|
|
assert.match(sync, /filterForm\.setAttribute\('aria-busy', 'true'\)/);
|
|
assert.match(sync, /queryButton\.disabled = true/);
|
|
assert.match(sync, /filterForm\.removeAttribute\('aria-busy'\)/);
|
|
assert.match(sync, /queryButton\.disabled = false/);
|
|
assert.match(app, /看板请求超时,请稍后重试;若正在筛选,请缩短日期范围或增加筛选条件/);
|
|
assert.match(app, /error\?\.errorCode === 'operations_dashboard_query_timeout'/);
|
|
});
|