feat(5.3): add workflow API frontend module with TypeScript types and React Query hooks
- Create frontend/src/api/workflows.ts with types matching backend workflow model - Workflow CRUD hooks: useWorkflows, useWorkflow, useCreateWorkflow, useUpdateWorkflow, useDeleteWorkflow - Instance hooks: useWorkflowInstances, useWorkflowInstance, useCreateWorkflowInstance, useAdvanceWorkflowInstance, useCancelWorkflowInstance - Step history types included in WorkflowInstanceDetail - Add comprehensive test suite (13 tests, all passing) - TSC: 0 new errors
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the API client
|
||||
const mockApiGet = vi.fn();
|
||||
const mockApiPost = vi.fn();
|
||||
const mockApiPatch = vi.fn();
|
||||
const mockApiDelete = vi.fn();
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
apiGet: (...args: any[]) => mockApiGet(...args),
|
||||
apiPost: (...args: any[]) => mockApiPost(...args),
|
||||
apiPatch: (...args: any[]) => mockApiPatch(...args),
|
||||
apiDelete: (...args: any[]) => mockApiDelete(...args),
|
||||
}));
|
||||
|
||||
// Mock react-query
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: vi.fn(),
|
||||
}),
|
||||
useQuery: vi.fn(),
|
||||
useMutation: vi.fn(({ mutationFn, onSuccess }) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Workflow API Hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useWorkflows', () => {
|
||||
it('calls apiGet with correct endpoint', async () => {
|
||||
const { useWorkflows } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryKey, queryFn }: any) => {
|
||||
expect(queryKey[0]).toBe('workflows');
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflows();
|
||||
expect(mockApiGet).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/workflows?')
|
||||
);
|
||||
});
|
||||
|
||||
it('passes is_active filter when provided', async () => {
|
||||
const { useWorkflows } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryFn }: any) => {
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflows(1, 20, true);
|
||||
expect(mockApiGet).toHaveBeenCalledWith(
|
||||
expect.stringContaining('is_active=true')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkflow', () => {
|
||||
it('calls apiGet with correct endpoint', async () => {
|
||||
const { useWorkflow } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryKey, queryFn, enabled }: any) => {
|
||||
expect(queryKey).toEqual(['workflows', 'wf-123']);
|
||||
expect(enabled).toBe(true);
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflow('wf-123');
|
||||
expect(mockApiGet).toHaveBeenCalledWith('/workflows/wf-123');
|
||||
});
|
||||
|
||||
it('is disabled when id is undefined', async () => {
|
||||
const { useWorkflow } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ enabled }: any) => {
|
||||
expect(enabled).toBe(false);
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflow(undefined);
|
||||
expect(mockApiGet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateWorkflow', () => {
|
||||
it('calls apiPost with correct endpoint and data', async () => {
|
||||
const { useCreateWorkflow } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useCreateWorkflow();
|
||||
const payload = {
|
||||
name: 'Test WF',
|
||||
steps: [{ name: 'S1', type: 'action' as const, config: {} }],
|
||||
};
|
||||
mockApiPost.mockResolvedValue({ id: 'wf-1', ...payload });
|
||||
await mutateAsync(payload);
|
||||
expect(mockApiPost).toHaveBeenCalledWith('/workflows', payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateWorkflow', () => {
|
||||
it('calls apiPatch with correct endpoint and data', async () => {
|
||||
const { useUpdateWorkflow } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useUpdateWorkflow();
|
||||
const payload = { id: 'wf-1', data: { name: 'Updated' } };
|
||||
mockApiPatch.mockResolvedValue({ id: 'wf-1', name: 'Updated' });
|
||||
await mutateAsync(payload);
|
||||
expect(mockApiPatch).toHaveBeenCalledWith('/workflows/wf-1', {
|
||||
name: 'Updated',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteWorkflow', () => {
|
||||
it('calls apiDelete with correct endpoint', async () => {
|
||||
const { useDeleteWorkflow } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useDeleteWorkflow();
|
||||
mockApiDelete.mockResolvedValue(undefined);
|
||||
await mutateAsync('wf-1');
|
||||
expect(mockApiDelete).toHaveBeenCalledWith('/workflows/wf-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkflowInstances', () => {
|
||||
it('calls apiGet with instances endpoint', async () => {
|
||||
const { useWorkflowInstances } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryFn }: any) => {
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflowInstances();
|
||||
expect(mockApiGet).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/workflows/instances?')
|
||||
);
|
||||
});
|
||||
|
||||
it('passes status filter when provided', async () => {
|
||||
const { useWorkflowInstances } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryFn }: any) => {
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflowInstances(1, 20, 'completed');
|
||||
expect(mockApiGet).toHaveBeenCalledWith(
|
||||
expect.stringContaining('status=completed')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkflowInstance', () => {
|
||||
it('calls apiGet with instance detail endpoint', async () => {
|
||||
const { useWorkflowInstance } = await import('../workflows');
|
||||
const { useQuery } = await import('@tanstack/react-query');
|
||||
|
||||
(useQuery as any).mockImplementation(({ queryFn, enabled }: any) => {
|
||||
expect(enabled).toBe(true);
|
||||
queryFn();
|
||||
return { data: undefined, isLoading: false };
|
||||
});
|
||||
|
||||
useWorkflowInstance('inst-1');
|
||||
expect(mockApiGet).toHaveBeenCalledWith(
|
||||
'/workflows/instances/inst-1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateWorkflowInstance', () => {
|
||||
it('calls apiPost with correct endpoint and data', async () => {
|
||||
const { useCreateWorkflowInstance } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useCreateWorkflowInstance();
|
||||
const payload = {
|
||||
workflowId: 'wf-1',
|
||||
data: { context: { foo: 'bar' } },
|
||||
};
|
||||
mockApiPost.mockResolvedValue({ id: 'inst-1' });
|
||||
await mutateAsync(payload);
|
||||
expect(mockApiPost).toHaveBeenCalledWith(
|
||||
'/workflows/wf-1/instances',
|
||||
{ context: { foo: 'bar' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAdvanceWorkflowInstance', () => {
|
||||
it('calls apiPost with advance endpoint and decision', async () => {
|
||||
const { useAdvanceWorkflowInstance } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useAdvanceWorkflowInstance();
|
||||
const payload = {
|
||||
instanceId: 'inst-1',
|
||||
data: { decision: 'approve' as const, comment: 'LGTM' },
|
||||
};
|
||||
mockApiPost.mockResolvedValue({ id: 'inst-1', status: 'in_progress' });
|
||||
await mutateAsync(payload);
|
||||
expect(mockApiPost).toHaveBeenCalledWith(
|
||||
'/workflows/instances/inst-1/advance',
|
||||
{ decision: 'approve', comment: 'LGTM' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCancelWorkflowInstance', () => {
|
||||
it('calls apiPost with cancel endpoint', async () => {
|
||||
const { useCancelWorkflowInstance } = await import('../workflows');
|
||||
const { useMutation } = await import('@tanstack/react-query');
|
||||
|
||||
(useMutation as any).mockImplementation(({ mutationFn, onSuccess }: any) => ({
|
||||
mutateAsync: async (args: any) => {
|
||||
const result = await mutationFn(args);
|
||||
if (onSuccess) onSuccess(result, args);
|
||||
return result;
|
||||
},
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
const { mutateAsync } = useCancelWorkflowInstance();
|
||||
mockApiPost.mockResolvedValue({ id: 'inst-1', status: 'cancelled' });
|
||||
await mutateAsync('inst-1');
|
||||
expect(mockApiPost).toHaveBeenCalledWith(
|
||||
'/workflows/instances/inst-1/cancel'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user