Files
BIPMaterialManager/tests/unit/services/erp/extractor-core.test.ts
Misaka d0f8ad0fef test(erp): add unit tests for ERP services and test coverage docs
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>
2026-04-05 17:23:00 +08:00

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
})
})
})