refactor(test): replace module-level mutable state with vi.fn() mocks

Replace fragile module-level let variables and SQL string parsing
with vi.hoisted() mock functions that are reset and configured
per-test in beforeEach via mockResolvedValue/mockResolvedValueOnce.

- Remove 8 module-level mutable state variables
- Remove matchQuery() SQL parser
- Use vi.hoisted() for shared mock functions across vi.mock() factories
- Each test explicitly controls mock return values with mockResolvedValueOnce
- Fix getCleanerData error test to use direct mock instead of dynamic import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-06 09:21:04 +08:00
parent 0560b3c84a
commit 188117e5ce

View File

@@ -18,130 +18,132 @@ vi.mock('../../../../src/main/services/logger', () => ({
getRequestId: () => undefined getRequestId: () => undefined
})) }))
// ─── DAO mock state ─── // ─── Hoisted mock functions shared between vi.mock() factories and tests ───
let returnEmptyMaterials = false const {
let materialRecords: any[] = [ mockQueryAll,
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' } mockQueryBySource,
] mockGetMaterialsByManager,
let filteredMaterialRecords: any[] = [ mockGetAllRecords,
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' } mockGetAllMaterialCodes,
] mockGetSourceNumbers,
mockReadProductionIds,
mockSharedIdsGet,
mockDbQuery,
mockDbDisconnect,
mockCreateDbService
} = vi.hoisted(() => ({
mockQueryAll: vi.fn(),
mockQueryBySource: vi.fn(),
mockGetMaterialsByManager: vi.fn(),
mockGetAllRecords: vi.fn(),
mockGetAllMaterialCodes: vi.fn(),
mockGetSourceNumbers: vi.fn(),
mockReadProductionIds: vi.fn(),
mockSharedIdsGet: vi.fn(),
mockDbQuery: vi.fn(),
mockDbDisconnect: vi.fn(),
mockCreateDbService: vi.fn()
}))
vi.mock('../../../../src/main/services/database/discrete-material-plan-dao', () => { // ─── DiscreteMaterialPlanDAO mock ───
return { vi.mock('../../../../src/main/services/database/discrete-material-plan-dao', () => ({
DiscreteMaterialPlanDAO: class { DiscreteMaterialPlanDAO: class {
async queryAllDistinctByMaterialCode() { queryAllDistinctByMaterialCode = mockQueryAll
if (returnEmptyMaterials) return [] queryBySourceNumbersDistinct = mockQueryBySource
return materialRecords
} }
async queryBySourceNumbersDistinct(_nums: string[]) { }))
if (returnEmptyMaterials) return []
return filteredMaterialRecords
}
}
}
})
// ─── MaterialsToBeDeletedDAO mock state ─── // ─── MaterialsToBeDeletedDAO mock ───
let materialsByManagerResult: any[] = [] vi.mock('../../../../src/main/services/database/materials-to-be-deleted-dao', () => ({
let allMaterialsResult: any[] = []
let allMaterialCodesResult: Set<string> = new Set()
vi.mock('../../../../src/main/services/database/materials-to-be-deleted-dao', () => {
return {
MaterialsToBeDeletedDAO: class { MaterialsToBeDeletedDAO: class {
async getMaterialsByManager(_managerName: string) { getMaterialsByManager = mockGetMaterialsByManager
return materialsByManagerResult getAllRecords = mockGetAllRecords
getAllMaterialCodes = mockGetAllMaterialCodes
} }
async getAllRecords() { }))
return allMaterialsResult
}
async getAllMaterialCodes() {
return allMaterialCodesResult
}
}
}
})
// ─── Production input service mock ─── // ─── Production input service mock ───
let sourceNumbersFromInputs: string[] = ['SC001', 'SC002']
vi.mock('../../../../src/main/services/validation/production-input-service', () => ({ vi.mock('../../../../src/main/services/validation/production-input-service', () => ({
getSourceNumbersFromInputs: vi.fn().mockImplementation(async () => sourceNumbersFromInputs), getSourceNumbersFromInputs: mockGetSourceNumbers,
readProductionIds: vi.fn().mockReturnValue(['PROD001', 'PROD002']) readProductionIds: mockReadProductionIds
})) }))
// ─── Shared production IDs store mock ─── // ─── Shared production IDs store mock ───
let sharedIds: string[] = []
vi.mock('../../../../src/main/services/validation/shared-production-ids-store', () => ({ vi.mock('../../../../src/main/services/validation/shared-production-ids-store', () => ({
sharedProductionIdsStore: { sharedProductionIdsStore: {
get: (_senderId: number) => sharedIds get: mockSharedIdsGet
} }
})) }))
// ─── Validation database mock with robust SQL matching ─── // ─── Validation database mock ───
function matchQuery(sql: string): string {
// Extract table name from FROM clause to avoid substring ambiguity
const fromMatch = sql.match(/FROM\s+(\S+)/i)
const table = fromMatch?.[1] ?? ''
if (/MaterialsTypeToBeDeleted/i.test(table)) return 'typeKeywords'
if (/MaterialsToBeDeleted/i.test(table)) return 'markedCodes'
if (/DiscreteMaterialPlanData/i.test(table)) return 'materialDetail'
return 'unknown'
}
vi.mock('../../../../src/main/services/validation/validation-database', () => ({ vi.mock('../../../../src/main/services/validation/validation-database', () => ({
createValidationDatabaseService: vi.fn().mockImplementation(async () => ({ createValidationDatabaseService: mockCreateDbService,
type: 'mysql' as const,
query: vi.fn().mockImplementation((sql: string) => {
const kind = matchQuery(sql)
switch (kind) {
case 'typeKeywords':
return Promise.resolve({
rows: [{ MaterialName: 'MatA', ManagerName: 'Mgr' }],
rowCount: 1
})
case 'markedCodes':
return Promise.resolve({
rows: [{ MaterialCode: 'M1', ManagerName: 'Mgr' }],
rowCount: 1
})
case 'materialDetail':
return Promise.resolve({
rows: [{ MaterialName: 'MatA', Specification: 'Spec', Model: 'Mod' }],
rowCount: 1
})
default:
return Promise.resolve({ rows: [], rowCount: 0 })
}
}),
disconnect: vi.fn().mockResolvedValue(undefined),
connect: vi.fn().mockResolvedValue(undefined)
})),
getValidationTableName: vi.fn().mockImplementation((name: string) => name) getValidationTableName: vi.fn().mockImplementation((name: string) => name)
})) }))
function createDbService() {
return {
type: 'mysql' as const,
query: mockDbQuery,
connect: vi.fn().mockResolvedValue(undefined),
disconnect: mockDbDisconnect
}
}
describe('ValidationApplicationService', () => { describe('ValidationApplicationService', () => {
let service: ValidationApplicationService let service: ValidationApplicationService
beforeEach(() => { beforeEach(() => {
returnEmptyMaterials = false vi.clearAllMocks()
materialRecords = [
// DiscreteMaterialPlanDAO defaults
mockQueryAll.mockResolvedValue([
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' } { MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }
] ])
filteredMaterialRecords = [ mockQueryBySource.mockResolvedValue([
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' } { MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' }
] ])
materialsByManagerResult = [{ materialCode: 'M1', managerName: 'Mgr' }]
allMaterialsResult = [ // MaterialsToBeDeletedDAO defaults
mockGetMaterialsByManager.mockResolvedValue([{ materialCode: 'M1', managerName: 'Mgr' }])
mockGetAllRecords.mockResolvedValue([
{ materialCode: 'M1', managerName: 'Mgr' }, { materialCode: 'M1', managerName: 'Mgr' },
{ materialCode: 'M2', managerName: 'Other' } { materialCode: 'M2', managerName: 'Other' }
] ])
allMaterialCodesResult = new Set(['M1']) mockGetAllMaterialCodes.mockResolvedValue(new Set(['M1']))
sourceNumbersFromInputs = ['SC001', 'SC002']
sharedIds = [] // Production input defaults
mockGetSourceNumbers.mockResolvedValue(['SC001', 'SC002'])
mockReadProductionIds.mockReturnValue(['PROD001', 'PROD002'])
// Shared IDs default: empty
mockSharedIdsGet.mockReturnValue([])
// DB service creation
mockCreateDbService.mockImplementation(async () => createDbService())
mockDbDisconnect.mockResolvedValue(undefined)
// DB query dispatches by table name extracted from SQL
mockDbQuery.mockImplementation((sql: string) => {
const table = sql.match(/FROM\s+(\S+)/i)?.[1] ?? ''
if (/MaterialsTypeToBeDeleted/i.test(table))
return Promise.resolve({
rows: [{ MaterialName: 'MatA', ManagerName: 'Mgr' }],
rowCount: 1
})
if (/MaterialsToBeDeleted/i.test(table))
return Promise.resolve({
rows: [{ MaterialCode: 'M1', ManagerName: 'Mgr' }],
rowCount: 1
})
if (/DiscreteMaterialPlanData/i.test(table))
return Promise.resolve({
rows: [{ MaterialName: 'MatA', Specification: 'Spec', Model: 'Mod' }],
rowCount: 1
})
return Promise.resolve({ rows: [], rowCount: 0 })
})
service = new ValidationApplicationService() service = new ValidationApplicationService()
}) })
@@ -166,7 +168,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return failure when material records are empty', async () => { it('should return failure when material records are empty', async () => {
returnEmptyMaterials = true mockQueryAll.mockResolvedValueOnce([])
const req: ValidationRequest = { mode: 'database_full' } const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1) const res = await service.validate(req, userInfo, 1)
@@ -175,10 +177,10 @@ describe('ValidationApplicationService', () => {
}) })
it('should correctly compute stats for matched and marked records', async () => { it('should correctly compute stats for matched and marked records', async () => {
materialRecords = [ mockQueryAll.mockResolvedValueOnce([
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }, { MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' },
{ MaterialName: 'Other', MaterialCode: 'M2', Model: 'Mod', Specification: 'Spec' } { MaterialName: 'Other', MaterialCode: 'M2', Model: 'Mod', Specification: 'Spec' }
] ])
const req: ValidationRequest = { mode: 'database_full' } const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1) const res = await service.validate(req, userInfo, 1)
@@ -189,15 +191,15 @@ describe('ValidationApplicationService', () => {
const m1Result = res.results!.find((r) => r.materialCode === 'M1') const m1Result = res.results!.find((r) => r.materialCode === 'M1')
expect(m1Result?.isMarkedForDeletion).toBe(true) expect(m1Result?.isMarkedForDeletion).toBe(true)
expect(m1Result?.managerName).toBe('Mgr') expect(m1Result?.managerName).toBe('Mgr')
// M2 is NOT in markedCodes but 'Other' doesn't match any type keyword // M2 is NOT in markedCodes and 'Other' doesn't match any type keyword
const m2Result = res.results!.find((r) => r.materialCode === 'M2') const m2Result = res.results!.find((r) => r.materialCode === 'M2')
expect(m2Result?.isMarkedForDeletion).toBe(false) expect(m2Result?.isMarkedForDeletion).toBe(false)
}) })
it('should match type keywords when material name contains keyword', async () => { it('should match type keywords when material name contains keyword', async () => {
materialRecords = [ mockQueryAll.mockResolvedValueOnce([
{ MaterialName: 'MatA-Extra', MaterialCode: 'MX1', Model: 'Mod', Specification: 'Spec' } { MaterialName: 'MatA-Extra', MaterialCode: 'MX1', Model: 'Mod', Specification: 'Spec' }
] ])
const req: ValidationRequest = { mode: 'database_full' } const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1) const res = await service.validate(req, userInfo, 1)
@@ -212,7 +214,7 @@ describe('ValidationApplicationService', () => {
// ─── validate database_filtered with shared production IDs ─── // ─── validate database_filtered with shared production IDs ───
describe('validate database_filtered with shared IDs', () => { describe('validate database_filtered with shared IDs', () => {
it('should return success when shared IDs resolve to orders', async () => { it('should return success when shared IDs resolve to orders', async () => {
sharedIds = ['PROD001'] mockSharedIdsGet.mockReturnValue(['PROD001'])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true } const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
@@ -224,7 +226,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return failure when shared IDs are empty', async () => { it('should return failure when shared IDs are empty', async () => {
sharedIds = [] mockSharedIdsGet.mockReturnValue([])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true } const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
@@ -235,8 +237,8 @@ describe('ValidationApplicationService', () => {
}) })
it('should return failure when shared IDs yield no source numbers', async () => { it('should return failure when shared IDs yield no source numbers', async () => {
sharedIds = ['PROD001'] mockSharedIdsGet.mockReturnValue(['PROD001'])
sourceNumbersFromInputs = [] mockGetSourceNumbers.mockResolvedValueOnce([])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true } const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
@@ -263,7 +265,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return failure when file IDs yield no source numbers', async () => { it('should return failure when file IDs yield no source numbers', async () => {
sourceNumbersFromInputs = [] mockGetSourceNumbers.mockResolvedValueOnce([])
const req: ValidationRequest = { const req: ValidationRequest = {
mode: 'database_filtered', mode: 'database_filtered',
productionIdFile: '/tmp/ids.txt' productionIdFile: '/tmp/ids.txt'
@@ -290,7 +292,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return empty array when manager has no materials', async () => { it('should return empty array when manager has no materials', async () => {
materialsByManagerResult = [] mockGetMaterialsByManager.mockResolvedValueOnce([])
const result = await service.getMaterialsByManager('Nobody') const result = await service.getMaterialsByManager('Nobody')
@@ -312,7 +314,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return empty array when no materials exist', async () => { it('should return empty array when no materials exist', async () => {
allMaterialsResult = [] mockGetAllRecords.mockResolvedValueOnce([])
const result = await service.getAllMaterials() const result = await service.getAllMaterials()
@@ -323,7 +325,7 @@ describe('ValidationApplicationService', () => {
// ─── getCleanerData ─── // ─── getCleanerData ───
describe('getCleanerData', () => { describe('getCleanerData', () => {
it('should return order numbers and material codes for admin user', async () => { it('should return order numbers and material codes for admin user', async () => {
sharedIds = ['PROD001'] mockSharedIdsGet.mockReturnValue(['PROD001'])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1) const result = await service.getCleanerData(userInfo, 1)
@@ -335,7 +337,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should return empty order numbers when no shared IDs', async () => { it('should return empty order numbers when no shared IDs', async () => {
sharedIds = [] mockSharedIdsGet.mockReturnValue([])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1) const result = await service.getCleanerData(userInfo, 1)
@@ -345,9 +347,7 @@ describe('ValidationApplicationService', () => {
}) })
it('should handle errors gracefully', async () => { it('should handle errors gracefully', async () => {
const { createValidationDatabaseService } = mockCreateDbService.mockRejectedValueOnce(new Error('DB down'))
await import('../../../../src/main/services/validation/validation-database')
vi.mocked(createValidationDatabaseService).mockRejectedValueOnce(new Error('DB down'))
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1) const result = await service.getCleanerData(userInfo, 1)