setupCraftServiceTestingByRegister
Sets up a craftService or toCraftService from an explicit flat register derived from the full dependency graph.
Import
import {
setupCraftComponentTestingByRegister,
setupCraftServiceTestingByRegister,
} from '@craft-ng/core';Introduction
setupCraftServiceTestingByRegister is the exhaustive testing utility for the craftService graph.
Instead of providing only the overrides you care about, you provide a full typed register where each service is marked as:
- real
- provided by its raw
provideX(...) - mocked with a raw object
- or pruned with
'notReached'
This is useful when you want explicit control over every node in the service graph.
For tests that should stay close to reality, use boundaryOnly. It keeps the application graph real by default and only lets you decide the services marked with browserBoundary: true.
Register Workflow
The intended workflow is:
- start from the full dependency graph of the SUT
- fill each key with a real provider,
'real', a mock object, or'notReached' - pass the register to
await setupCraftServiceTestingByRegister(...)
Basic Example
import {
craftService,
setupCraftServiceTestingByRegister,
state,
} from '@craft-ng/core';
import { vi } from 'vitest';
const { CounterToYield } = craftService(
{ name: 'Counter', scope: 'global' },
() =>
state(10, ({ update }) => ({
increment: () => update((value) => value + 1),
})),
);
const { injectCounterConsumer, provideCounterConsumer } = craftService(
{ name: 'CounterConsumer', scope: 'toProvide' },
function* () {
const counter = yield* CounterToYield();
return {
read: () => counter(),
increment: () => counter.increment(),
};
},
);
const { sut, mocks } = await setupCraftServiceTestingByRegister(
injectCounterConsumer,
{
CounterConsumer: provideCounterConsumer(),
Counter: {
$self: vi.fn(() => 41),
increment: vi.fn(),
},
},
);
expect(sut.read()).toBe(41);
sut.increment();
expect(mocks.Counter.increment).toHaveBeenCalledTimes(1);Register Semantics
'real'
Use 'real' for reachable non-provider scopes such as global or function.
await setupCraftServiceTestingByRegister(injectCounterConsumer, {
CounterConsumer: provideCounterConsumer(),
Counter: 'real',
});Raw provider
Use the provider returned by provideX(...) for toProvide or manuallyProvidedAtRoot services.
await setupCraftServiceTestingByRegister(injectRootCounter, {
RootCounter: provideRootCounter(),
ParentCounter: provideParentCounter(),
ChildCounter: provideChildCounter(),
});Raw mock object
Use a plain object when you want to override the public service shape.
await setupCraftServiceTestingByRegister(injectCounterConsumer, {
CounterConsumer: provideCounterConsumer(),
Counter: {
$self: vi.fn(() => 12),
increment: vi.fn(),
},
});'notReached'
Use 'notReached' only when the service is on a branch fully pruned by an ancestor mock.
await setupCraftServiceTestingByRegister(injectRootCounter, {
RootCounter: provideRootCounter(),
ParentCounter: {
incrementParent: vi.fn(),
},
ChildCounter: 'notReached',
});Return Value
The function resolves to:
sut: the resolved service under testmocks: only the services that were actually mocked in the register
Entries marked as 'real', 'notReached', or provided through raw providers are not exposed in mocks.
App Start Hooks
Reachable real services declared with appStart: true must be acknowledged explicitly.
const { sut } = await setupCraftServiceTestingByRegister(
injectDashboard,
{
Dashboard: provideDashboard(),
AuthSession: 'real',
Analytics: 'real',
},
{
appStart: {
AuthSession: 'run',
Analytics: 'ignore',
},
},
);'run' injects the real service and awaits its onAppStart(...) hook. 'ignore' documents that the test intentionally skips it. Mocked services and 'notReached' branches do not require appStart entries.
Boundary-Only Mode
setupCraftServiceTestingByRegister.boundaryOnly(...) is the recommended mode when the test should mock only browser or platform edges.
const { sut, mocks } = await setupCraftServiceTestingByRegister.boundaryOnly(
injectDashboard,
{
toProvideRegister: {
Dashboard: provideDashboard(),
FeatureConfig: provideFeatureConfig({ env: 'test' }),
},
boundaryRegister: {
LocalStorageService: {
getItem: vi.fn(() => 'cached'),
},
ConsoleService: 'real',
},
appStart: {
AuthSession: 'run',
},
},
);toProvideRegistercontains real providers required by reachable services.boundaryRegistercontains the explicit decision for each reachable browser boundary.- non-boundary services cannot be mocked in this mode.
- descendants of a mocked boundary are pruned and do not need entries.
The component helper exposes the same mode:
const { fixture, component, mocks } =
await setupCraftComponentTestingByRegister.boundaryOnly(
DashboardPage,
{} as GenDeps_DashboardPage,
{
boundaryRegister: {
BrowserWindowService: 'real',
LocalStorageService: {
getItem: vi.fn(() => 'cached'),
},
},
inputs: {
userId: '42',
},
},
);The helper never decides automatically from jsdom or another test environment. Use 'real' when the real boundary is appropriate, and provide a mock when the test needs deterministic platform behavior.
Extra Angular Providers
When the graph depends on real Angular infrastructure, append providers through the third argument.
const { sut } = await setupCraftServiceTestingByRegister(
injectNavigation,
register,
{
providers: [provideRouter([])],
},
);Alias
setupTestingService is a backward-compatible alias of setupCraftServiceTestingByRegister.