Skip to content

Commit 165f147

Browse files
authored
Unit tests updates (#915)
Update unit tests after upgrade.
1 parent 23840fe commit 165f147

File tree

457 files changed

+7102
-7323
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

457 files changed

+7102
-7323
lines changed

.github/scripts/check-coverage-thresholds.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const fs = require('fs');
21
const coverage = require('../../coverage/coverage-summary.json');
32
const jestConfig = require('../../jest.config.js');
43

@@ -43,7 +42,7 @@ if (failed) {
4342
console.log('\n\nCongratulations! You have successfully run the coverage check and added tests.');
4443
console.log('\n\nThe jest.config.js file is not insync with your new test additions.');
4544
console.log('Please update the coverage thresholds in jest.config.js.');
46-
console.log('You will need to commit again once you have updated the jst.config.js file.');
45+
console.log('You will need to commit again once you have updated the jst.config.ts file.');
4746
console.log('This is only necessary until we hit 100% coverage.');
4847
console.log(`\n\n${stars}`);
4948
errors.forEach((err) => {

docs/testing.md

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
```
4646
src/testing/
4747
├── osf.testing.provider.ts ← provideOSFCore(), provideOSFHttp()
48-
├── osf.testing.module.ts ← OSFTestingModule (legacy — prefer providers)
4948
├── providers/ ← Builder-pattern mocks for services
5049
│ ├── store-provider.mock.ts
5150
│ ├── route-provider.mock.ts
@@ -68,21 +67,14 @@ src/testing/
6867

6968
### `provideOSFCore()` — mandatory base provider
7069

71-
Every component test must include `provideOSFCore()`. It configures animations, translations, and environment tokens.
70+
Every component test must include `provideOSFCore()`. It configures translations and environment tokens.
7271

7372
```typescript
7473
export function provideOSFCore() {
75-
return [
76-
provideNoopAnimations(),
77-
importProvidersFrom(TranslateModule.forRoot()),
78-
TranslationServiceMock,
79-
EnvironmentTokenMock,
80-
];
74+
return [provideTranslation, TranslateServiceMock, EnvironmentTokenMock];
8175
}
8276
```
8377

84-
> **Never** import `OSFTestingModule` directly in new tests. It is retained for legacy compatibility only. Use `provideOSFCore()` instead.
85-
8678
---
8779

8880
## 3. Test File Structure
@@ -319,7 +311,35 @@ expect(store.dispatch).toHaveBeenCalledWith(new SpecificAction());
319311

320312
## 7. Router & Route Mocking
321313

322-
### ActivatedRoute
314+
Use this checklist:
315+
316+
### `provideRouter([])`
317+
318+
- Use when the template/component needs router infrastructure (`routerLink`, `routerLinkActive`, router directives/providers).
319+
- Keep it local to the spec.
320+
- Skip it for pure logic tests without router directive usage.
321+
322+
### `ActivatedRouteMockBuilder`
323+
324+
- Use when code reads route state (`snapshot.paramMap`, `params`, `queryParams`, `parent`, and similar route inputs).
325+
- Use it for deterministic route inputs in unit tests.
326+
- Works with or without `provideRouter([])` depending on template needs.
327+
328+
### `RouterMockBuilder`
329+
330+
- Use when you assert navigation calls (`navigate`, `navigateByUrl`, `url`/events usage).
331+
- Best for behavior assertions, not real router integration.
332+
- If you mock `Router`, you test whether navigation was requested, not real routing execution.
333+
- Do not mock `Router` in specs that validate real routing behavior via `provideRouter(...)`.
334+
335+
### Typical combos
336+
337+
- Params only: `ActivatedRouteMockBuilder`
338+
- Params + navigation assertions: `ActivatedRouteMockBuilder` + `RouterMockBuilder`
339+
- Template has `routerLink` + params + navigation assertions: `provideRouter([])` + `ActivatedRouteMockBuilder` + `RouterMockBuilder`
340+
- Need real router behavior: `provideRouter(...)` + `ActivatedRoute` setup, avoid mocking `Router`
341+
342+
### `ActivatedRouteMockBuilder` examples
323343

324344
```typescript
325345
const mockRoute = ActivatedRouteMockBuilder.create()
@@ -338,7 +358,7 @@ const mockRoute = ActivatedRouteMockBuilder.create()
338358
const mockRoute = ActivatedRouteMockBuilder.create().withParams({ id: 'reg-1' }).withNoParent().build();
339359
```
340360

341-
### Router
361+
### `RouterMockBuilder` examples
342362

343363
```typescript
344364
const mockRouter = RouterMockBuilder.create().withUrl('/registries/drafts/reg-1/metadata').build();
@@ -421,37 +441,57 @@ fixture.detectChanges();
421441

422442
## 10. Async Operations
423443

424-
### `fakeAsync` + `tick` for debounced operations
444+
### Zoneless change detection
445+
446+
In a zoneless environment, `fixture.detectChanges()` is used for immediate synchronous rendering. For signal updates and other async logic, use `await fixture.whenStable()` before DOM assertions so the scheduler can finish.
447+
448+
```typescript
449+
it('should update UI after signal change', async () => {
450+
mySignal.set(newVal);
451+
await fixture.whenStable();
452+
expect(fixture.nativeElement.textContent).toContain(newVal);
453+
});
454+
```
455+
456+
### Debounced operations with Jest timers
425457

426458
```typescript
427-
it('should dispatch after debounce', fakeAsync(() => {
459+
it('should dispatch after debounce', () => {
460+
jest.useFakeTimers();
428461
(store.dispatch as jest.Mock).mockClear();
462+
429463
component.onProjectFilter('abc');
430-
tick(300);
464+
jest.advanceTimersByTime(300);
465+
431466
expect(store.dispatch).toHaveBeenCalledWith(new GetProjects('user-1', 'abc'));
432-
}));
467+
jest.useRealTimers();
468+
});
433469

434-
// Deduplication — only the last value dispatches
435-
it('should debounce rapid calls', fakeAsync(() => {
470+
it('should debounce rapid calls', () => {
471+
jest.useFakeTimers();
436472
(store.dispatch as jest.Mock).mockClear();
473+
437474
component.onProjectFilter('a');
438475
component.onProjectFilter('ab');
439476
component.onProjectFilter('abc');
440-
tick(300);
441-
const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [any]) => a instanceof GetProjects);
477+
jest.advanceTimersByTime(300);
478+
479+
const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [unknown]) => a instanceof GetProjects);
442480
expect(calls.length).toBe(1);
443-
}));
481+
jest.useRealTimers();
482+
});
444483
```
445484

446-
### `done` callback for output emissions
485+
### Output emissions with explicit async flow
447486

448487
```typescript
449-
it('should emit attachFile', (done) => {
450-
component.attachFile.subscribe((f) => {
451-
expect(f).toEqual({ id: 'file-1' });
452-
done();
488+
it('should emit attachFile', async () => {
489+
const emitted = new Promise<FileModel>((resolve) => {
490+
component.attachFile.subscribe((file) => resolve(file));
453491
});
492+
454493
component.selectFile({ id: 'file-1' } as FileModel);
494+
await expect(emitted).resolves.toEqual({ id: 'file-1' });
455495
});
456496
```
457497

@@ -854,7 +894,7 @@ This project strictly enforces 90%+ test coverage through GitHub Actions CI.
854894

855895
## 18. Best Practices
856896

857-
1. **Always use `provideOSFCore()`** — never import `OSFTestingModule` directly in new tests.
897+
1. **Always use `provideOSFCore()`**.
858898
2. **Always use `provideMockStore()`** — never mock `component.actions` via `Object.defineProperty`.
859899
3. **Always pass explicit mocks to `MockProvider`** when you need `jest.fn()` assertions. Bare `MockProvider(Service)` creates ng-mocks stubs.
860900
4. **Check `@testing/` before creating inline mocks** — builders and factories almost certainly exist.
@@ -863,13 +903,12 @@ This project strictly enforces 90%+ test coverage through GitHub Actions CI.
863903
7. **Use `(store.dispatch as jest.Mock).mockClear()`** when `ngOnInit` dispatches and you need isolated per-test assertions.
864904
8. **Use `WritableSignal` for dynamic state** — pass `signal()` values to `provideMockStore` when tests need to mutate state mid-test.
865905
9. **Use `Subject` for dialog `onClose`** — gives explicit control over dialog result timing. Use `provideDynamicDialogRefMock()` where applicable.
866-
10. **Use `fakeAsync` + `tick`** for debounced operations — specify the exact debounce duration.
906+
10. **Use Jest fake timers** for debounced operations — `jest.useFakeTimers()`, `jest.advanceTimersByTime(ms)`, and `jest.useRealTimers()`.
867907
11. **Use `fixture.componentRef.setInput()`** for signal inputs — never direct property assignment.
868-
12. **Use `ngMocks.faster()`** when all tests in a file share identical `TestBed` config — reuses the compiled module for speed. Do not use if any test requires a different config: shared state will cause subtle test pollution.
869-
13. **Use typed mock interfaces** (`ToastServiceMockType`, `RouterMockType`, etc.) — avoid `any`.
870-
14. **Test both positive and negative paths** — confirm an action fires AND confirm it does not fire when conditions are not met.
871-
15. **Only use `@testing/data` fixtures in HTTP flushes** — never hardcode response values inline in service or state tests.
872-
16. **Each test should highlight the most critical aspect of the code** — if a test fails during a refactor, it should clearly signal that a core feature was impacted.
908+
12. **Use typed mock interfaces** (`ToastServiceMockType`, `RouterMockType`, etc.) — avoid `any`.
909+
13. **Test both positive and negative paths** — confirm an action fires AND confirm it does not fire when conditions are not met.
910+
14. **Only use `@testing/data` fixtures in HTTP flushes** — never hardcode response values inline in service or state tests.
911+
15. **Each test should highlight the most critical aspect of the code** — if a test fails during a refactor, it should clearly signal that a core feature was impacted.
873912

874913
---
875914

@@ -902,7 +941,7 @@ expect(callArgs[1].data.draftId).toBe('draft-1');
902941
### Filtering dispatch calls by action type
903942

904943
```typescript
905-
const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [any]) => a instanceof GetProjects);
944+
const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [unknown]) => a instanceof GetProjects);
906945
expect(calls.length).toBe(1);
907946
expect(calls[0][0]).toEqual(new GetProjects('user-1', 'abc'));
908947
```

jest.config.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
module.exports = {
1+
const config = {
22
preset: 'jest-preset-angular',
33
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
44
globalSetup: '<rootDir>/jest.global-setup.ts',
5-
collectCoverage: false,
5+
testEnvironment: 'jsdom',
66
clearMocks: true,
77
restoreMocks: true,
8-
coverageReporters: ['json-summary', 'lcov', 'clover'],
8+
collectCoverage: false,
9+
coverageDirectory: 'coverage',
10+
coverageReporters: ['json-summary', 'lcov', 'clover', 'text-summary'],
11+
moduleFileExtensions: ['ts', 'js', 'html', 'json', 'mjs'],
12+
extensionsToTreatAsEsm: ['.ts'],
13+
testMatch: ['<rootDir>/src/**/*.spec.ts'],
914
moduleNameMapper: {
1015
'^@osf/(.*)$': '<rootDir>/src/app/$1',
1116
'^@core/(.*)$': '<rootDir>/src/app/core/$1',
@@ -27,11 +32,8 @@ module.exports = {
2732
],
2833
},
2934
transformIgnorePatterns: [
30-
'node_modules/(?!.*\\.mjs$|@ngxs|@angular|@ngrx|parse5|entities|chart.js|@mdit|@citation-js|@traptitech|@sentry|@primeng|@newrelic)',
35+
'node_modules/(?!(@angular|@ngxs|@ngx-translate|angular-google-tag-manager|ngx-cookie-service|ngx-markdown-editor|angularx-qrcode|ngx-captcha|@sentry|@newrelic|@centerforopenscience|@mdit|@traptitech|@citation-js|primeng|@primeuix|markdown-it|markdown-it-anchor|markdown-it-toc-done-right|markdown-it-video|chart\\.js)/)',
3136
],
32-
testEnvironment: 'jsdom',
33-
moduleFileExtensions: ['ts', 'js', 'html', 'json', 'mjs'],
34-
coverageDirectory: 'coverage',
3537
collectCoverageFrom: [
3638
'src/app/**/*.{ts,js}',
3739
'!src/app/core/theme/**',
@@ -40,24 +42,21 @@ module.exports = {
4042
'!src/app/**/*.routes.{ts,js}',
4143
'!src/app/**/*.route.{ts,js}',
4244
'!src/app/**/mappers/**',
43-
'!src/app/shared/mappers/**',
4445
'!src/app/**/*.model.{ts,js}',
4546
'!src/app/**/models/*.{ts,js}',
46-
'!src/app/shared/models/**',
4747
'!src/app/**/*.enum.{ts,js}',
4848
'!src/app/**/*.type.{ts,js}',
4949
'!src/app/**/*.spec.{ts,js}',
5050
'!src/app/**/*.module.ts',
5151
'!src/app/**/index.ts',
5252
'!src/app/**/public-api.ts',
5353
],
54-
extensionsToTreatAsEsm: ['.ts'],
5554
coverageThreshold: {
5655
global: {
5756
branches: 43.3,
58-
functions: 42.7,
59-
lines: 69.3,
60-
statements: 69.8,
57+
functions: 43.8,
58+
lines: 70.18,
59+
statements: 70.6,
6160
},
6261
},
6362
watchPathIgnorePatterns: [
@@ -68,11 +67,7 @@ module.exports = {
6867
'<rootDir>/src/environments/',
6968
'<rootDir>/src/@types/',
7069
],
71-
testPathIgnorePatterns: [
72-
'<rootDir>/src/environments',
73-
'<rootDir>/src/app/features/files/pages/file-detail',
74-
'<rootDir>/src/app/features/project/addons/',
75-
'<rootDir>/src/app/features/settings/addons/',
76-
'<rootDir>/src/app/features/settings/tokens/store/',
77-
],
70+
testPathIgnorePatterns: ['<rootDir>/src/environments'],
7871
};
72+
73+
module.exports = config;

0 commit comments

Comments
 (0)