Atlas Forms
Testing Custom Actions
Testing action handlers is straightforward because they are plain async functions. Mock FormActionContext to control the inputs, then assert on the mock's calls. No special test setup or DOM environment required for pure handlers.
Creating a Mock Context
// test/helpers/mockContext.ts
import type { FormActionContext } from '@atlas-forms/types-js';
import { vi } from 'vitest';
export function createMockContext(
overrides?: Partial<FormActionContext>
): FormActionContext {
return {
formId: 'test-form-123',
tenantId: 'tenant-42',
userId: 'user-99',
formValues: {},
formEngine: {
setFieldValue: vi.fn(),
getValues: vi.fn().mockReturnValue({}),
getFieldError: vi.fn().mockReturnValue(null),
} as any,
apiClient: {
getFormById: vi.fn(),
listForms: vi.fn(),
} as any,
navigate: vi.fn(),
complete: vi.fn(),
fail: vi.fn(),
validate: vi.fn().mockResolvedValue(true),
config: {},
...overrides,
};
}
Unit Test Examples
// src/handlers/approveWorkflow.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { approveWorkflowHandler } from './approveWorkflow';
import { createMockContext } from '../../test/helpers/mockContext';
import { approvalService } from '../services/approvalService';
vi.mock('../services/approvalService');
describe('approveWorkflowHandler', () => {
let ctx: ReturnType<typeof createMockContext>;
beforeEach(() => {
ctx = createMockContext({
formValues: {
'workflow-instance-id': 'wf-001',
'approver-notes': 'Looks good'
},
config: { transitionTo: 'approved' }
});
});
it('calls approvalService.approve with correct args', async () => {
vi.mocked(approvalService.approve).mockResolvedValue(undefined);
await approveWorkflowHandler(ctx);
expect(approvalService.approve).toHaveBeenCalledWith({
workflowInstanceId: 'wf-001',
tenantId: 'tenant-42',
notes: 'Looks good',
status: 'approved'
});
});
it('calls ctx.complete() on success', async () => {
vi.mocked(approvalService.approve).mockResolvedValue(undefined);
await approveWorkflowHandler(ctx);
expect(ctx.complete).toHaveBeenCalled();
});
it('calls ctx.fail() when service throws', async () => {
vi.mocked(approvalService.approve).mockRejectedValue(new Error('Network error'));
await approveWorkflowHandler(ctx);
expect(ctx.fail).toHaveBeenCalledWith(expect.stringContaining('Network error'));
});
it('calls ctx.fail() when workflow-instance-id is missing', async () => {
ctx.formValues = {};
await approveWorkflowHandler(ctx);
expect(ctx.fail).toHaveBeenCalled();
expect(approvalService.approve).not.toHaveBeenCalled();
});
});
Test Coverage Requirements
Aim for these coverage targets before publishing:
| Test Type | Required | Covers |
|---|---|---|
| Happy path | Yes | Successful execution, complete() called |
| Missing required values | Yes | Guards that call fail() early |
| Service error | Yes | catch block calls fail(reason) |
| Validation failure | If used | validate() returns false path |
| Config variation | If configurable | Different handlerConfig branches |