Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AllianceBioversityCIAT/alliance-research-indicators-client/llms.txt

Use this file to discover all available pages before exploring further.

The Alliance Research Indicators client uses Jest with jest-preset-angular for all unit tests. Spec files are co-located with the subject file, shared fixtures live in src/app/testing/, and a project-wide coverage floor is enforced by jest.config.ts on every CI run. This page explains the test setup, how to run tests, where fixtures come from, and what to test for each code category.

Test environment

Tests run in a jsdom environment using the jest-preset-angular preset, which bridges the Angular compilation pipeline (via ts-jest / Babel) with Jest’s test runner. The bootstrapping file at src/setup-jest.ts is loaded after the framework before every suite.
jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'jest-preset-angular',
  setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
  globalSetup: 'jest-preset-angular/global-setup',
  testEnvironment: 'jsdom',
  testEnvironmentOptions: {
    customExportConditions: ['node', 'node-addons']
  },
  transform: {
    '^.+\\.(ts|js|html|svg)$': [
      'jest-preset-angular',
      {
        tsconfig: '<rootDir>/tsconfig.spec.json',
        stringifyContentPathRegex: '\\.html$',
        isolatedModules: true
      }
    ]
  },
  transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
  // ...
};

Path aliases in tests

All TypeScript path aliases declared in tsconfig.json are mirrored in jest.config.ts under moduleNameMapper. Use the aliases — never use relative ../../.. imports in spec files.
AliasResolves to
@shared/*src/app/shared/*
@services/*src/app/shared/services/*
@interfaces/*src/app/shared/interfaces/*
@components/*src/app/shared/components/*
@pages/*src/app/pages/*
@guards/*src/app/shared/guards/*
@interceptors/*src/app/shared/interceptors/*
@envs/*src/environments/*
@utils/*src/app/shared/utils/*
SCSS imports are stubbed via identity-obj-proxy and the @microsoft/clarity SDK is mocked at tests/mocks/clarityMock.ts.

Running tests

All commands run from the research-indicators/ directory.
npm run test
Coverage reporters produce text, text-summary, cobertura, and lcov output. The lcov report is consumed by SonarCloud in the sonarcloud-analysis.yml CI workflow.

Coverage floors

The following project-wide thresholds are enforced by jest.config.ts. If any threshold is breached the test run exits with a non-zero code and CI fails.
jest.config.ts (coverage thresholds)
coverageThreshold: {
  global: {
    statements: 40,
    branches:   20,
    lines:      45,
    functions:  30
  }
}
MetricFloor
Statements40%
Branches20%
Lines45%
Functions30%
Never push changes that regress any of these metrics. Run npm run test:coverage locally before opening a pull request and compare the summary against the floors above.

What is excluded from coverage

The following files are excluded from coverage measurement by design. Do not add new exclusions without a documented reason.
jest.config.ts (coverage exclusions)
coveragePathIgnorePatterns: [
  '<rootDir>/src/app/app.config.ts',
  '<rootDir>/src/app/app.routes.ts',
  '<rootDir>/src/app/shared/sockets/websocket.service.ts',
  '<rootDir>/src/app/shared/components/alert/alert.component.ts'
]
app.config.ts and app.routes.ts are framework bootstrap/configuration files with no testable logic. websocket.service.ts depends on a live socket connection. alert.component.ts is a thin display wrapper with no branching logic. Coverage is collected from ./src/app/**/*.ts and ./src/app/**/*.html, excluding routing and module files.

Where spec files live

Co-locate *.spec.ts files next to the subject file. This keeps test files discoverable and prevents them from drifting when the subject file is moved.
src/app/shared/services/
├── api.service.ts
├── api.service.spec.ts        ← co-located
├── cache/
│   ├── cache.service.ts
│   └── cache.service.spec.ts  ← co-located
The spec file for alert.component.ts is excluded from the test run via testPathIgnorePatterns in jest.config.ts.

Shared fixtures in src/app/testing/

All shared mocks and test fixtures live in src/app/testing/mock-services.mock.ts. Always import from here — never re-mock the same service in individual spec files. Duplicating mocks creates drift and makes refactors painful. The file exports pre-built mock objects for every major injectable:
src/app/testing/mock-services.mock.ts (excerpt)
export const cacheServiceMock = { ... } as unknown as CacheService;
export const actionsServiceMock = { ... } as unknown as ActionsService;
export const apiServiceMock = { ... } as unknown as jest.Mocked<ApiService>;
export const httpClientMock = { ... } as unknown as HttpClient;
export const routerMock = { navigate: jest.fn().mockResolvedValue(true), ... };
export const routeMock = { snapshot: { ... } };
export const submissionServiceMock = { ... } as unknown as SubmissionService;
It also exports typed mock data objects that carry the full MainResponse<T> shape:
export const mockLatestResults = {
  status: 200,
  description: 'Success',
  successfulRequest: true,
  errorDetail: { errors: '', detail: '', description: '' },
  data: [ /* typed result objects */ ]
};

