Testing

What exists

Suite Status Detail

Unit tests

Maintained

Karma + Jasmine, ~400 spec files, run by npm run test-single and by buildspec-ci.yml.

E2E tests

Not maintained

Cypress 7.x, 10 spec files. Marked NOT MAINTAINED in capitals in the repository README.

The deploy buildspec has npm run test:ci commented out — only buildspec-ci.yml runs the unit suite, as a separate pipeline. A deployed build has not necessarily had its tests run. See build and deployment.

Running unit tests

Command Use

npm test

Watch mode, for interactive development.

npm run test-single

One run. Prefer this in CI and agent loops.

npm run test:ci

Headless with coverage and reports (ChromeHeadlessNoSandbox, --code-coverage).

npm run test-double

ng lint then a sourcemap-free single run.

The repository README says test-single "will also run linting test". It does not — that is test-double. Run npm run lint separately.

Karma configuration

karma.conf.js:

process.env.CHROME_BIN = require('puppeteer').executablePath();   // Chrome comes from Puppeteer

frameworks: ['jasmine', '@angular-devkit/build-angular']
browsers:   ['ChromeHeadlessNoSandbox']
client: { jasmine: { random: false }, clearContext: false, args: ['--sourcemaps=false'] }
reporters: ['progress', 'coverage', 'junit']
port: 9876
singleRun: false
autoWatch: true

Points that matter:

  • Chrome is Puppeteer’s. No system Chrome needed, but the Puppeteer install must have succeeded.

  • ChromeHeadlessNoSandbox passes --no-sandbox --disable-setuid-sandbox --disable-gpu --disable-translate --disable-extensions --no-proxy-server — required in containers.

  • random: false. Specs run in a deterministic order, which means a spec can accidentally depend on another’s side effects. Given that several reducers touch localStorage at module-evaluation time and cerialize casing is global state, this is not hypothetical — see data modelling.

  • Coverage reporters: html, lcov, text-summary, cobertura, written to coverage/.

  • specReporters is configured with failFast: true, but jasmine-spec-reporter is not in the reporters list, so those options are inert.

src/test.ts

The Karma entry point. It imports the zone.js testing shims, initialises the Angular TestBed, then discovers specs with require.context('./', true, /\.spec\.ts$/) and starts Karma manually (karma.loaded = function () {} prevents a premature start).

So every *.spec.ts under src/ is picked up automatically — there is no manual registry.

Spec idioms

The house pattern uses ng-bullet and ng-mocks:

import { TestBed } from '@angular/core/testing';
import { configureTestSuite } from 'ng-bullet';
import { MockProvider } from 'ng-mocks';

describe('FeatureFlagsService', () => {
  let service: FeatureFlagsService;

  configureTestSuite(() => {
    TestBed.configureTestingModule({
      providers: [FeatureFlagsService, MockProvider(GlobalSettingsService)],
    });
    service = TestBed.inject(FeatureFlagsService);
  });

  it('should be created', () => { … });
});
Tool Why

configureTestSuite (ng-bullet)

Configures the TestBed once per suite instead of per test, which is a large speed win with this many specs. Note the consequence: the module is not rebuilt between tests, so state mutated in one it can leak into the next.

MockProvider / MockComponent / MockModule (ng-mocks)

Auto-mock a dependency without hand-writing a stub.

PybMaterialModule

Import this instead of thirty individual Material modules — that is its stated purpose.

*-mock.service.ts siblings

~30 hand-written doubles, provided by token. Several services have an *-abstract.service.ts base specifically to make this substitution clean — see service abstractions.

*-mock.reducers.ts

Store slice stubs: auth-mock.reducers.ts, crm-mock.reducers.ts, supplier-mock.reducers.ts, utils-mock.reducets.ts (sic), marketplace.reducer.mock.ts.

mocks/ module

Shared test doubles and mock pipes.

ToastrMockService

shared/services/toastr-mock/ — stubs notifications.

Naming convention for test constants

From the repository’s own conventions: production constants are UPPERCASE; constants that exist only for tests or mocks are wrapped in double underscores.

const SOME_PRODUCTION_CONSTANT = 'some value';
const __SOME_MOCKED_CONSTANT__ = 'some value';

feature-flags.service.spec.ts follows it: TEST_DISABLED_FLAGS, TEST_PAYLOAD, TEST_MODULE1.

Testing private members

Existing specs reach into privates via a cast, and use spyOn on private methods:

spyOn(service as any, 'getFeatureFlagsFromEnv').and.returnValue(__TEST_DISABLED_FLAGS__);
(service as any).initFF();
expect((service as any).featureFlags.length).toEqual(0);

Not ideal, but it is the local convention. getFeatureFlagsFromEnv exists as a private method specifically so specs can stub the environment.

What is well tested, and what is not

Area State

Advance calculator

calculator.spec.ts (~560 lines) is the most thorough spec in the repository. If you change a pricing formula or limit, that spec is where it must be proven. See advances.

Feature flags

feature-flags.service.spec.ts and feature-flag.guard.spec.ts cover the layer precedence.

Card model

card.model.spec.ts covers the pure mappers and predicates — the payoff of the interface-plus-functions model style.

Real-time validation

Both the validator and the input factory have specs and mocks.

Auth guard

auth.guard.spec.ts.

Effects

Thinly covered. The large effect classes (SupplierEffects, CrmEffects) have no dedicated specs.

Components

~400 spec files exist for ~420 components, but many are generated "should create" smoke tests rather than behavioural.

