test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user