test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP

This commit is contained in:
2026-06-13 22:33:11 +07:00
parent 190b9a3af5
commit 9c5df0abdb
37 changed files with 6128 additions and 24 deletions
@@ -0,0 +1,277 @@
// File: frontend/lib/services/__tests__/workflow-engine.service.test.ts
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
workflowEngineService,
normalizeWorkflowType,
extractDslDefinition,
extractArrayData,
extractNestedData,
mapWorkflow,
} from '../workflow-engine.service';
import apiClient from '@/lib/api/client';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
describe('workflowEngineService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Engine Execution', () => {
it('should get available actions', async () => {
const mockActions = ['APPROVE', 'REJECT'];
(apiClient.post as any).mockResolvedValue({ data: { data: mockActions } });
const result = await workflowEngineService.getAvailableActions({
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
});
expect(result).toEqual(mockActions);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/available-actions', {
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
});
});
it('should evaluate workflow transition', async () => {
const mockEvaluation = { nextState: 'APPROVED', events: [] };
(apiClient.post as any).mockResolvedValue({ data: { data: mockEvaluation } });
const result = await workflowEngineService.evaluate({
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
action: 'APPROVE',
});
expect(result).toEqual(mockEvaluation);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/evaluate', {
entityType: 'RFA',
entityId: '019505a1-7c3e-7000-8000-abc123def456',
action: 'APPROVE',
});
});
});
describe('Definition Management', () => {
it('should get all workflow definitions', async () => {
const mockWorkflows = [
{
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'RFA Workflow',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
},
];
(apiClient.get as any).mockResolvedValue({ data: mockWorkflows });
const result = await workflowEngineService.getDefinitions();
expect(result).toHaveLength(1);
expect(result[0].workflowName).toBe('RFA Flow');
expect(result[0].workflowType).toBe('RFA');
expect(apiClient.get).toHaveBeenCalledWith('/workflow-engine/definitions');
});
it('should get workflow definition by id', async () => {
const mockWorkflow = {
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'RFA Workflow',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
};
(apiClient.get as any).mockResolvedValue({ data: { data: mockWorkflow } });
const result = await workflowEngineService.getDefinitionById(1);
expect(result.workflowName).toBe('RFA Flow');
expect(apiClient.get).toHaveBeenCalledWith('/workflow-engine/definitions/1');
});
it('should create workflow definition', async () => {
const mockCreated = { id: 1, workflow_code: 'NEW_FLOW' };
(apiClient.post as any).mockResolvedValue({ data: { data: mockCreated } });
const result = await workflowEngineService.createDefinition({
workflowCode: 'NEW_FLOW',
dslDefinition: '{}',
});
expect(result).toEqual(mockCreated);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/definitions', {
workflowCode: 'NEW_FLOW',
dslDefinition: '{}',
});
});
it('should update workflow definition', async () => {
const mockUpdated = { id: 1, workflow_code: 'UPDATED_FLOW' };
(apiClient.patch as any).mockResolvedValue({ data: { data: mockUpdated } });
const result = await workflowEngineService.updateDefinition(1, {
dslDefinition: '{}',
});
expect(result).toEqual(mockUpdated);
expect(apiClient.patch).toHaveBeenCalledWith('/workflow-engine/definitions/1', {
dslDefinition: '{}',
});
});
it('should validate DSL', async () => {
const mockValidation = { valid: true };
(apiClient.post as any).mockResolvedValue({ data: { data: mockValidation } });
const result = await workflowEngineService.validateDsl({ workflowName: 'Test' });
expect(result).toEqual(mockValidation);
expect(apiClient.post).toHaveBeenCalledWith('/workflow-engine/definitions/validate', {
dsl: { workflowName: 'Test' },
});
});
it('should return validation errors for invalid DSL', async () => {
const mockValidation = {
valid: false,
errors: [{ path: 'states', message: 'Invalid state' }],
};
(apiClient.post as any).mockResolvedValue({ data: { data: mockValidation } });
const result = await workflowEngineService.validateDsl({});
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.errors).toHaveLength(1);
}
});
it('should delete workflow definition', async () => {
const mockDeleted = { id: 1 };
(apiClient.delete as any).mockResolvedValue({ data: { data: mockDeleted } });
const result = await workflowEngineService.deleteDefinition(1);
expect(result).toEqual(mockDeleted);
expect(apiClient.delete).toHaveBeenCalledWith('/workflow-engine/definitions/1');
});
});
describe('Workflow Transition and History', () => {
it('should transition workflow instance', async () => {
const mockTransition = { instanceId: 'uuid-1', state: 'APPROVED' };
(apiClient.post as any).mockResolvedValue({ data: { data: mockTransition } });
const result = await workflowEngineService.transition(
'019505a1-7c3e-7000-8000-abc123def456',
{ action: 'APPROVE', comments: 'Approved' },
'idempotency-key-123'
);
expect(result).toEqual(mockTransition);
expect(apiClient.post).toHaveBeenCalledWith(
'/workflow-engine/instances/019505a1-7c3e-7000-8000-abc123def456/transition',
{ action: 'APPROVE', comments: 'Approved' },
{ headers: { 'Idempotency-Key': 'idempotency-key-123' } }
);
});
it('should get workflow history', async () => {
const mockHistory = [
{
id: 1,
fromState: 'DFT',
toState: 'FAP',
action: 'SUBMIT',
actorId: 'user-uuid',
timestamp: '2026-01-01T00:00:00Z',
},
];
(apiClient.get as any).mockResolvedValue({ data: { data: mockHistory } });
const result = await workflowEngineService.getHistory('019505a1-7c3e-7000-8000-abc123def456');
expect(result).toEqual(mockHistory);
expect(apiClient.get).toHaveBeenCalledWith(
'/workflow-engine/instances/019505a1-7c3e-7000-8000-abc123def456/history'
);
});
it('should handle empty history array', async () => {
(apiClient.get as any).mockResolvedValue({ data: { data: [] } });
const result = await workflowEngineService.getHistory('019505a1-7c3e-7000-8000-abc123def456');
expect(result).toEqual([]);
});
});
describe('Helper Functions', () => {
it('should normalize workflow type to RFA', () => {
expect(normalizeWorkflowType('RFA_FLOW_V1')).toBe('RFA');
expect(normalizeWorkflowType('rfa_flow_v1')).toBe('RFA');
});
it('should normalize workflow type to DRAWING', () => {
expect(normalizeWorkflowType('DRAWING_FLOW_V1')).toBe('DRAWING');
expect(normalizeWorkflowType('drawing_flow_v1')).toBe('DRAWING');
});
it('should normalize workflow type to CORRESPONDENCE by default', () => {
expect(normalizeWorkflowType('CORR_FLOW_V1')).toBe('CORRESPONDENCE');
expect(normalizeWorkflowType(undefined)).toBe('CORRESPONDENCE');
});
it('should extract DSL definition from string', () => {
const dsl = '{"workflowName": "Test"}';
expect(extractDslDefinition(dsl)).toBe(dsl);
});
it('should extract DSL definition from object', () => {
const dsl = { dslDefinition: '{"workflowName": "Test"}' };
expect(extractDslDefinition(dsl)).toBe('{"workflowName": "Test"}');
});
it('should return empty string for invalid DSL', () => {
expect(extractDslDefinition(null)).toBe('');
expect(extractDslDefinition(undefined)).toBe('');
expect(extractDslDefinition('')).toBe('');
});
it('should extract array data from nested structure', () => {
const data = { data: { data: [1, 2, 3] } };
const result = extractArrayData(data);
expect(result).toEqual([1, 2, 3]);
});
it('should return empty array for non-array data', () => {
const data = { data: 'not an array' };
const result = extractArrayData(data);
expect(result).toEqual([]);
});
it('should extract nested data', () => {
const data = { data: { data: { id: 1 } } };
const result = extractNestedData(data);
expect(result).toEqual({ id: 1 });
});
it('should map backend workflow to frontend workflow', () => {
const backendWorkflow = {
id: 1,
workflow_code: 'RFA_FLOW_V1',
description: 'Test',
version: 1,
is_active: true,
dsl: { workflowName: 'RFA Flow' },
compiled: { states: { DFT: {}, FAP: {} } },
updated_at: '2026-01-01T00:00:00Z',
};
const result = mapWorkflow(backendWorkflow);
expect(result.publicId).toBe('1');
expect(result.workflowName).toBe('RFA Flow');
expect(result.workflowType).toBe('RFA');
expect(result.version).toBe(1);
expect(result.isActive).toBe(true);
expect(result.stepCount).toBe(2);
});
it('should throw error when mapping null workflow', () => {
expect(() => mapWorkflow(null as any)).toThrow('Workflow not found');
});
});
});