Writing a new test

  1. Put it next to the unit under test as <name>.spec.ts. It is discovered automatically.

  2. Use configureTestSuite from ng-bullet.

  3. Mock dependencies with ng-mocks MockProvider, or provide the existing *-mock.service.ts by token.

  4. Import PybMaterialModule rather than individual Material modules.

  5. Prefix test-only constants with … .

  6. Assert behaviour, not that the component exists. A should create test alone does not satisfy the definition of done.

  7. For a reducer, follow calculator.spec.ts: construct the action, run the reducer, assert on the returned state.

  8. Run npm run test-single before pushing — Husky’s pre-push hook runs it anyway.

Because random: false and configureTestSuite share the TestBed across a suite, a spec that writes to localStorage, sessionStorage, or sets cerialize casing should clean up after itself. Order-dependent failures usually trace back to one of those three.

Coverage

npm run test:ci writes to coverage/:

Output Consumer

coverage/lcov-report/**

Human-readable HTML; published as a CodeBuild artifact.

coverage/cobertura-coverage.xml

CodeBuild’s cobertura-report.

TESTS.xml

CodeBuild’s karma-junit-report (JUnit XML). junitReporter.useBrowserName is false, so filenames are stable.

There is no enforced coverage threshold.

Cypress (unmaintained)

The repository README marks the E2E suite NOT MAINTAINED. Treat the specs as documentation of intent rather than a working gate. Do not add a Cypress test as the only proof of a change.

Configuration

cypress.json:

{
  "env": { "LOCAL": false, "ENV_NAME": "staging", "ENV_TYPE": "dev" },
  "chromeWebSecurity": false,
  "reporter": "junit",
  "reporterOptions": { "mochaFile": "cypress/reports/test-report-[hash].xml" },
  "fixturesFolder": false,
  "pluginsFile": false,
  "defaultCommandTimeout": 30000
}
Setting Why

chromeWebSecurity: false

Required because the app embeds EVA in cross-origin iframes.

defaultCommandTimeout: 30000

The app is slow to settle — many screens fan out to several backends.

LOCAL

true targets a locally served app; false targets a deployed environment.

ENV_NAME / ENV_TYPE

Which environment to point at.

QA_TEAM_PASSWORD

Not in the file — it must not be versioned. Retrieve it from Secrets Manager (internal-client-v2/cypress) or put it in a local, untracked cypress.env.json.

Commands

Command Does

npm run cy:open / cy:run

Interactive / headless against the configured environment.

npm run cy:open-local / cy:run-local

Start the app (start-server-and-test on port 4200) then run Cypress with --env LOCAL=true.

npm run cy:run-local-no-watch

As above with npm start — --watch false — the CI variant.

npm run cy:merge-reports

jrm merges cypress/reports/*.xml into merged-test-report.xml (required by CodeBuild).

npm run cy:clear-reports

rimraf cypress/reports.

Note the README refers to cy:clean-reports; the script is cy:clear-reports.

Suite layout

cypress/
├── constants.js          URLS, QA_TEAM_CREDENTIALS, FINAL_ENV, ENV_VAR_KEYS
├── utils.js
├── support/
│   ├── commands.js       custom commands, incl. setupToken()
│   └── index.js
└── integration/
    ├── login.spec.js
    ├── search.spec.js
    ├── crm-notes.spec.js
    ├── marketplaces.spec.js
    ├── eva-menu.spec.js
    ├── awx-disputes.spec.js
    └── general/
        ├── change-password.spec.js
        ├── contact-data-form.spec.js
        ├── supplier-main-bar.spec.js
        └── suspension.spec.js

support/commands.js imports from the application source:

import { AMAZON_SELLER_CENTRAL_MARKETPLACE_KEY } from '../../src/app/suppliers/…/supplier-onboarding.service';
import { AMAZON_SELLER_CENTRAL } from '../../src/app/shared/enums/marketplace.enum';

So renaming those exports breaks the E2E suite silently. It also holds an ffTempStorage = { eva: null, ia: null } for saving and restoring feature flags around a test, and setupToken() authenticates by calling the internal API’s auth/login with a Basic header built from the QA credentials — the same flow as authentication.

Driving the real backend, or stubbing it

Most specs act against a deployed environment and the TEST_CYPRESS_SUPPLIER fixture, undoing whatever they create. awx-disputes.spec.js does not: it stubs the three Airwallex dispute endpoints its create-then-submit path touches — list, create and submit — with cy.intercept, swapping the list response as the flow progresses so each reload reflects the mutation. Evidence upload and cancel are covered by unit tests rather than here.

The reason is that the flow is not reversible. An Airwallex dispute cannot be deleted — cancelling is the only unwind, and only before it reaches IN_PROGRESS — so a live run would leave permanent records on the test supplier and would not be idempotent. See Airwallex disputes.

Prefer the real backend when a flow can clean up after itself, and stub when it cannot.

data-test attributes

The convention, from the README: whenever a test needs to act on an element, add data-test="uniqueName" to it. The sidebar is thoroughly annotated (sidebarSearch, sidebarActions, sidebarMarketplaces, …).

data-test values are not guaranteed unique. The Manage menu uses data-test="sidebarContracts" for both the Contracts and the Month End Close items — a copy-paste error that would make a selector ambiguous. See manage.

Definition of done, testing part

New behaviour needs a unit test. A user-facing flow should have a Cypress test, but given the suite’s state, a unit test plus a described manual check is the practical bar. See conventions and the definition of done in CLAUDE.md.