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) {
|
||||
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')) {
|
||||
return { label: '失败', cls: 'badge-error' };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export default defineConfig(({ mode }) => {
|
||||
const proxyTarget = resolveDevProxyTarget(env);
|
||||
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 {
|
||||
root: 'src/web',
|
||||
plugins: [react(), tailwindcss()],
|
||||
@@ -16,6 +20,8 @@ export default defineConfig(({ mode }) => {
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
host: frontendHost,
|
||||
port: resolvedFrontendPort,
|
||||
proxy: {
|
||||
'^/api($|/)': {
|
||||
target: proxyTarget,
|
||||
|
||||
Reference in New Issue
Block a user