Add unit tests for core ERP service modules including ErpBrowserManager, cleaner, erp-auth, extractor-core, extractor, and order-resolver. Also includes test coverage improvement plan and quality review report. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { ExtractorCore } from '../../../../src/main/services/erp/extractor-core'
|
|
import type { ErpSession } from '../../../../src/main/types/erp.types'
|
|
import type { ExtractorCoreInput } from '../../../../src/main/types/extractor.types'
|
|
|
|
// Mock playwright
|
|
vi.mock('playwright', () => ({
|
|
chromium: {
|
|
launch: vi.fn(),
|
|
connect: vi.fn()
|
|
}
|
|
}))
|
|
|
|
// Mock logger
|
|
vi.mock('../../../../src/main/services/logger', () => ({
|
|
createLogger: vi.fn(() => ({
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn()
|
|
}))
|
|
}))
|
|
|
|
describe('ExtractorCore', () => {
|
|
let extractorCore: ExtractorCore
|
|
let mockSession: ErpSession
|
|
let mockPage: any
|
|
let mockMainFrame: any
|
|
let mockPopupPage: any
|
|
let mockWorkFrame: any
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
// Create mock session
|
|
mockWorkFrame = {
|
|
locator: vi.fn().mockImplementation(() => mockWorkFrame),
|
|
filter: vi.fn().mockImplementation(() => mockWorkFrame),
|
|
nth: vi.fn().mockImplementation(() => mockWorkFrame),
|
|
getByRole: vi.fn().mockReturnThis(),
|
|
getByText: vi.fn().mockReturnThis(),
|
|
getByName: vi.fn().mockReturnThis(),
|
|
fill: vi.fn().mockResolvedValue(undefined),
|
|
press: vi.fn().mockResolvedValue(undefined),
|
|
click: vi.fn().mockResolvedValue(undefined),
|
|
hover: vi.fn().mockResolvedValue(undefined),
|
|
waitFor: vi.fn().mockResolvedValue(undefined),
|
|
contentFrame: vi.fn().mockResolvedValue(null)
|
|
}
|
|
|
|
mockPopupPage = {
|
|
locator: vi.fn().mockReturnThis(),
|
|
waitForEvent: vi.fn().mockResolvedValue(undefined),
|
|
contentFrame: vi.fn().mockResolvedValue(null)
|
|
}
|
|
|
|
mockMainFrame = {
|
|
locator: vi.fn().mockReturnThis(),
|
|
getByTitle: vi.fn().mockReturnThis(),
|
|
first: vi.fn().mockReturnThis(),
|
|
click: vi.fn().mockResolvedValue(undefined),
|
|
contentFrame: vi.fn().mockResolvedValue(null)
|
|
}
|
|
|
|
mockPage = {
|
|
waitForEvent: vi.fn().mockResolvedValue(mockPopupPage)
|
|
}
|
|
|
|
mockSession = {
|
|
page: mockPage,
|
|
mainFrame: mockMainFrame
|
|
} as unknown as ErpSession
|
|
|
|
extractorCore = new ExtractorCore()
|
|
})
|
|
|
|
describe('waitForLoading()', () => {
|
|
let mockLoadingLocator: any
|
|
|
|
beforeEach(() => {
|
|
mockLoadingLocator = {
|
|
waitFor: vi.fn().mockResolvedValue(undefined)
|
|
}
|
|
|
|
// Mock the locator chain: workFrame.locator().filter().nth()
|
|
mockWorkFrame.locator.mockReturnValue(mockWorkFrame)
|
|
mockWorkFrame.filter.mockReturnValue(mockWorkFrame)
|
|
mockWorkFrame.nth.mockReturnValue(mockLoadingLocator)
|
|
})
|
|
|
|
it('should wait for loading to appear and disappear', async () => {
|
|
// @ts-ignore - accessing private method for testing
|
|
await extractorCore.waitForLoading(mockWorkFrame)
|
|
|
|
expect(mockLoadingLocator.waitFor).toHaveBeenCalledWith({
|
|
state: 'visible',
|
|
timeout: 3000
|
|
})
|
|
expect(mockLoadingLocator.waitFor).toHaveBeenCalledWith({
|
|
state: 'hidden',
|
|
timeout: 0
|
|
})
|
|
})
|
|
|
|
it('should handle loading that never appears (timeout)', async () => {
|
|
mockLoadingLocator.waitFor.mockResolvedValueOnce(undefined).mockResolvedValue(undefined)
|
|
|
|
// @ts-ignore - accessing private method for testing
|
|
await expect(extractorCore.waitForLoading(mockWorkFrame)).resolves.not.toThrow()
|
|
})
|
|
|
|
it('should handle loading that completes quickly', async () => {
|
|
mockLoadingLocator.waitFor.mockRejectedValueOnce(new Error('Already hidden'))
|
|
|
|
// @ts-ignore - accessing private method for testing
|
|
await expect(extractorCore.waitForLoading(mockWorkFrame)).resolves.not.toThrow()
|
|
})
|
|
|
|
it('should use correct loading text from locators', async () => {
|
|
// @ts-ignore - accessing private method for testing
|
|
await extractorCore.waitForLoading(mockWorkFrame)
|
|
|
|
expect(mockWorkFrame.locator).toHaveBeenCalledWith('div')
|
|
expect(mockWorkFrame.filter).toHaveBeenCalled()
|
|
expect(mockLoadingLocator.waitFor).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|
|
|
|
describe('downloadAllBatches()', () => {
|
|
it('should process all batches with progress updates', async () => {
|
|
const orderNumbers = ['ORD001', 'ORD002', 'ORD003', 'ORD004']
|
|
const batchSize = 2
|
|
const progressCallback = vi.fn()
|
|
|
|
const mockDownloadPath = '/path/to/downloaded/file.xlsx'
|
|
|
|
// Mock internal methods to avoid complex iframe/locator mocking
|
|
vi.spyOn(extractorCore as any, 'navigateToExtractorPage').mockResolvedValue({
|
|
popupPage: mockPopupPage,
|
|
workFrame: mockWorkFrame
|
|
})
|
|
|
|
vi.spyOn(extractorCore as any, 'downloadBatch').mockResolvedValue(mockDownloadPath)
|
|
|
|
const input: ExtractorCoreInput = {
|
|
session: mockSession,
|
|
orderNumbers,
|
|
downloadDir: '/test/downloads',
|
|
batchSize,
|
|
onProgress: progressCallback
|
|
}
|
|
|
|
const result = await extractorCore.downloadAllBatches(input)
|
|
|
|
expect(result.downloadedFiles).toEqual([mockDownloadPath, mockDownloadPath])
|
|
expect(result.errors).toHaveLength(0)
|
|
expect(progressCallback).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle errors in batch download gracefully', async () => {
|
|
const orderNumbers = ['ORD001', 'ORD002']
|
|
const batchSize = 1
|
|
const progressCallback = vi.fn()
|
|
|
|
vi.spyOn(extractorCore as any, 'navigateToExtractorPage').mockResolvedValue({
|
|
popupPage: mockPopupPage,
|
|
workFrame: mockWorkFrame
|
|
})
|
|
|
|
// First batch succeeds, second fails
|
|
vi.spyOn(extractorCore as any, 'downloadBatch')
|
|
.mockResolvedValueOnce('/path/file1.xlsx')
|
|
.mockRejectedValueOnce(new Error('Network error'))
|
|
|
|
const input: ExtractorCoreInput = {
|
|
session: mockSession,
|
|
orderNumbers,
|
|
downloadDir: '/test/downloads',
|
|
batchSize,
|
|
onProgress: progressCallback
|
|
}
|
|
|
|
const result = await extractorCore.downloadAllBatches(input)
|
|
|
|
expect(result.downloadedFiles).toEqual(['/path/file1.xlsx'])
|
|
expect(result.errors).toHaveLength(1)
|
|
expect(result.errors[0]).toContain('Batch 2')
|
|
expect(result.errors[0]).toContain('Network error')
|
|
})
|
|
|
|
it('should calculate progress correctly', async () => {
|
|
const orderNumbers = ['ORD001', 'ORD002', 'ORD003', 'ORD004']
|
|
const batchSize = 2
|
|
const progressCallback = vi.fn()
|
|
|
|
vi.spyOn(extractorCore as any, 'navigateToExtractorPage').mockResolvedValue({
|
|
popupPage: mockPopupPage,
|
|
workFrame: mockWorkFrame
|
|
})
|
|
|
|
vi.spyOn(extractorCore as any, 'downloadBatch').mockResolvedValue('/path/file.xlsx')
|
|
|
|
const input: ExtractorCoreInput = {
|
|
session: mockSession,
|
|
orderNumbers,
|
|
downloadDir: '/test/downloads',
|
|
batchSize,
|
|
onProgress: progressCallback
|
|
}
|
|
|
|
await extractorCore.downloadAllBatches(input)
|
|
|
|
// totalPoints = 1 + 2 batches + 2 = 5, progressPerPoint = 20
|
|
// Batch 1: progress = (1 + 1) * 20 = 40
|
|
// Batch 2: progress = (1 + 2) * 20 = 60
|
|
expect(progressCallback).toHaveBeenCalledTimes(2)
|
|
expect(progressCallback).toHaveBeenNthCalledWith(1, '处理批次 1/2', 40, {
|
|
phase: 'downloading',
|
|
currentBatch: 1,
|
|
totalBatches: 2
|
|
})
|
|
expect(progressCallback).toHaveBeenNthCalledWith(2, '处理批次 2/2', 60, {
|
|
phase: 'downloading',
|
|
currentBatch: 2,
|
|
totalBatches: 2
|
|
})
|
|
})
|
|
|
|
it('should work without progress callback', async () => {
|
|
const orderNumbers = ['ORD001']
|
|
const batchSize = 1
|
|
|
|
vi.spyOn(extractorCore as any, 'navigateToExtractorPage').mockResolvedValue({
|
|
popupPage: mockPopupPage,
|
|
workFrame: mockWorkFrame
|
|
})
|
|
|
|
vi.spyOn(extractorCore as any, 'downloadBatch').mockResolvedValue('/path/file.xlsx')
|
|
|
|
const input: ExtractorCoreInput = {
|
|
session: mockSession,
|
|
orderNumbers,
|
|
downloadDir: '/test/downloads',
|
|
batchSize
|
|
}
|
|
|
|
const result = await extractorCore.downloadAllBatches(input)
|
|
|
|
expect(result.downloadedFiles).toHaveLength(1)
|
|
expect(result.errors).toHaveLength(0)
|
|
})
|
|
|
|
it.todo('TODO: needs integration test setup - should handle complete navigation flow', () => {
|
|
// Complex test requiring full iframe structure mocking
|
|
})
|
|
|
|
it.todo('TODO: needs integration test setup - should handle download events correctly', () => {
|
|
// Complex test requiring download event mocking
|
|
})
|
|
|
|
it.todo('TODO: needs integration test setup - should verify locator interactions', () => {
|
|
// Complex test requiring detailed locator interaction verification
|
|
})
|
|
})
|
|
})
|