@c-a-f/testing
v1.0.2
Published
Testing utilities and helpers for CAF applications. Provides mocks, test helpers, and utilities for testing UseCase, Ploc, Pulse, Workflow, Permission, I18n, and Validation.
Maintainers
Readme
@c-a-f/testing
Testing utilities and helpers for CAF applications. Provides mocks, test helpers, and utilities for testing UseCase, Ploc, Pulse, Workflow, Permission, I18n, and Validation.
Documentation: @c-a-f/testing docs
Installation
npm install @c-a-f/testing --save-devUsage
Core Testing Utilities
Testing Ploc
import { createPlocTester, waitForStateChange } from '@c-a-f/testing/core';
import { Ploc } from '@c-a-f/core';
class CounterPloc extends Ploc<number> {
constructor() {
super(0);
}
increment() {
this.changeState(this.state + 1);
}
}
describe('CounterPloc', () => {
it('tracks state changes', () => {
const ploc = new CounterPloc();
const tester = createPlocTester(ploc);
ploc.increment();
ploc.increment();
expect(tester.getStateChangeCount()).toBe(2);
expect(tester.getStateHistory()).toEqual([0, 1, 2]);
expect(tester.getLastStateChange()).toBe(2);
tester.cleanup();
});
it('waits for state change', async () => {
const ploc = new CounterPloc();
setTimeout(() => ploc.increment(), 100);
const finalState = await waitForStateChange(ploc, (state) => state === 1);
expect(finalState).toBe(1);
});
});Testing Pulse
import { createPulseTester, waitForPulseValue } from '@c-a-f/testing/core';
import { pulse } from '@c-a-f/core';
describe('Pulse', () => {
it('tracks value changes', () => {
const count = pulse(0);
const tester = createPulseTester(count);
count.value = 5;
count.value = 10;
expect(tester.getValueChangeCount()).toBe(2);
expect(tester.getValueHistory()).toEqual([0, 5, 10]);
expect(tester.getLastValueChange()).toBe(10);
tester.cleanup();
});
it('waits for pulse value', async () => {
const count = pulse(0);
setTimeout(() => { count.value = 5; }, 100);
const finalValue = await waitForPulseValue(count, (value) => value === 5);
expect(finalValue).toBe(5);
});
});Testing UseCase
import {
createMockUseCase,
createMockUseCaseSuccess,
createMockUseCaseError,
createUseCaseTester,
createSuccessResult,
} from '@c-a-f/testing/core';
import { UseCase } from '@c-a-f/core';
describe('UseCase', () => {
it('creates and tests mock use case', async () => {
const mockUseCase = createMockUseCase<[], string>(() =>
createSuccessResult('result')
);
const tester = createUseCaseTester(mockUseCase);
const result = await tester.execute([]);
expect(result.data.value).toBe('result');
expect(result.error.value).toBeNull();
});
it('uses success shortcut', async () => {
const useCase = createMockUseCaseSuccess([{ id: '1', name: 'John' }]);
const result = await useCase.execute();
expect(result.data.value).toEqual([{ id: '1', name: 'John' }]);
});
it('uses error shortcut', async () => {
const useCase = createMockUseCaseError(new Error('Failed'));
const result = await useCase.execute();
expect(result.error.value?.message).toBe('Failed');
});
it('executes and gets data', async () => {
const mockUseCase = createMockUseCase<[number], number>((count) =>
createSuccessResult(count * 2)
);
const tester = createUseCaseTester(mockUseCase);
const data = await tester.executeAndGetData([5]);
expect(data).toBe(10);
});
});Mock Ploc and state history (snapshot)
import {
createMockPloc,
createPlocTester,
assertStateHistory,
getStateHistorySnapshot,
} from '@c-a-f/testing/core';
it('mock Ploc and state history', () => {
const ploc = createMockPloc({ count: 0 });
const tester = createPlocTester(ploc);
ploc.changeState({ count: 1 });
ploc.changeState({ count: 2 });
assertStateHistory(tester, [{ count: 0 }, { count: 1 }, { count: 2 }]);
expect(getStateHistorySnapshot(tester)).toMatchSnapshot();
tester.cleanup();
});Mock Repository (domain I*Repository)
import { createMockRepository, createMockRepositoryStub } from '@c-a-f/testing/core';
import type { IUserRepository } from '../domain';
it('mocks repository', async () => {
const repo = createMockRepository<IUserRepository>({
getUsers: async () => [{ id: '1', name: 'John' }],
getUserById: async (id) => ({ id, name: 'User ' + id }),
});
expect(await repo.getUsers()).toHaveLength(1);
expect((await repo.getUserById('2'))?.name).toBe('User 2');
});
it('stub and spy', async () => {
const stub = createMockRepositoryStub<IUserRepository>();
stub.getUsers = vi.fn().mockResolvedValue([]);
await stub.getUsers();
expect(stub.getUsers).toHaveBeenCalledTimes(1);
});Integration test helpers
import {
createPlocUseCaseContext,
flushPromises,
} from '@c-a-f/testing/core';
it('integration context', async () => {
const { ploc, useCase } = createPlocUseCaseContext(
{ items: [], loading: false },
[{ id: '1', name: 'Item' }]
);
// Use with CAFProvider or pass as props
expect(ploc.state.loading).toBe(false);
const result = await useCase.execute();
expect(result.data.value).toHaveLength(1);
await flushPromises();
});React Testing Utilities (@c-a-f/testing/react)
Use these when testing React components that use usePlocFromContext, useUseCaseFromContext, or usePloc / useUseCase with context-provided instances.
Requirements: react and @testing-library/react (peer dependencies). Install in your app:
npm install react @testing-library/reactrenderWithCAF
Render a component wrapped in CAFProvider so Ploc/UseCase context is available:
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithCAF, createTestPloc, mockUseCase } from '@c-a-f/testing/react';
import { usePlocFromContext, usePloc } from '@c-a-f/infrastructure-react';
const Counter = () => {
const ploc = usePlocFromContext('counter');
if (!ploc) return null;
const [state] = usePloc(ploc);
return <span data-testid="count">{state.count}</span>;
};
it('renders with CAF context', () => {
const ploc = createTestPloc({ count: 5 });
renderWithCAF(<Counter />, { plocs: { counter: ploc } });
expect(screen.getByTestId('count')).toHaveTextContent('5');
});createTestPloc
Create a Ploc with controllable state (no business logic). Same idea as createMockPloc from core, for use in React tests:
const ploc = createTestPloc({ count: 0 });
renderWithCAF(<Counter />, { plocs: { counter: ploc } });
ploc.changeState({ count: 1 });
expect(screen.getByTestId('count')).toHaveTextContent('1');waitForPlocState
Wait for a Ploc to reach a state matching a predicate (e.g. after an async update):
const ploc = createTestPloc({ loading: true, items: [] });
renderWithCAF(<List />, { plocs: { list: ploc } });
ploc.changeState({ loading: false, items: [{ id: '1' }] });
await waitForPlocState(ploc, (state) => !state.loading && state.items.length > 0);
expect(screen.getByText('1')).toBeInTheDocument();mockUseCase
Create mock UseCases for context:
const submit = mockUseCase.success({ id: '1' });
const load = mockUseCase.error(new Error('Network error'));
const search = mockUseCase.async([{ id: '1' }], 50); // resolves after 50ms
renderWithCAF(<Form />, { useCases: { submit, load, search } });mockUseCase.success(data)— always returns success withdatamockUseCase.error(error)— always returnserrormockUseCase.async(data, delayMs?)— resolves withdataafter optional delaymockUseCase.fn(implementation)— custom implementation (same ascreateMockUseCasefrom core)
Vue Testing Utilities (@c-a-f/testing/vue)
Use these when testing Vue components that use usePlocFromContext, useUseCaseFromContext, or useCAFContext.
Requirements: vue and @vue/test-utils (peer dependencies). Install in your app:
npm install vue @vue/test-utilsmountWithCAF
Mount a component with CAF context (Plocs/UseCases) so inject works:
import { mount } from '@vue/test-utils';
import { mountWithCAF, createTestPloc, mockUseCase } from '@c-a-f/testing/vue';
import { usePlocFromContext, usePloc } from '@c-a-f/infrastructure-vue';
const Counter = defineComponent({
setup() {
const ploc = usePlocFromContext('counter');
const [state] = usePloc(ploc!);
return () => h('span', { 'data-testid': 'count' }, state.count);
},
});
it('renders with CAF context', () => {
const ploc = createTestPloc({ count: 5 });
const wrapper = mountWithCAF(Counter, { plocs: { counter: ploc } });
expect(wrapper.get('[data-testid="count"]').text()).toBe('5');
});createTestPloc / waitForPlocState / mockUseCase
Same as React: use createTestPloc, waitForPlocState, and mockUseCase with mountWithCAF for Vue component tests.
Angular Testing Utilities (@c-a-f/testing/angular)
Use these when testing Angular components that use injectPlocFromContext, injectUseCaseFromContext, or injectCAFContext.
Requirements: @angular/core and @c-a-f/infrastructure-angular (peer/dependency). Install in your app:
npm install @angular/core @c-a-f/infrastructure-angularprovideTestingCAF
Provide CAF context in TestBed so injection works:
import { TestBed } from '@angular/core/testing';
import { provideTestingCAF, createTestPloc, mockUseCase } from '@c-a-f/testing/angular';
const ploc = createTestPloc({ count: 0 });
await TestBed.configureTestingModule({
imports: [MyComponent],
providers: [
provideTestingCAF({
plocs: { counter: ploc },
useCases: { submit: mockUseCase.success({ id: '1' }) },
}),
],
}).compileComponents();
const fixture = TestBed.createComponent(MyComponent);createTestPloc / waitForPlocState / mockUseCase
Same as React/Vue: use createTestPloc, waitForPlocState, and mockUseCase with provideTestingCAF in your Angular tests.
Testing RouteManager
import { createMockRouteRepository, createRouteManagerTester } from '@c-a-f/testing/core';
import { RouteManager } from '@c-a-f/core';
describe('RouteManager', () => {
it('tracks route changes', async () => {
const mockRepo = createMockRouteRepository();
const routeManager = new RouteManager(mockRepo);
const tester = createRouteManagerTester(routeManager);
await tester.changeRoute('/dashboard');
expect(tester.getCurrentRoute()).toBe('/dashboard');
expect(tester.getRouteHistory()).toContain('/dashboard');
});
});Workflow Testing Utilities
import { createWorkflowTester, waitForWorkflowState } from '@c-a-f/testing/workflow';
import { WorkflowManager, WorkflowDefinition } from '@c-a-f/workflow';
describe('Workflow', () => {
it('tracks workflow state changes', async () => {
const workflow = new WorkflowManager(definition);
const tester = createWorkflowTester(workflow);
await tester.dispatch('approve');
await waitForWorkflowState(workflow, 'approved');
expect(tester.getCurrentState()).toBe('approved');
expect(tester.getStateHistory().length).toBeGreaterThan(1);
tester.cleanup();
});
it('waits for final state', async () => {
const workflow = new WorkflowManager(definition);
await workflow.dispatch('complete');
const finalState = await waitForFinalState(workflow);
expect(finalState.isFinal).toBe(true);
});
});Permission Testing Utilities
import { createMockPermissionChecker, createPermissionTester } from '@c-a-f/testing/permission';
import { PermissionManager } from '@c-a-f/permission';
describe('Permission', () => {
it('tests permission checking', async () => {
const mockChecker = createMockPermissionChecker(['user.edit', 'post.create']);
const manager = new PermissionManager(mockChecker);
const tester = createPermissionTester(manager);
expect(await tester.hasPermission('user.edit')).toBe(true);
expect(await tester.hasPermission('user.delete')).toBe(false);
expect(await tester.hasAnyPermission(['user.edit', 'user.delete'])).toBe(true);
});
it('modifies permissions dynamically', () => {
const mockChecker = createMockPermissionChecker(['user.view']);
mockChecker.addPermission('user.edit');
expect(mockChecker.check('user.edit').granted).toBe(true);
mockChecker.removePermission('user.view');
expect(mockChecker.check('user.view').granted).toBe(false);
});
});I18n Testing Utilities
import { createMockTranslator, createTranslationTester } from '@c-a-f/testing/i18n';
import { TranslationManager } from '@c-a-f/i18n';
describe('I18n', () => {
it('tests translation', () => {
const mockTranslator = createMockTranslator({
en: { greeting: 'Hello', welcome: 'Welcome {{name}}' },
fa: { greeting: 'سلام', welcome: 'خوش آمدید {{name}}' },
});
const manager = new TranslationManager(mockTranslator);
const tester = createTranslationTester(manager);
expect(tester.t('greeting')).toBe('Hello');
expect(tester.translateWithValues('welcome', { name: 'John' })).toBe('Welcome John');
});
it('tests language switching', async () => {
const mockTranslator = createMockTranslator({
en: { greeting: 'Hello' },
fa: { greeting: 'سلام' },
});
const manager = new TranslationManager(mockTranslator);
const tester = createTranslationTester(manager);
expect(tester.getCurrentLanguage()).toBe('en');
await tester.changeLanguage('fa');
expect(tester.getCurrentLanguage()).toBe('fa');
expect(tester.t('greeting')).toBe('سلام');
});
});Validation Testing Utilities
import { createMockValidator, createValidationTester } from '@c-a-f/testing/validation';
describe('Validation', () => {
it('tests validation', async () => {
const mockValidator = createMockValidator((data: any) => {
return data.email && data.email.includes('@');
});
const tester = createValidationTester(mockValidator);
const successResult = await tester.expectSuccess({ email: '[email protected]' });
expect(successResult.email).toBe('[email protected]');
const errors = await tester.expectFailure({ email: 'invalid' });
expect(errors.length).toBeGreaterThan(0);
});
});Exports
Core Testing (@c-a-f/testing/core)
PlocTester— Tester for Ploc instancescreatePlocTester— Create a Ploc testercreateMockPloc— Create a Ploc with controllable state (no logic)MockPloc— Concrete Ploc class for testswaitForStateChange— Wait for state change matching predicatewaitForStateChanges— Wait for specific number of state changesassertStateHistory— Assert state history matches expected (snapshot-style)getStateHistorySnapshot/getStateHistorySnapshotJson— Snapshot state historyPulseTester— Tester for Pulse instancescreatePulseTester— Create a Pulse testerwaitForPulseValue— Wait for pulse value matching predicateMockUseCase— Mock UseCase implementationcreateMockUseCase— Create a mock UseCasecreateMockUseCaseSuccess— Mock UseCase that returns success with datacreateMockUseCaseError— Mock UseCase that returns an errorcreateMockUseCaseAsync— Mock UseCase that resolves after optional delayUseCaseTester— Tester for UseCase instancescreateUseCaseTester— Create a UseCase testercreateSuccessResult— Create successful RequestResultcreateErrorResult— Create failed RequestResultcreateLoadingResult— Create loading RequestResultcreateMockRepository— Generic stub for domain I*Repository interfacescreateMockRepositoryStub— Empty repository stub (assign or spy methods)flushPromises— Resolve pending microtasks in integration testsrunWithFakeTimers— Placeholder for running code with fake timerscreatePlocUseCaseContext— Minimal Ploc + UseCase context for integration testsMockRouteRepository— Mock RouteRepository implementationcreateMockRouteRepository— Create a mock RouteRepositoryRouteManagerTester— Tester for RouteManager instancescreateRouteManagerTester— Create a RouteManager tester
Workflow Testing (@c-a-f/testing/workflow)
WorkflowTester— Tester for WorkflowManager instancescreateWorkflowTester— Create a Workflow testerwaitForWorkflowState— Wait for workflow to reach specific statewaitForFinalState— Wait for workflow to reach final state
Permission Testing (@c-a-f/testing/permission)
MockPermissionChecker— Mock PermissionChecker implementationcreateMockPermissionChecker— Create a mock PermissionCheckerPermissionTester— Tester for PermissionManager instancescreatePermissionTester— Create a Permission tester
I18n Testing (@c-a-f/testing/i18n)
MockTranslator— Mock Translator implementationcreateMockTranslator— Create a mock TranslatorTranslationTester— Tester for TranslationManager instancescreateTranslationTester— Create a Translation tester
Validation Testing (@c-a-f/testing/validation)
MockValidator— Mock Validator implementationcreateMockValidator— Create a mock ValidatorValidationTester— Tester for Validator instancescreateValidationTester— Create a Validation testerexpectSuccess— Validate and expect successexpectFailure— Validate and expect failure
React Testing (@c-a-f/testing/react)
renderWithCAF— Render with CAFProvider (Ploc/UseCase context)RenderWithCAFOptions— Options for renderWithCAF (plocs, useCases, and RTL options)createTestPloc— Create a test Ploc with controllable statewaitForPlocState— Wait for Ploc state to match a predicatemockUseCase— Mock UseCase helpers:success,error,async,fn
Vue Testing (@c-a-f/testing/vue)
mountWithCAF— Mount with CAF context (Ploc/UseCase provide)MountWithCAFOptions— Options for mountWithCAF (plocs, useCases, and Vue Test Utils options)createTestPloc— Create a test Ploc with controllable statewaitForPlocState— Wait for Ploc state to match a predicatemockUseCase— Mock UseCase helpers:success,error,async,fn
Angular Testing (@c-a-f/testing/angular)
provideTestingCAF— Provide CAF context for TestBed (plocs, useCases)ProvideTestingCAFConfig— Config for provideTestingCAFcreateTestPloc— Create a test Ploc with controllable statewaitForPlocState— Wait for Ploc state to match a predicatemockUseCase— Mock UseCase helpers:success,error,async,fn
Dependencies
@c-a-f/core— Core primitives@c-a-f/infrastructure-react— React provider (for@c-a-f/testing/react)@c-a-f/infrastructure-vue— Vue provider (for@c-a-f/testing/vue)@c-a-f/infrastructure-angular— Angular provider (for@c-a-f/testing/angular)@c-a-f/workflow— Workflow package (for workflow testing utilities)@c-a-f/permission— Permission package (for permission testing utilities)@c-a-f/i18n— I18n package (for i18n testing utilities)@c-a-f/validation— Validation package (for validation testing utilities)
Framework testing: Optional peer dependencies per entry point:
@c-a-f/testing/react:react,@testing-library/react@c-a-f/testing/vue:vue,@vue/test-utils@c-a-f/testing/angular:@angular/core
Dev Dependencies
vitest— Testing framework (optional, works with any testing framework)
License
MIT
