Skip to content

Commit b501939

Browse files
authored
feat(spectator): add support for runInInjectionContext() (#690)
* feat(spectator): add support for runInInjectionContext * docs(spectator): add docs for runInInjectionContext
1 parent 4c4140d commit b501939

15 files changed

+338
-0
lines changed

README.md

+54
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Become a bronze sponsor and get your logo on our README on GitHub.
8585
- [Additional Options](#additional-options)
8686
- [Testing Pipes](#testing-pipes)
8787
- [Using Custom Host Component](#using-custom-host-component)
88+
- [Testing DI Functions](#testing-di-functions)
8889
- [Mocking Providers](#mocking-providers)
8990
- [Mocking OnInit Dependencies](#mocking-oninit-dependencies)
9091
- [Mocking Constructor Dependencies](#mocking-constructor-dependencies)
@@ -238,6 +239,7 @@ The `createComponent()` method returns an instance of `Spectator` which exposes
238239
- `debugElement` - The tested fixture's debug element
239240

240241
- `flushEffects()` - Provides a wrapper for `TestBed.flushEffects()`
242+
- `runInInjectionContext()` - Provides a wrapper for `TestBed.runInInjectionContext()`
241243
- `inject()` - Provides a wrapper for `TestBed.inject()`:
242244
```ts
243245
const service = spectator.inject(QueryService);
@@ -926,6 +928,8 @@ describe('AuthService', () => {
926928
The `createService()` function returns `SpectatorService` with the following properties:
927929
- `service` - Get an instance of the service
928930
- `inject()` - A proxy for Angular `TestBed.inject()`
931+
- `flushEffects()` - A proxy for Angular `TestBed.flushEffects()`
932+
- `runInInjectionContext()` - A proxy for Angular `TestBed.runInInjectionContext()`
929933

930934
### Additional Options
931935

@@ -1020,6 +1024,8 @@ The `createPipe()` function returns `SpectatorPipe` with the following propertie
10201024
- `element` - The native element of the host component
10211025
- `detectChanges()` - A proxy for Angular `TestBed.fixture.detectChanges()`
10221026
- `inject()` - A proxy for Angular `TestBed.inject()`
1027+
- `flushEffects()` - A proxy for Angular `TestBed.flushEffects()`
1028+
- `runInInjectionContext()` - A proxy for Angular `TestBed.runInInjectionContext()`
10231029

10241030
Setting inputs directly on a pipe using `setInput` or `props` is not possible.
10251031
Inputs should be set through `hostProps` or `setHostInput` instead, and passed through to your pipe in the template.
@@ -1075,6 +1081,52 @@ describe('AveragePipe', () => {
10751081
});
10761082
```
10771083

1084+
## Testing DI Functions
1085+
1086+
Every Spectator instance supports testing DI Function by passing them to `runInInjectionContext()` function. There is a dedicated test factory that simplifies such testing by eliminating the need to pass some arbitrary Angular class amongst other factory options. Let's say we have a following function that uses the http module to fetch users:
1087+
1088+
```ts
1089+
import { HttpClient } from '@angular/common/http';
1090+
import { inject } from '@angular/core';
1091+
1092+
function getUsers() {
1093+
return inject(HttpClient).get<User[]>('users');
1094+
}
1095+
```
1096+
1097+
Let's see how we can test DI Function easily with Spectator:
1098+
1099+
```ts
1100+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
1101+
import { createInjectionContextFactory, SpectatorInjectionContext } from '@ngneat/spectator';
1102+
1103+
function getUsers() {
1104+
return inject(HttpClient).get<User[]>('users');
1105+
}
1106+
1107+
describe('Users', () => {
1108+
let spectator: SpectatorInjectionContext;
1109+
const createContext = createInjectionContextFactory({ providers: [provideHttpClientTesting()] });
1110+
1111+
it('should fetch users', () => {
1112+
spectator = createContext();
1113+
1114+
const controller = spectator.inject(HttpTestingController);
1115+
1116+
spectator.runInInjectionContext(getUsers).subscribe((users) => {
1117+
expect(users.length).toBe(1);
1118+
});
1119+
1120+
controller.expectOne('users').flush([{ id: 1 }]);
1121+
});
1122+
});
1123+
```
1124+
1125+
The `createContext()` function returns `SpectatorInjectionContext` with the following properties:
1126+
- `inject()` - A proxy for Angular `TestBed.inject()`
1127+
- `flushEffects()` - A proxy for Angular `TestBed.flushEffects()`
1128+
- `runInInjectionContext()` - A proxy for Angular `TestBed.runInInjectionContext()`
1129+
10781130
## Mocking Providers
10791131

10801132
For every Spectator factory, we can easily mock any provider.
@@ -1321,6 +1373,8 @@ We need to create an HTTP factory by using the `createHttpFactory()` function, p
13211373
- `httpClient` - A proxy for Angular `HttpClient`
13221374
- `service` - The service instance
13231375
- `inject()` - A proxy for Angular `TestBed.inject()`
1376+
- `flushEffects()` - A proxy for Angular `TestBed.flushEffects()`
1377+
- `runInInjectionContext()` - A proxy for Angular `TestBed.runInInjectionContext()`
13241378
- `expectOne()` - Expect that a single request was made which matches the given URL and it's method, and return its mock request
13251379

13261380

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AbstractType, InjectionToken, Type } from '@angular/core';
2+
import {
3+
createInjectionContextFactory as baseInjectionContextFactory,
4+
SpectatorInjectionContextOverrides,
5+
SpectatorInjectionContextOptions,
6+
SpectatorInjectionContext as BaseSpectatorInjectionContext,
7+
} from '@ngneat/spectator';
8+
import { mockProvider, SpyObject } from './mock';
9+
10+
/**
11+
* @publicApi
12+
*/
13+
export interface SpectatorInjectionContext extends BaseSpectatorInjectionContext {
14+
inject<T>(token: Type<T> | InjectionToken<T> | AbstractType<T>): SpyObject<T>;
15+
}
16+
17+
/**
18+
* @publicApi
19+
*/
20+
export type SpectatorInjectionContextFactory = (overrides?: SpectatorInjectionContextOverrides) => SpectatorInjectionContext;
21+
22+
/**
23+
* @publicApi
24+
*/
25+
export function createInjectionContextFactory(options: SpectatorInjectionContextOptions): SpectatorInjectionContextFactory {
26+
return baseInjectionContextFactory({
27+
mockProvider,
28+
...options,
29+
}) as SpectatorInjectionContextFactory;
30+
}

projects/spectator/jest/src/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './lib/spectator-service';
99
export * from './lib/spectator-host';
1010
export * from './lib/spectator-routing';
1111
export * from './lib/spectator-pipe';
12+
export * from './lib/spectator-injection-context';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { inject, Injectable, InjectionToken, NgModule } from '@angular/core';
2+
import { createInjectionContextFactory, SpectatorInjectionContext } from '@ngneat/spectator/jest';
3+
4+
const TEST_TOKEN = new InjectionToken<string>('simple-token');
5+
6+
@Injectable()
7+
export class TestService {
8+
flag = false;
9+
}
10+
11+
@NgModule({
12+
providers: [TestService],
13+
})
14+
export class TestModule {}
15+
16+
const testFn = (arg: any) => {
17+
const token = inject(TEST_TOKEN);
18+
const { flag } = inject(TestService);
19+
20+
return { token, flag, arg };
21+
};
22+
23+
describe('Run in injection context', () => {
24+
describe('with Spectator', () => {
25+
const createContext = createInjectionContextFactory({ imports: [TestModule], providers: [{ provide: TEST_TOKEN, useValue: 'abcd' }] });
26+
27+
let spectator: SpectatorInjectionContext;
28+
29+
beforeEach(() => (spectator = createContext()));
30+
31+
it('should execute fn in injection context', () => {
32+
const service = spectator.inject(TestService);
33+
service.flag = true;
34+
35+
const result = spectator.runInInjectionContext(() => testFn(2));
36+
expect(result).toEqual({ token: 'abcd', flag: true, arg: 2 });
37+
});
38+
});
39+
});

projects/spectator/src/lib/base/base-spectator.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ export abstract class BaseSpectator {
1717
public flushEffects(): void {
1818
TestBed.flushEffects();
1919
}
20+
21+
public runInInjectionContext<T>(fn: () => T): T {
22+
return TestBed.runInInjectionContext(fn);
23+
}
2024
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { initialInjectionContextModule as initialInjectionContextModule } from './initial-module';
3+
import { getDefaultFunctionOptions, SpectatorInjectionContextOptions } from './options';
4+
import { overrideModules } from '../spectator/create-factory';
5+
import { BaseSpectatorOverrides } from '../base/options';
6+
import { SpectatorInjectionContext } from './spectator-injection-context';
7+
import { Provider } from '@angular/core';
8+
9+
/**
10+
* @publicApi
11+
*/
12+
export type SpectatorInjectionContextFactory = (overrides?: SpectatorInjectionContextOverrides) => SpectatorInjectionContext;
13+
14+
/**
15+
* @publicApi
16+
*/
17+
export interface SpectatorInjectionContextOverrides extends BaseSpectatorOverrides {}
18+
19+
/**
20+
* @publicApi
21+
*/
22+
export function createInjectionContextFactory(options: SpectatorInjectionContextOptions): SpectatorInjectionContextFactory {
23+
const fullOptions = getDefaultFunctionOptions(options);
24+
25+
const moduleMetadata = initialInjectionContextModule(fullOptions);
26+
27+
beforeEach(() => {
28+
TestBed.configureTestingModule(moduleMetadata);
29+
overrideModules(fullOptions);
30+
});
31+
32+
return (overrides?: SpectatorInjectionContextOverrides) => {
33+
const defaults: SpectatorInjectionContextOverrides = { providers: [] };
34+
const { providers } = { ...defaults, ...overrides };
35+
36+
if (providers && providers.length) {
37+
providers.forEach((provider: Provider) => {
38+
TestBed.overrideProvider((provider as any).provide, provider as any);
39+
});
40+
}
41+
42+
return new SpectatorInjectionContext();
43+
};
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { initialModule, ModuleMetadata } from '../base/initial-module';
2+
import { FullInjectionContextOptions } from './options';
3+
4+
/**
5+
* @internal
6+
*/
7+
export function initialInjectionContextModule<F>(options: FullInjectionContextOptions): ModuleMetadata {
8+
const moduleMetadata = initialModule(options);
9+
10+
return moduleMetadata;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { BaseSpectatorOptions, getDefaultBaseOptions } from '../base/options';
2+
import { merge } from '../internals/merge';
3+
import { AtLeastOneRequired, OptionalsRequired } from '../types';
4+
5+
export type SpectatorInjectionContextOptions = AtLeastOneRequired<
6+
Pick<BaseSpectatorOptions, 'imports' | 'mockProvider' | 'mocks' | 'providers'>
7+
>;
8+
9+
const defaultFunctionOptions: OptionalsRequired<SpectatorInjectionContextOptions> = {
10+
...getDefaultBaseOptions(),
11+
};
12+
13+
/**
14+
* @internal
15+
*/
16+
export type FullInjectionContextOptions = Required<SpectatorInjectionContextOptions> & Required<BaseSpectatorOptions>;
17+
18+
/**
19+
* @internal
20+
*/
21+
export function getDefaultFunctionOptions(overrides: SpectatorInjectionContextOptions): FullInjectionContextOptions {
22+
return merge(defaultFunctionOptions, overrides) as FullInjectionContextOptions;
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { BaseSpectator } from '../base/base-spectator';
2+
3+
/**
4+
* @publicApi
5+
*/
6+
export class SpectatorInjectionContext extends BaseSpectator {
7+
constructor() {
8+
super();
9+
}
10+
}

projects/spectator/src/lib/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export type InferInputSignals<C> = {
1515

1616
export type OptionalsRequired<T> = Required<OptionalProperties<T>> & Partial<T>;
1717

18+
export type AtLeastOneRequired<T> = {
19+
[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>;
20+
}[keyof T];
21+
1822
export type SpectatorElement = string | Element | DebugElement | ElementRef | Window | Document | DOMSelector;
1923

2024
export type QueryType = Type<any> | DOMSelector | string;

projects/spectator/src/public_api.ts

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export { SpectatorPipeOptions } from './lib/spectator-pipe/options';
3333
export { createPipeFactory, SpectatorPipeFactory, SpectatorPipeOverrides } from './lib/spectator-pipe/create-factory';
3434
export { initialSpectatorPipeModule } from './lib/spectator-pipe/initial-module';
3535

36+
export { SpectatorInjectionContext } from './lib/spectator-injection-context/spectator-injection-context';
37+
export { SpectatorInjectionContextOptions } from './lib/spectator-injection-context/options';
38+
export {
39+
createInjectionContextFactory,
40+
SpectatorInjectionContextFactory,
41+
SpectatorInjectionContextOverrides,
42+
} from './lib/spectator-injection-context/create-factory';
43+
export { initialInjectionContextModule } from './lib/spectator-injection-context/initial-module';
44+
3645
export * from './lib/dom-selectors';
3746
export * from './lib/matchers';
3847
export * from './lib/mock';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { inject, Injectable, InjectionToken, NgModule } from '@angular/core';
2+
import { createInjectionContextFactory, SpectatorInjectionContext } from '@ngneat/spectator';
3+
4+
const TEST_TOKEN = new InjectionToken<string>('simple-token');
5+
6+
@Injectable()
7+
export class TestService {
8+
flag = false;
9+
}
10+
11+
@NgModule({
12+
providers: [TestService],
13+
})
14+
export class TestModule {}
15+
16+
const testFn = (arg: any) => {
17+
const token = inject(TEST_TOKEN);
18+
const { flag } = inject(TestService);
19+
20+
return { token, flag, arg };
21+
};
22+
23+
describe('Run in injection context', () => {
24+
describe('with Spectator', () => {
25+
const createContext = createInjectionContextFactory({ imports: [TestModule], providers: [{ provide: TEST_TOKEN, useValue: 'abcd' }] });
26+
27+
let spectator: SpectatorInjectionContext;
28+
29+
beforeEach(() => (spectator = createContext()));
30+
31+
it('should execute fn in injection context', () => {
32+
const service = spectator.inject(TestService);
33+
service.flag = true;
34+
35+
const result = spectator.runInInjectionContext(() => testFn(2));
36+
expect(result).toEqual({ token: 'abcd', flag: true, arg: 2 });
37+
});
38+
});
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AbstractType, InjectionToken, Type } from '@angular/core';
2+
import {
3+
createInjectionContextFactory as baseInjectionContextFactory,
4+
SpectatorInjectionContextOverrides,
5+
SpectatorInjectionContextOptions,
6+
SpectatorInjectionContext as BaseSpectatorInjectionContext,
7+
} from '@ngneat/spectator';
8+
import { mockProvider, SpyObject } from './mock';
9+
10+
/**
11+
* @publicApi
12+
*/
13+
export interface SpectatorInjectionContext extends BaseSpectatorInjectionContext {
14+
inject<T>(token: Type<T> | InjectionToken<T> | AbstractType<T>): SpyObject<T>;
15+
}
16+
17+
/**
18+
* @publicApi
19+
*/
20+
export type SpectatorInjectionContextFactory = (overrides?: SpectatorInjectionContextOverrides) => SpectatorInjectionContext;
21+
22+
/**
23+
* @publicApi
24+
*/
25+
export function createInjectionContextFactory(options: SpectatorInjectionContextOptions): SpectatorInjectionContextFactory {
26+
return baseInjectionContextFactory({
27+
mockProvider,
28+
...options,
29+
}) as SpectatorInjectionContextFactory;
30+
}

projects/spectator/vitest/src/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './lib/spectator-service';
99
export * from './lib/spectator-host';
1010
export * from './lib/spectator-routing';
1111
export * from './lib/spectator-pipe';
12+
export * from './lib/spectator-injection-context';

0 commit comments

Comments
 (0)