| name | testing-patterns |
| description | Testing patterns for the Nuclia frontend monorepo — covering Jest + jest-preset-angular for Angular apps/libs and Vitest for search-widget (Svelte 5) and rao-widget (React 19). Use this skill whenever you are writing or modifying a spec file, debugging a failing test, setting up a new TestBed configuration, mocking Angular services, working with OnPush components in tests, or using Vitest with Svelte/React. Do not wait to be asked about "testing patterns" specifically — any task involving *.spec.ts files, ng-mocks, TestBed, vitest, or fixture.detectChanges() should activate this skill.
|
Testing Patterns — Nuclia Frontend Monorepo
When to Write Tests
Tests are not auto-generated after feature work. The user triggers test writing manually
when they decide the feature is stable enough. Agents must follow these rules:
- Never auto-generate tests as part of a feature build — unless the user explicitly says
"write tests", "add tests", or similar.
- Do not offer to write tests at the end of a feature implementation. The user will ask
when ready.
- When the user does ask for tests, use the
test-writer agent or follow the patterns below.
Two separate test stacks live in this repo. Match the stack to the project:
| Projects | Runner | Config |
|---|
Angular apps & libs (dashboard, rao, manager-v2, nucliadb-admin, core, common, sistema, user, sync, pastanaga-angular) | Jest 30 + jest-preset-angular | jest.config.js / jest.config.ts per project |
libs/search-widget (Svelte 5) | Vitest 4 | libs/search-widget/vite.config.mjs |
libs/rao-widget (React 19) | Vitest 4 | libs/rao-widget/vite.config.ts |
Run with nx test <project-name> in all cases.
Angular Testing (Jest + jest-preset-angular)
Global setup
Each Angular project's jest.config.js points to a test-setup.ts:
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();
This bootstraps Zone.js for Angular's async model. Never remove or replace it.
The workspace-level jest.preset.js also maps d3 to a stub (test/d3.js) — you don't need to mock d3 yourself.
Component test skeleton (standalone)
New standalone components use imports: in TestBed, not declarations:.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockModule, MockProvider } from 'ng-mocks';
import { TranslateModule } from '@ngx-translate/core';
import { MyFeatureComponent } from './my-feature.component';
import { SomeService } from './some.service';
import { of } from 'rxjs';
describe('MyFeatureComponent', () => {
let component: MyFeatureComponent;
let fixture: ComponentFixture<MyFeatureComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MyFeatureComponent,
MockModule(TranslateModule),
],
providers: [MockProvider(SomeService, { items$: of([]) })],
}).compileComponents();
fixture = TestBed.createComponent(MyFeatureComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Legacy non-standalone components still use declarations: [MyComponent] alongside imports: [MockModule(...)].
Setting signal inputs in tests
Angular 17+ signal inputs (input() / input.required()) cannot be set by direct property assignment. Use fixture.componentRef.setInput():
fixture.componentRef.setInput('itemId', 'abc-123');
fixture.detectChanges();
component.itemId = 'abc-123';
For input.required<T>() you must call setInput before the first detectChanges(), otherwise Angular throws a required-input error.
OnPush + fixture.detectChanges() gotchas
ChangeDetectionStrategy.OnPush (used on every component) suppresses automatic re-renders. You must manually trigger change detection after any state mutation:
it('shows updated label', () => {
fixture.componentRef.setInput('label', 'New Label');
fixture.detectChanges();
const el = fixture.nativeElement.querySelector('[data-cy="label"]');
expect(el.textContent).toBe('New Label');
});
For timer-based or async work:
import { fakeAsync, tick } from '@angular/core/testing';
it('debounces search', fakeAsync(() => {
component.searchTerm.set('hello');
fixture.detectChanges();
tick(300);
fixture.detectChanges();
expect(component.results().length).toBeGreaterThan(0);
}));
Mocking services with ng-mocks
The entire codebase uses ng-mocks for test doubles. Key helpers:
| Helper | Usage |
|---|
MockProvider(Service, overrides) | Provide a mock service with specific method/property overrides |
MockModule(SomeModule) | Replace an NgModule with empty mocks of all its declarations |
MockComponent(SomeComponent) | Replace a specific standalone component with a stub |
providers: [
MockProvider(SDKService, {
currentKb: of(mockKb),
nuclia: { options: { standalone: false }, db: {} },
}),
MockProvider(SisToastService, {
error: jest.fn(),
success: jest.fn(),
}),
{
provide: ZoneService,
useValue: {
getZones: () => of([{ slug: 'eu', title: 'Europe' }]),
},
},
];
Never import the real HttpClientModule or call real network in unit tests; use MockProvider with stub return values instead.
Service testing (no component)
When testing a service in isolation, skip compileComponents() and inject via TestBed.inject():
import { TestBed, waitForAsync } from '@angular/core/testing';
import { MockProvider } from 'ng-mocks';
import { skip, take } from 'rxjs';
describe('SomeService', () => {
let service: SomeService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MockProvider(SDKService, { currentKb: of(mockKb) })],
});
service = TestBed.inject(SomeService);
});
it('emits updated state after action', waitForAsync(() => {
service.items$.pipe(skip(1), take(1)).subscribe((items) => {
expect(items).toEqual([{ id: '1' }]);
});
service.loadItems();
}));
});
Key patterns:
skip(1) + take(1) to assert on the next emission from a BehaviorSubject
// @ts-ignore access to private member when seeding internal state directly
jest.fn() for methods that should be called (not vi.fn() — that's Vitest)
Spectator (pastanaga-angular only)
The libs/pastanaga-angular lib uses @ngneat/spectator/jest with createHostFactory for host-component-based tests. Don't introduce Spectator in app code — it's only for that lib.
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
const createHost = createHostFactory({
component: TabsListComponent,
declarations: [TabsListComponent, MockComponent(TabItemComponent)],
detectChanges: false,
});
beforeEach(() => {
spectator = createHost(`<pa-tabs>...</pa-tabs>`);
component = spectator.component;
});
Translate in tests
For components that use TranslateModule, use either:
MockModule(TranslateModule) — all translations return the key string. Simple and fast.
TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: () => ({ getTranslation: () => of(EN) }) } }) — loads real translations. Use only when the test asserts on translated text.
Vitest Testing (search-widget / rao-widget)
Environment & globals
Both Vitest projects run with:
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.ts'],
}
Use vi.fn() for mocks (not jest.fn()).
Svelte 5 component tests (search-widget)
Svelte 5 uses mount / unmount from 'svelte'. Always unmount after each test to prevent DOM leaks:
import { mount, unmount } from 'svelte';
import { vi } from 'vitest';
import { Button } from './';
describe('Button', () => {
it('renders with correct class', () => {
const component = mount(Button, {
target: document.body,
props: { size: 'small' },
});
expect(document.body.querySelector('button')?.classList.value).toContain('small');
unmount(component);
});
it('dispatches close event', () => {
const mock = vi.fn();
const component = mount(Modal, {
target: document.body,
props: { show: true },
events: { close: mock },
});
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(mock).toHaveBeenCalled();
unmount(component);
});
});
Key points:
events: { eventName: handler } in mount() binds DOM/custom events
- Direct DOM queries (
document.querySelector) are the primary assertion mechanism
- Stores (e.g.,
searchQuery, typeAhead) can be set directly in tests to seed state
firstValueFrom(store$) to await a store value asynchronously
React component tests (rao-widget)
Use React Testing Library (@testing-library/react) when tests exist. The rao-widget currently has no spec files — if you add them, follow this pattern:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
describe('MyWidget', () => {
it('shows content', () => {
render(<MyWidget label="Hello" />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});
Common Pitfalls
| Symptom | Cause | Fix |
|---|
NullInjectorError: No provider for X | Forgot to mock a transitive dependency | Add MockProvider(X) to providers |
| Template renders stale data | OnPush not re-checked | Call fixture.detectChanges() after mutation |
ExpressionChangedAfterItHasBeenCheckedError | State mutated after CD | Move initialization to ngOnInit or use signals |
| Signal input throws required input error | setInput called after first detectChanges | Set all required signal inputs before first detectChanges |
Cannot read property 'subscribe' of undefined | MockProvider missing an Observable property | Pass { myObs$: of(value) } as the override |
ERROR: 'describe' is not defined in Vitest | globals: true missing in vite config | Already set in both widget configs — check you're running nx test, not vitest directly |