feat(ProgramLogs): add event status summary parsing and corresponding labels
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { act, create, type ReactTestInstance } from 'react-test-renderer';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { ToastProvider } from '../components/Toast.js';
|
||||||
|
import ProgramLogs from './ProgramLogs.js';
|
||||||
|
|
||||||
|
const { apiMock } = vi.hoisted(() => ({
|
||||||
|
apiMock: {
|
||||||
|
getEvents: vi.fn(),
|
||||||
|
markEventRead: vi.fn(),
|
||||||
|
markAllEventsRead: vi.fn(),
|
||||||
|
clearEvents: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../api.js', () => ({
|
||||||
|
api: apiMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function collectText(node: ReactTestInstance): string {
|
||||||
|
const children = node.children || [];
|
||||||
|
return children.map((child) => {
|
||||||
|
if (typeof child === 'string') return child;
|
||||||
|
return collectText(child);
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushMicrotasks() {
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ProgramLogs status label', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats summary with failed=0 as success', async () => {
|
||||||
|
apiMock.getEvents.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'status',
|
||||||
|
title: '同步全部账号令牌已完成(成功31/跳过0/失败0)',
|
||||||
|
message: '全部账号令牌同步完成:成功 31,跳过 0,失败 0',
|
||||||
|
level: 'info',
|
||||||
|
read: false,
|
||||||
|
createdAt: '2026-03-04T06:43:03.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let root: ReturnType<typeof create> | null = null;
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/events']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProgramLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const rows = root!.root.findAll((node) => node.type === 'tr');
|
||||||
|
const targetRow = rows.find((row) => collectText(row).includes('同步全部账号令牌已完成'));
|
||||||
|
expect(targetRow).toBeTruthy();
|
||||||
|
|
||||||
|
const tds = targetRow!.findAll((node) => node.type === 'td');
|
||||||
|
const statusCell = tds[5];
|
||||||
|
expect(collectText(statusCell).trim()).toBe('成功');
|
||||||
|
const statusBadge = statusCell.find((node) => node.type === 'span');
|
||||||
|
expect(String(statusBadge.props.className || '')).toContain('badge-success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats parenthesized counts with failed=0 as success', async () => {
|
||||||
|
apiMock.getEvents.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'status',
|
||||||
|
title: '同步全部账号令牌已完成',
|
||||||
|
message: '成功(15): a, b\n跳过(1): c\n失败(0): -',
|
||||||
|
level: 'info',
|
||||||
|
read: false,
|
||||||
|
createdAt: '2026-03-04T06:43:03.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let root: ReturnType<typeof create> | null = null;
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/events']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProgramLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const rows = root!.root.findAll((node) => node.type === 'tr');
|
||||||
|
const targetRow = rows.find((row) => collectText(row).includes('同步全部账号令牌已完成'));
|
||||||
|
expect(targetRow).toBeTruthy();
|
||||||
|
|
||||||
|
const tds = targetRow!.findAll((node) => node.type === 'td');
|
||||||
|
const statusCell = tds[5];
|
||||||
|
expect(collectText(statusCell).trim()).toBe('成功');
|
||||||
|
const statusBadge = statusCell.find((node) => node.type === 'span');
|
||||||
|
expect(String(statusBadge.props.className || '')).toContain('badge-success');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,6 +37,32 @@ function levelLabel(level: string) {
|
|||||||
function eventStatusLabel(row: ProgramEvent) {
|
function eventStatusLabel(row: ProgramEvent) {
|
||||||
const text = `${row.title || ''} ${row.message || ''}`.toLowerCase();
|
const text = `${row.title || ''} ${row.message || ''}`.toLowerCase();
|
||||||
|
|
||||||
|
const parseCount = (pattern: RegExp): number | undefined => {
|
||||||
|
const match = text.match(pattern);
|
||||||
|
if (!match?.[1]) return undefined;
|
||||||
|
const value = Number.parseInt(match[1], 10);
|
||||||
|
return Number.isFinite(value) ? value : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
success: parseCount(/成功[^\d]{0,6}(\d+)/i) ?? parseCount(/success(?:ful)?[^\d]{0,6}(\d+)/i),
|
||||||
|
skipped: parseCount(/跳过[^\d]{0,6}(\d+)/i) ?? parseCount(/skipped?[^\d]{0,6}(\d+)/i),
|
||||||
|
failed: parseCount(/失败[^\d]{0,6}(\d+)/i) ?? parseCount(/failed[^\d]{0,6}(\d+)/i),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (summary.failed !== undefined || summary.success !== undefined || summary.skipped !== undefined) {
|
||||||
|
if ((summary.failed ?? 0) > 0) {
|
||||||
|
return { label: '失败', cls: 'badge-error' };
|
||||||
|
}
|
||||||
|
if ((summary.success ?? 0) > 0) {
|
||||||
|
return { label: '成功', cls: 'badge-success' };
|
||||||
|
}
|
||||||
|
if ((summary.skipped ?? 0) > 0) {
|
||||||
|
return { label: '跳过', cls: 'badge-warning' };
|
||||||
|
}
|
||||||
|
return { label: '成功', cls: 'badge-success' };
|
||||||
|
}
|
||||||
|
|
||||||
if (text.includes('失败') || text.includes('failed') || text.includes('error')) {
|
if (text.includes('失败') || text.includes('failed') || text.includes('error')) {
|
||||||
return { label: '失败', cls: 'badge-error' };
|
return { label: '失败', cls: 'badge-error' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
const proxyTarget = resolveDevProxyTarget(env);
|
const proxyTarget = resolveDevProxyTarget(env);
|
||||||
console.log(`[vite] dev proxy target: ${proxyTarget}`);
|
console.log(`[vite] dev proxy target: ${proxyTarget}`);
|
||||||
|
|
||||||
|
const frontendPort = Number.parseInt(env.FRONTEND_PORT || env.VITE_FRONTEND_PORT || '', 10);
|
||||||
|
const resolvedFrontendPort = Number.isFinite(frontendPort) && frontendPort > 0 ? frontendPort : 5173;
|
||||||
|
const frontendHost = (env.VITE_DEV_HOST || '127.0.0.1').trim() || '127.0.0.1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: 'src/web',
|
root: 'src/web',
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
@@ -16,6 +20,8 @@ export default defineConfig(({ mode }) => {
|
|||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
host: frontendHost,
|
||||||
|
port: resolvedFrontendPort,
|
||||||
proxy: {
|
proxy: {
|
||||||
'^/api($|/)': {
|
'^/api($|/)': {
|
||||||
target: proxyTarget,
|
target: proxyTarget,
|
||||||
|
|||||||
Reference in New Issue
Block a user