feat: add report analysis and visualization feature
- Add recharts library for data visualization - Implement ReportAnalyzer service to parse report data - Add report analysis IPC handler with Admin permission check - Add ReportAnalysisDialog component with charts - Add unit and E2E tests for report analyzer - Update Playwright config to exclude vitest-specific test files - Expand vitest config to include source code tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
313
tests/unit/report-analyzer.test.ts
Normal file
313
tests/unit/report-analyzer.test.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ReportAnalyzer } from '../../src/main/services/report/report-analyzer'
|
||||
|
||||
describe('ReportAnalyzer', () => {
|
||||
const analyzer = new ReportAnalyzer()
|
||||
|
||||
describe('parseMarkdownReport', () => {
|
||||
it('should parse standard report with all fields', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`admin\` |
|
||||
| **处理订单数** | \`150\` |
|
||||
| **删除物料数** | \`45\` |
|
||||
| **跳过物料数** | \`30\` |
|
||||
| **错误数量** | \`2\` |
|
||||
| **重试订单数** | \`10\` |
|
||||
| **成功重试数** | \`8\` |
|
||||
| **执行耗时** | \`2 分 30 秒\` |
|
||||
| **执行时间** | \`2026-03-25 08:30:45\` |
|
||||
`
|
||||
|
||||
const result = analyzer.parseMarkdownReport(content)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.date).toBe('2026-03-25')
|
||||
expect(result!.username).toBe('admin')
|
||||
expect(result!.ordersProcessed).toBe(150)
|
||||
expect(result!.materialsDeleted).toBe(45)
|
||||
expect(result!.materialsSkipped).toBe(30)
|
||||
expect(result!.errorCount).toBe(2)
|
||||
expect(result!.retriedOrders).toBe(10)
|
||||
expect(result!.successfulRetries).toBe(8)
|
||||
expect(result!.durationSeconds).toBe(150)
|
||||
})
|
||||
|
||||
it('should use defaults for missing fields', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`user1\` |
|
||||
| **处理订单数** | \`50\` |
|
||||
`
|
||||
|
||||
const result = analyzer.parseMarkdownReport(content)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.username).toBe('user1')
|
||||
expect(result!.ordersProcessed).toBe(50)
|
||||
expect(result!.materialsDeleted).toBe(0)
|
||||
expect(result!.materialsSkipped).toBe(0)
|
||||
expect(result!.errorCount).toBe(0)
|
||||
expect(result!.retriedOrders).toBe(0)
|
||||
expect(result!.successfulRetries).toBe(0)
|
||||
expect(result!.durationSeconds).toBe(0)
|
||||
})
|
||||
|
||||
it('should return null for malformed format', () => {
|
||||
const content = `This is not a valid report format
|
||||
Just some random text without proper structure
|
||||
No execution summary section here`
|
||||
|
||||
const result = analyzer.parseMarkdownReport(content)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for empty content', () => {
|
||||
expect(analyzer.parseMarkdownReport('')).toBeNull()
|
||||
expect(analyzer.parseMarkdownReport(' ')).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle special characters in fields', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`admin@test.com\` |
|
||||
| **处理订单数** | \`100\` |
|
||||
| **删除物料数** | \`25\` |
|
||||
| **跳过物料数** | \`10\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`5\` |
|
||||
| **成功重试数** | \`5\` |
|
||||
| **执行耗时** | \`1 分\` |
|
||||
| **执行时间** | \`2026-03-25 10:00:00\` |
|
||||
`
|
||||
|
||||
const result = analyzer.parseMarkdownReport(content)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.username).toBe('admin@test.com')
|
||||
expect(result!.durationSeconds).toBe(60)
|
||||
})
|
||||
|
||||
it('should extract date from content when execution time missing', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`20\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`30 秒\` |
|
||||
`
|
||||
|
||||
const result = analyzer.parseMarkdownReport(content)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.date).toMatch(/^\d{4}-\d{2}-\d{2}$/)
|
||||
})
|
||||
|
||||
it('should handle various duration formats', () => {
|
||||
// Test "2 分 30 秒"
|
||||
const content1 = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`10\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`2 分 30 秒\` |
|
||||
| **执行时间** | \`2026-03-25 10:00:00\` |
|
||||
`
|
||||
const result1 = analyzer.parseMarkdownReport(content1)
|
||||
expect(result1!.durationSeconds).toBe(150)
|
||||
|
||||
// Test "1 分"
|
||||
const content2 = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`10\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`1 分\` |
|
||||
| **执行时间** | \`2026-03-25 10:00:00\` |
|
||||
`
|
||||
const result2 = analyzer.parseMarkdownReport(content2)
|
||||
expect(result2!.durationSeconds).toBe(60)
|
||||
|
||||
// Test "30 秒" - use different time to avoid confusion
|
||||
const content3 = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`10\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`30 秒\` |
|
||||
| **执行时间** | \`2026-03-25 14:20:15\` |
|
||||
`
|
||||
const result3 = analyzer.parseMarkdownReport(content3)
|
||||
console.log(
|
||||
'Duration test - extracted duration:',
|
||||
result3?.durationSeconds,
|
||||
'content:',
|
||||
content3
|
||||
)
|
||||
expect(result3!.durationSeconds).toBe(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe('analyzeReports', () => {
|
||||
it('should batch process multiple reports', () => {
|
||||
const reports = [
|
||||
{
|
||||
content: `## 执行摘要
|
||||
|
||||
| **操作用户** | \`user1\` |
|
||||
| **处理订单数** | \`100\` |
|
||||
| **删除物料数** | \`20\` |
|
||||
| **跳过物料数** | \`10\` |
|
||||
| **错误数量** | \`1\` |
|
||||
| **重试订单数** | \`5\` |
|
||||
| **成功重试数** | \`4\` |
|
||||
| **执行耗时** | \`1 分\` |
|
||||
| **执行时间** | \`2026-03-25 08:00:00\` |
|
||||
`,
|
||||
filename: 'report-2026-03-25.md'
|
||||
},
|
||||
{
|
||||
content: `## 执行摘要
|
||||
|
||||
| **操作用户** | \`user2\` |
|
||||
| **处理订单数** | \`200\` |
|
||||
| **删除物料数** | \`50\` |
|
||||
| **跳过物料数** | \`20\` |
|
||||
| **错误数量** | \`2\` |
|
||||
| **重试订单数** | \`10\` |
|
||||
| **成功重试数** | \`8\` |
|
||||
| **执行耗时** | \`2 分\` |
|
||||
| **执行时间** | \`2026-03-26 09:00:00\` |
|
||||
`,
|
||||
filename: 'report-2026-03-26.md'
|
||||
}
|
||||
]
|
||||
|
||||
const results = analyzer.analyzeReports(reports)
|
||||
|
||||
expect(results.length).toBe(2)
|
||||
expect(results[0].username).toBe('user1')
|
||||
expect(results[0].date).toBe('2026-03-25')
|
||||
expect(results[1].username).toBe('user2')
|
||||
expect(results[1].date).toBe('2026-03-26')
|
||||
})
|
||||
|
||||
it('should skip invalid reports in batch', () => {
|
||||
const reports = [
|
||||
{
|
||||
content: `## 执行摘要
|
||||
|
||||
| **操作用户** | \`valid\` |
|
||||
| **处理订单数** | \`50\` |
|
||||
| **删除物料数** | \`10\` |
|
||||
| **跳过物料数** | \`5\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`30 秒\` |
|
||||
| **执行时间** | \`2026-03-25 10:00:00\` |
|
||||
`,
|
||||
filename: 'valid-report.md'
|
||||
},
|
||||
{
|
||||
content: 'Invalid report content',
|
||||
filename: 'invalid-report.md'
|
||||
}
|
||||
]
|
||||
|
||||
const results = analyzer.analyzeReports(reports)
|
||||
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].username).toBe('valid')
|
||||
})
|
||||
|
||||
it('should use filename date to override content date', () => {
|
||||
const reports = [
|
||||
{
|
||||
content: `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`30\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`20 秒\` |
|
||||
| **执行时间** | \`2026-03-20 10:00:00\` |
|
||||
`,
|
||||
filename: 'report-2026-03-25-08-30-45.md'
|
||||
}
|
||||
]
|
||||
|
||||
const results = analyzer.analyzeReports(reports)
|
||||
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].date).toBe('2026-03-25')
|
||||
})
|
||||
})
|
||||
|
||||
describe('date extraction', () => {
|
||||
it('should extract date from filename with ISO format', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`10\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`10 秒\` |
|
||||
| **执行时间** | \`2026-03-20 10:00:00\` |
|
||||
`
|
||||
|
||||
const report = {
|
||||
content,
|
||||
filename: 'cleaner-report-2026-03-25-08-30-45.md'
|
||||
}
|
||||
|
||||
const results = analyzer.analyzeReports([report])
|
||||
expect(results[0].date).toBe('2026-03-25')
|
||||
})
|
||||
|
||||
it('should extract date from filename with YYYYMMDD format', () => {
|
||||
const content = `## 执行摘要
|
||||
|
||||
| **操作用户** | \`test\` |
|
||||
| **处理订单数** | \`10\` |
|
||||
| **删除物料数** | \`5\` |
|
||||
| **跳过物料数** | \`2\` |
|
||||
| **错误数量** | \`0\` |
|
||||
| **重试订单数** | \`0\` |
|
||||
| **成功重试数** | \`0\` |
|
||||
| **执行耗时** | \`10 秒\` |
|
||||
| **执行时间** | \`2026-03-20 10:00:00\` |
|
||||
`
|
||||
|
||||
const report = {
|
||||
content,
|
||||
filename: 'report-20260325-100000.md'
|
||||
}
|
||||
|
||||
const results = analyzer.analyzeReports([report])
|
||||
expect(results[0].date).toBe('2026-03-25')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user