export const mockInstitutions  = { status: 200, successfulRequest: true, data: [ ... ] };
export const mockResults       = { status: 200, successfulRequest: true, data: { results: [...], total: 2 } };
export const mockLanguages     = { status: 200, successfulRequest: true, data: [ ... ] };
// ...and more
When you need a fixture that does not yet exist in mock-services.mock.ts, add it there — do not create a local constant in your spec file. This keeps the shared library complete and reusable.

What to test

Service tests

Services that delegate HTTP calls through ApiService must be tested with HttpTestingController. Always assert on the full MainResponse<T> envelope — the status, successfulRequest, and data fields — not just the inner data.
src/app/shared/services/example.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { ExampleService } from '@services/example.service';

describe('ExampleService', () => {
  let service: ExampleService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ExampleService, provideHttpClient(), provideHttpClientTesting()]
    });
    service = TestBed.inject(ExampleService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());

  it('should return a MainResponse<T> envelope on success', async () => {
    const promise = service.getResults();

    const req = httpMock.expectOne('/api/results');
    req.flush({ status: 200, successfulRequest: true, data: [] });

    const response = await promise;
    expect(response.successfulRequest).toBe(true);
    expect(Array.isArray(response.data)).toBe(true);
  });
});
Also cover the 409 conflict flow: assert that your service surfaces the conflict through the ActionsService link-to-existing modal, not by silently retrying.

Component tests

Component specs must cover:
  • Role-conditional rendering — assert that elements visible to one role are absent for another. Use the cacheServiceMock with its dataCache signal to swap the simulated role.
  • Signal-driven state transitions — update a writable signal and assert the rendered DOM reflects the new value.
  • Form validity — set invalid values on reactive form controls and assert that the submit button is disabled and error messages appear.
  • Error surfaces — trigger an error path and assert that the appropriate toast or inline message is shown.
src/app/pages/example/example.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { ComponentFixture } from '@angular/core/testing';
import { ExampleComponent } from './example.component';
import { CacheService } from '@services/cache/cache.service';
import { cacheServiceMock } from 'src/app/testing/mock-services.mock';

describe('ExampleComponent', () => {
  let fixture: ComponentFixture<ExampleComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ExampleComponent],
      providers: [{ provide: CacheService, useValue: cacheServiceMock }]
    }).compileComponents();

    fixture = TestBed.createComponent(ExampleComponent);
    fixture.detectChanges();
  });

  it('should show the edit button for an admin role', () => {
    cacheServiceMock.dataCache.set({
      ...cacheServiceMock.dataCache(),
      user: { ...cacheServiceMock.dataCache().user, roleName: 'Admin' }
    });
    fixture.detectChanges();
    const btn = fixture.nativeElement.querySelector('[data-testid="edit-btn"]');
    expect(btn).not.toBeNull();
  });

  it('should hide the edit button for a read-only role', () => {
    cacheServiceMock.dataCache.set({
      ...cacheServiceMock.dataCache(),
      user: { ...cacheServiceMock.dataCache().user, roleName: 'Viewer' }
    });
    fixture.detectChanges();
    const btn = fixture.nativeElement.querySelector('[data-testid="edit-btn"]');
    expect(btn).toBeNull();
  });
});

Interceptor tests

For interceptors, verify:
  • Token attachment (jWtInterceptor — assert the Authorization header is set).
  • HTTP error surfacing (httpErrorInterceptor — assert toast/alert dispatch via ActionsService).
  • 409 conflict flow — assert the link-to-existing modal is opened, not a blind retry.

Guard tests

For rolesGuard and centerAdminGuard, test both the authorized and unauthorized path. Use routerMock.navigate to assert the redirect destination when the guard blocks access.

Test patterns at a glance

Code typeKey assertions
ServiceMainResponse<T> envelope shape; 409 conflict flow; HttpTestingController.verify()
ComponentRole-conditional element presence; signal → DOM; form validity; error surface
GuardAuthorized path returns true; unauthorized path calls router.navigate
InterceptorHeader injection; error toast/alert dispatch; 409 modal trigger
PipePure transformation with edge cases (null, empty string, boundary values)

Build docs developers (and LLMs) love