From d0c745e243595a187df692456747c73b8a43b928 Mon Sep 17 00:00:00 2001 From: Misaka Date: Mon, 6 Apr 2026 11:55:58 +0800 Subject: [PATCH] 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 --- tests/e2e/extractor-workflow.test.ts | 40 ++++---- tests/unit/cleaner.test.ts | 145 --------------------------- tests/unit/erp-auth.unit.test.ts | 28 ------ tests/unit/extractor.test.ts | 70 ------------- vitest.config.ts | 3 +- 5 files changed, 18 insertions(+), 268 deletions(-) delete mode 100644 tests/unit/cleaner.test.ts delete mode 100644 tests/unit/erp-auth.unit.test.ts delete mode 100644 tests/unit/extractor.test.ts diff --git a/tests/e2e/extractor-workflow.test.ts b/tests/e2e/extractor-workflow.test.ts index 2127ec7..54554c1 100644 --- a/tests/e2e/extractor-workflow.test.ts +++ b/tests/e2e/extractor-workflow.test.ts @@ -5,45 +5,39 @@ * Run: npx playwright test tests/e2e/extractor-workflow.test.ts */ -import { _electron as electron } from '@playwright/test' -import { describe, it, expect, beforeAll, afterAll } from 'vitest' -import type { ElectronApplication, Page, BrowserWindow } from 'playwright' +import { test, expect, type ElectronApplication, type Page } from '@playwright/test' +import { _electron as electron } from 'playwright' import { join } from 'path' -describe('Extractor E2E Workflow', () => { - let electronApp: ElectronApplication - let window: BrowserWindow - let page: Page - - beforeAll(async () => { - // Build the app first (if not already built) - // npm run build +let electronApp: ElectronApplication +let page: Page +test.describe('Extractor E2E Workflow', () => { + test.beforeAll(async () => { // Launch Electron app for testing electronApp = await electron.launch({ args: [join(process.cwd(), 'out/main/index.js')] }) // Get the main window - window = await electronApp.firstWindow() page = await electronApp.firstWindow() // Wait for app to load await page.waitForLoadState('domcontentloaded') - }, 60000) + }) - afterAll(async () => { + test.afterAll(async () => { if (electronApp) { await electronApp.close() } - }, 60000) + }) - it('should launch the application', async () => { + test('should launch the application', async () => { const title = await page.title() expect(title).toBeDefined() }) - it('should navigate to Extractor page', async () => { + test('should navigate to Extractor page', async () => { // Click on the "数据提取" link await page.click('a:has-text("数据提取")') @@ -55,7 +49,7 @@ describe('Extractor E2E Workflow', () => { 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 const textarea = page.locator('.order-textarea') await expect(textarea).toBeVisible() @@ -65,7 +59,7 @@ describe('Extractor E2E Workflow', () => { expect(placeholder).toContain('订单号') }) - it('should update order count when typing', async () => { + test('should update order count when typing', async () => { // Fill in order numbers const textarea = page.locator('.order-textarea') await textarea.fill('SC70202602120085\nSC70202602120120') @@ -79,7 +73,7 @@ describe('Extractor E2E Workflow', () => { 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 const textarea = page.locator('.order-textarea') await textarea.fill('') @@ -90,7 +84,7 @@ describe('Extractor E2E Workflow', () => { 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"]') await expect(batchSizeInput).toBeVisible() @@ -98,7 +92,7 @@ describe('Extractor E2E Workflow', () => { expect(value).toBe('100') }) - it('should have reset button', async () => { + test('should have reset button', async () => { const resetButton = page.locator('.btn-secondary:has-text("重置")') await expect(resetButton).toBeVisible() @@ -111,7 +105,7 @@ describe('Extractor E2E Workflow', () => { expect(value).toBe('') }) - it('should navigate back to home', async () => { + test('should navigate back to home', async () => { // Click back button await page.click('.nav-btn:has-text("返回主页")') diff --git a/tests/unit/cleaner.test.ts b/tests/unit/cleaner.test.ts deleted file mode 100644 index 65684c5..0000000 --- a/tests/unit/cleaner.test.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/tests/unit/erp-auth.unit.test.ts b/tests/unit/erp-auth.unit.test.ts deleted file mode 100644 index 4a9985c..0000000 --- a/tests/unit/erp-auth.unit.test.ts +++ /dev/null @@ -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() - }) - }) -}) diff --git a/tests/unit/extractor.test.ts b/tests/unit/extractor.test.ts deleted file mode 100644 index 71aeaed..0000000 --- a/tests/unit/extractor.test.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/vitest.config.ts b/vitest.config.ts index f8564fa..da8951e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,8 +11,7 @@ export default defineConfig({ env: { NODE_ENV: 'test' }, - // CI 环境启用隔离以捕获跨文件状态污染;本地开发禁用以提升速度 - isolate: !!process.env.CI, + isolate: true, pool: 'threads', // 使用线程池 maxWorkers: 4, bail: process.env.CI ? 1 : undefined,