refactor(test): migrate e2e to Playwright and remove duplicate unit tests
Switch extractor-workflow e2e test from vitest to Playwright test runner for consistency with playwright.config.ts. Remove redundant unit tests (cleaner, erp-auth, extractor) that have been superseded by more thorough replacements under tests/unit/services/erp/. Enable test isolation unconditionally to prevent cross-file state pollution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,45 +5,39 @@
|
|||||||
* Run: npx playwright test tests/e2e/extractor-workflow.test.ts
|
* Run: npx playwright test tests/e2e/extractor-workflow.test.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { _electron as electron } from '@playwright/test'
|
import { test, expect, type ElectronApplication, type Page } from '@playwright/test'
|
||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
import { _electron as electron } from 'playwright'
|
||||||
import type { ElectronApplication, Page, BrowserWindow } from 'playwright'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
describe('Extractor E2E Workflow', () => {
|
let electronApp: ElectronApplication
|
||||||
let electronApp: ElectronApplication
|
let page: Page
|
||||||
let window: BrowserWindow
|
|
||||||
let page: Page
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
// Build the app first (if not already built)
|
|
||||||
// npm run build
|
|
||||||
|
|
||||||
|
test.describe('Extractor E2E Workflow', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
// Launch Electron app for testing
|
// Launch Electron app for testing
|
||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [join(process.cwd(), 'out/main/index.js')]
|
args: [join(process.cwd(), 'out/main/index.js')]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get the main window
|
// Get the main window
|
||||||
window = await electronApp.firstWindow()
|
|
||||||
page = await electronApp.firstWindow()
|
page = await electronApp.firstWindow()
|
||||||
|
|
||||||
// Wait for app to load
|
// Wait for app to load
|
||||||
await page.waitForLoadState('domcontentloaded')
|
await page.waitForLoadState('domcontentloaded')
|
||||||
}, 60000)
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
if (electronApp) {
|
if (electronApp) {
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
}
|
}
|
||||||
}, 60000)
|
})
|
||||||
|
|
||||||
it('should launch the application', async () => {
|
test('should launch the application', async () => {
|
||||||
const title = await page.title()
|
const title = await page.title()
|
||||||
expect(title).toBeDefined()
|
expect(title).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should navigate to Extractor page', async () => {
|
test('should navigate to Extractor page', async () => {
|
||||||
// Click on the "数据提取" link
|
// Click on the "数据提取" link
|
||||||
await page.click('a:has-text("数据提取")')
|
await page.click('a:has-text("数据提取")')
|
||||||
|
|
||||||
@@ -55,7 +49,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(pageTitle).toContain('ERP 数据提取')
|
expect(pageTitle).toContain('ERP 数据提取')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display order number input', async () => {
|
test('should display order number input', async () => {
|
||||||
// Check if order number textarea is visible
|
// Check if order number textarea is visible
|
||||||
const textarea = page.locator('.order-textarea')
|
const textarea = page.locator('.order-textarea')
|
||||||
await expect(textarea).toBeVisible()
|
await expect(textarea).toBeVisible()
|
||||||
@@ -65,7 +59,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(placeholder).toContain('订单号')
|
expect(placeholder).toContain('订单号')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update order count when typing', async () => {
|
test('should update order count when typing', async () => {
|
||||||
// Fill in order numbers
|
// Fill in order numbers
|
||||||
const textarea = page.locator('.order-textarea')
|
const textarea = page.locator('.order-textarea')
|
||||||
await textarea.fill('SC70202602120085\nSC70202602120120')
|
await textarea.fill('SC70202602120085\nSC70202602120120')
|
||||||
@@ -79,7 +73,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(countText).toContain('2')
|
expect(countText).toContain('2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show error when extracting without order numbers', async () => {
|
test('should show error when extracting without order numbers', async () => {
|
||||||
// Clear the textarea
|
// Clear the textarea
|
||||||
const textarea = page.locator('.order-textarea')
|
const textarea = page.locator('.order-textarea')
|
||||||
await textarea.fill('')
|
await textarea.fill('')
|
||||||
@@ -90,7 +84,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(isDisabled).toBe(true)
|
expect(isDisabled).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have batch size input', async () => {
|
test('should have batch size input', async () => {
|
||||||
const batchSizeInput = page.locator('input[type="number"]')
|
const batchSizeInput = page.locator('input[type="number"]')
|
||||||
await expect(batchSizeInput).toBeVisible()
|
await expect(batchSizeInput).toBeVisible()
|
||||||
|
|
||||||
@@ -98,7 +92,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(value).toBe('100')
|
expect(value).toBe('100')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have reset button', async () => {
|
test('should have reset button', async () => {
|
||||||
const resetButton = page.locator('.btn-secondary:has-text("重置")')
|
const resetButton = page.locator('.btn-secondary:has-text("重置")')
|
||||||
await expect(resetButton).toBeVisible()
|
await expect(resetButton).toBeVisible()
|
||||||
|
|
||||||
@@ -111,7 +105,7 @@ describe('Extractor E2E Workflow', () => {
|
|||||||
expect(value).toBe('')
|
expect(value).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should navigate back to home', async () => {
|
test('should navigate back to home', async () => {
|
||||||
// Click back button
|
// Click back button
|
||||||
await page.click('.nav-btn:has-text("返回主页")')
|
await page.click('.nav-btn:has-text("返回主页")')
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import {
|
|
||||||
CleanerService,
|
|
||||||
createBatches,
|
|
||||||
getMissingOrders,
|
|
||||||
runWithConcurrency
|
|
||||||
} from '../../src/main/services/erp/cleaner'
|
|
||||||
|
|
||||||
describe('Cleaner Service (Unit)', () => {
|
|
||||||
describe('shouldDeleteMaterial', () => {
|
|
||||||
// CleanerService constructor requires ErpAuthService, but shouldDeleteMaterial doesn't use it
|
|
||||||
const cleaner = new CleanerService({} as any)
|
|
||||||
|
|
||||||
it('should skip materials with row number 2000-7999', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ rowNumber: 2000, pendingQty: '', materialCode: 'TEST001', expected: false },
|
|
||||||
{ rowNumber: 5000, pendingQty: '', materialCode: 'TEST001', expected: false },
|
|
||||||
{ rowNumber: 7999, pendingQty: '', materialCode: 'TEST001', expected: false },
|
|
||||||
{ rowNumber: 1999, pendingQty: '', materialCode: 'TEST001', expected: true },
|
|
||||||
{ rowNumber: 8000, pendingQty: '', materialCode: 'TEST001', expected: true }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const tc of testCases) {
|
|
||||||
const shouldDelete = cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: tc.rowNumber,
|
|
||||||
pendingQty: tc.pendingQty,
|
|
||||||
materialCode: tc.materialCode,
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
expect(shouldDelete).toBe(tc.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should skip materials with non-empty pending quantity', () => {
|
|
||||||
const result = cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 100,
|
|
||||||
pendingQty: '5',
|
|
||||||
materialCode: 'TEST001',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should skip materials not in delete list', () => {
|
|
||||||
const result = cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 100,
|
|
||||||
pendingQty: '',
|
|
||||||
materialCode: 'NOT_IN_LIST',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should delete materials with empty pending qty and valid row number', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ rowNumber: 1, pendingQty: '', materialCode: 'TEST001', expected: true },
|
|
||||||
{ rowNumber: 100, pendingQty: '', materialCode: 'TEST001', expected: true },
|
|
||||||
{ rowNumber: 1999, pendingQty: '', materialCode: 'TEST001', expected: true },
|
|
||||||
{ rowNumber: 8000, pendingQty: '', materialCode: 'TEST001', expected: true },
|
|
||||||
{ rowNumber: 10000, pendingQty: '', materialCode: 'TEST001', expected: true }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const tc of testCases) {
|
|
||||||
const shouldDelete = cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: tc.rowNumber,
|
|
||||||
pendingQty: tc.pendingQty,
|
|
||||||
materialCode: tc.materialCode,
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
expect(shouldDelete).toBe(tc.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle multiple conditions correctly', () => {
|
|
||||||
// Material in list, valid row, no pending qty = should delete
|
|
||||||
expect(
|
|
||||||
cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 100,
|
|
||||||
pendingQty: '',
|
|
||||||
materialCode: 'TEST001',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
).toBe(true)
|
|
||||||
|
|
||||||
// Material in list, protected row, no pending qty = should NOT delete
|
|
||||||
expect(
|
|
||||||
cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 7500,
|
|
||||||
pendingQty: '',
|
|
||||||
materialCode: 'TEST001',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
).toBe(false)
|
|
||||||
|
|
||||||
// Material in list, valid row, has pending qty = should NOT delete
|
|
||||||
expect(
|
|
||||||
cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 100,
|
|
||||||
pendingQty: '10',
|
|
||||||
materialCode: 'TEST001',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
).toBe(false)
|
|
||||||
|
|
||||||
// Material NOT in list = should NOT delete
|
|
||||||
expect(
|
|
||||||
cleaner.shouldDeleteMaterial({
|
|
||||||
rowNumber: 100,
|
|
||||||
pendingQty: '',
|
|
||||||
materialCode: 'OTHER',
|
|
||||||
deleteSet: new Set(['TEST001'])
|
|
||||||
})
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('batch and concurrency helpers', () => {
|
|
||||||
it('should split orders into batches', () => {
|
|
||||||
const batches = createBatches(['A', 'B', 'C', 'D', 'E'], 2)
|
|
||||||
expect(batches).toEqual([['A', 'B'], ['C', 'D'], ['E']])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should identify missing orders', () => {
|
|
||||||
const missing = getMissingOrders(['SC1', 'SC2', 'SC3'], new Set(['SC1', 'SC3']))
|
|
||||||
expect(missing).toEqual(['SC2'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect concurrency limit', async () => {
|
|
||||||
const items = [1, 2, 3, 4, 5, 6]
|
|
||||||
let running = 0
|
|
||||||
let peak = 0
|
|
||||||
await runWithConcurrency(items, 2, async () => {
|
|
||||||
running += 1
|
|
||||||
peak = Math.max(peak, running)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
running -= 1
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(peak).toBeLessThanOrEqual(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import { ErpAuthService } from '../../src/main/services/erp/erp-auth'
|
|
||||||
import type { ErpConfig } from '../../src/main/types/erp.types'
|
|
||||||
|
|
||||||
const testConfig: ErpConfig = {
|
|
||||||
url: 'https://test.example.com',
|
|
||||||
username: 'testuser',
|
|
||||||
password: 'testpass'
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ERP Authentication Service (Unit)', () => {
|
|
||||||
describe('Initial State', () => {
|
|
||||||
it('should report inactive status before login', () => {
|
|
||||||
const service = new ErpAuthService(testConfig)
|
|
||||||
expect(service.isActive()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when getting session before login', () => {
|
|
||||||
const service = new ErpAuthService(testConfig)
|
|
||||||
expect(() => service.getSession()).toThrow('Not logged in. Call login() first.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle close when no session exists', async () => {
|
|
||||||
const service = new ErpAuthService(testConfig)
|
|
||||||
await expect(service.close()).resolves.toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
|
||||||
import { ExtractorService } from '../../src/main/services/erp/extractor'
|
|
||||||
import { ErpAuthService } from '../../src/main/services/erp/erp-auth'
|
|
||||||
import type { ErpConfig } from '../../src/main/types/erp.types'
|
|
||||||
|
|
||||||
describe('Extractor Service (Unit)', () => {
|
|
||||||
let authService: ErpAuthService
|
|
||||||
let extractor: ExtractorService
|
|
||||||
const mockConfig: ErpConfig = {
|
|
||||||
url: 'https://test.erp.com',
|
|
||||||
username: 'test_user',
|
|
||||||
password: 'test_pass'
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
authService = new ErpAuthService(mockConfig)
|
|
||||||
extractor = new ExtractorService(authService, './test-downloads')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Service Initialization', () => {
|
|
||||||
it('should create service instance as ExtractorService', () => {
|
|
||||||
expect(extractor).toBeInstanceOf(ExtractorService)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create service with default download directory', () => {
|
|
||||||
const defaultExtractor = new ExtractorService(authService)
|
|
||||||
expect(defaultExtractor).toBeInstanceOf(ExtractorService)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create service with custom download directory', () => {
|
|
||||||
const customExtractor = new ExtractorService(authService, './custom-downloads')
|
|
||||||
expect(customExtractor).toBeInstanceOf(ExtractorService)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Error Handling', () => {
|
|
||||||
it('should handle extraction with no auth session', async () => {
|
|
||||||
const result = await extractor.extract({
|
|
||||||
orderNumbers: ['ORDER1']
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.errors.length).toBeGreaterThan(0)
|
|
||||||
expect(result.downloadedFiles).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include error message when session is missing', async () => {
|
|
||||||
const result = await extractor.extract({
|
|
||||||
orderNumbers: ['ORD-001', 'ORD-002']
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.errors).toEqual(
|
|
||||||
expect.arrayContaining([expect.stringContaining('Not logged in')])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return empty result structure even on failure', async () => {
|
|
||||||
const result = await extractor.extract({
|
|
||||||
orderNumbers: ['ORDER1']
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toHaveProperty('downloadedFiles')
|
|
||||||
expect(result).toHaveProperty('mergedFile')
|
|
||||||
expect(result).toHaveProperty('recordCount')
|
|
||||||
expect(result).toHaveProperty('errors')
|
|
||||||
expect(result).toHaveProperty('orderRecordCounts')
|
|
||||||
expect(result.mergedFile).toBeNull()
|
|
||||||
expect(result.recordCount).toBe(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -11,8 +11,7 @@ export default defineConfig({
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'test'
|
NODE_ENV: 'test'
|
||||||
},
|
},
|
||||||
// CI 环境启用隔离以捕获跨文件状态污染;本地开发禁用以提升速度
|
isolate: true,
|
||||||
isolate: !!process.env.CI,
|
|
||||||
pool: 'threads', // 使用线程池
|
pool: 'threads', // 使用线程池
|
||||||
maxWorkers: 4,
|
maxWorkers: 4,
|
||||||
bail: process.env.CI ? 1 : undefined,
|
bail: process.env.CI ? 1 : undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user