import { TestBed, TestModuleMetadata, ComponentFixture, MetadataOverride, ComponentFixtureAutoDetect } from '@angular/core/testing';
import { SchemaMetadata, Type, AbstractType, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { spyOnFunctionsOf } from '../spy-on-functions/spy-on-functions-of.function';
import { Observable } from 'rxjs';
import { observe } from '../observe/observe.function';
import { SpyObserver } from '../observe/spy-observer.class';
import { By } from '@angular/platform-browser';
export interface FullTestModuleMetadata extends TestModuleMetadata{
providers: any[];
declarations: any[];
imports: any[];
schemas: Array<SchemaMetadata | any[]>;
teardown: {
destroyAfterEach: boolean;
rethrowErrors: boolean;
};
errorOnUnknownElements: boolean;
errorOnUnknownProperties: boolean;
}
export interface IFactory<I> {
create(...args: Array<any>): I;
}
export class NgMagicSetupTestBed {
/**
* @ignore
*/
private config: FullTestModuleMetadata;
/**
* @ignore
*/
private configured = false;
/**
* @ignore
*/
private compiled = false;
/**
* @ignore
*/
private postConfigureJobs: Array<() => void> = [];
/**
* @ignore
*/
private fixtureJobs: Array<() => void> = [];
/**
* @ignore
*/
private fixtureInstance?: ComponentFixture<any> = undefined;
/**
* @param initialConfig initial config which will be extended by the other methods of this NgMagicSetupTestBed instance.
* The final config will be used to call TestBed.configureTestingModule implicitly by
* calling e.g. .injection()
*/
constructor(initialConfig: TestModuleMetadata = {}) {
this.config = {
providers: initialConfig.providers ? initialConfig.providers.slice() : [],
declarations: initialConfig.declarations ? initialConfig.declarations.slice() : [],
imports: initialConfig.imports ? initialConfig.imports.slice() : [],
schemas: initialConfig.schemas ? initialConfig.schemas.slice() : [],
errorOnUnknownElements: initialConfig.errorOnUnknownElements ?? false,
errorOnUnknownProperties: initialConfig.errorOnUnknownProperties ?? false,
teardown: {
destroyAfterEach: initialConfig.teardown?.destroyAfterEach ?? true,
rethrowErrors: initialConfig.teardown?.rethrowErrors ?? false
}
};
}
/**
* @ignore
*/
private configureTestingModule() {
if (this.configured) {
return;
}
this.configured = true;
TestBed.configureTestingModule(this.config);
this.postConfigureJobs.forEach(job => job());
this.postConfigureJobs.length = 0;
}
/**
* @ignore
*/
private expectToBePreConfiguration() {
if (this.configured) {
throw new Error('The TestBed has been implicitly configured by calling e.g.' +
'"injection" or "fixture" the method you called needs a not configured TestBed to be executed');
}
}
/**
* @param declarations declarations will be pushed to the declarations of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public declarations(declarations: Array<any>) {
this.expectToBePreConfiguration();
this.config.declarations.push(...declarations);
}
/**
* @param declaration declaration will be pushed to the declarations of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public declaration(declaration: any) {
this.declarations([declaration]);
}
/**
* @param schemas schemas will be pushed to the schemas of the testing module config. Note that the NO_ERRORS_SCHEMA
* is pushed by default. This can be disabled when calling .fixture().
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public schemas(schemas: Array<SchemaMetadata | any[]>) {
this.expectToBePreConfiguration();
this.config.schemas.push(...schemas);
}
/**
* @param schema schema will be pushed to the schemas of the testing module config. Note that the NO_ERRORS_SCHEMA
* is pushed by default. This can be disabled when calling .fixture().
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public schema(schema: SchemaMetadata | any[]) {
this.schemas([schema]);
}
/**
* @param imports imports will be pushed to the imports of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public imports(imports: any[]) {
this.expectToBePreConfiguration();
this.config.imports.push(...imports);
}
/**
* @param imports import will be pushed to the imports of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public import(aImport: any) {
this.imports([aImport]);
}
/**
* @param providers providers will be pushed to the providers of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public providers(providers: any[]) {
this.expectToBePreConfiguration();
this.config.providers.push(...providers);
}
/**
* @param provider import will be pushed to the providers of the testing module config.
* check out angular docs for more information
* https://angular.io/guide/testing-services#angular-testbed
* https://angular.io/api/core/testing/TestBed#configuretestingmodule
* https://angular.io/api/core/testing/TestModuleMetadata
*/
public provider(provider: any) {
this.providers([provider]);
}
public pipeServiceMock<S, M extends Partial<S>>(pipeClass: Type<any>, serviceClass: AbstractType<S>, mock: M,
dontSpy: true): S & M;
public pipeServiceMock<S, M extends Partial<S>>(pipeClass: Type<any>, serviceClass: AbstractType<S>, mock: M):
jasmine.SpyObj<S> & M;
public pipeServiceMock<S, M extends Partial<S>>(pipeClass: Type<any>, serviceClass: AbstractType<S>):
jasmine.SpyObj<S>;
/**
* If you have pipe that provides a service you can mock it using this method.
* @param pipeClass the pipeClass is the reference of the class of your angular pipe.
* @param serviceClass the serviceClass is the reference to the class of the service that you want to mock
* @param mock the mock mocks the service and should implement a partial of the service class
* @param dontSpy optional parameter to prevent the default spy creation on the mock using the prototype of the serviceClass
*/
public pipeServiceMock<S, M extends Partial<S>>(pipeClass: Type<any>, serviceClass: AbstractType<S>,
mock?: M, dontSpy?: boolean):
jasmine.SpyObj<S> & M | S & M | jasmine.SpyObj<S> | S {
return <any> this.componentProviderMock(pipeClass, serviceClass, mock, dontSpy, serviceClass);
}
/**
* If you have pipe that provides a provider you can mock it using this method.
* @param pipeClass the pipeClass is the reference of the class of your angular pipe.
* @param token the provider token that you want to mock
* @param mock the mock
* @param dontSpy optional parameter to prevent the default spy creation on the mock
*/
public pipeProviderMock<M>(pipeClass: Type<any>, token: any, mock: M, dontSpy = false,
spySource?: AbstractType<any>): M {
return this.uiThingProviderMock('overridePipe', pipeClass, token, mock, dontSpy, spySource);
}
/**
* If you have directive that provides a service you can mock it using this method.
* @param directiveClass the directiveClass is the reference of the class of your angular directive.
* @param serviceClass the serviceClass is the reference to the class of the service that you want to mock
* @param mock the mock mocks the service and should implement a partial of the service class
* @param dontSpy optional parameter to prevent the default spy creation on the mock using the prototype of the serviceClass
*/
public directiveServiceMock<S, M extends Partial<S>>(directiveClass: Type<any>, serviceClass: AbstractType<S>, mock: M,
dontSpy: true): S & M;
public directiveServiceMock<S, M extends Partial<S>>(directiveClass: Type<any>, serviceClass: AbstractType<S>, mock: M):
jasmine.SpyObj<S> & M;
public directiveServiceMock<S, M extends Partial<S>>(directiveClass: Type<any>, serviceClass: AbstractType<S>):
jasmine.SpyObj<S>;
public directiveServiceMock<S, M extends Partial<S>>(directiveClass: Type<any>, serviceClass: AbstractType<S>,
mock?: M, dontSpy?: boolean):
S & M | jasmine.SpyObj<S> & M | jasmine.SpyObj<S> {
return <any> this.componentProviderMock(directiveClass, serviceClass, mock, dontSpy, serviceClass);
}
/**
* If you have directive that provides a provider you can mock it using this method.
* @param directiveClass the directiveClass is the reference of the class of your angular directive.
* @param token the provider token that you want to mock
* @param mock the mock
* @param dontSpy optional parameter to prevent the default spy creation on the mock
*/
public directiveProviderMock<M>(directiveClass: Type<any>, token: any, mock: M, dontSpy = false,
spySource?: AbstractType<any>): M {
return this.uiThingProviderMock('overrideDirective', directiveClass, token, mock, dontSpy, spySource);
}
public componentServiceMock<S, M extends Partial<S>>(componentClass: Type<any>, serviceClass: AbstractType<S>, mock: M,
dontSpy: true): S & M;
public componentServiceMock<S, M extends Partial<S>>(componentClass: Type<any>, serviceClass: AbstractType<S>, mock: M):
jasmine.SpyObj<S> & M;
public componentServiceMock<S, M extends Partial<S>>(componentClass: Type<any>, serviceClass: AbstractType<S>):
jasmine.SpyObj<S>;
/**
* If you have component that provides a service you can mock it using this method.
* @param componentClass the componentClass is the reference of the class of your angular component.
* @param serviceClass the serviceClass is the reference to the class of the service that you want to mock
* @param mock the mock mocks the service and should implement a partial of the service class
* @param dontSpy optional parameter to prevent the default spy creation on the mock using the prototype of the serviceClass
*/
public componentServiceMock<S, M extends Partial<S>>(componentClass: Type<any>, serviceClass: AbstractType<S>,
mock?: M, dontSpy?: boolean):
S & M | jasmine.SpyObj<S> & M | jasmine.SpyObj<S> {
return <any>this.componentProviderMock(componentClass, serviceClass, mock, dontSpy, serviceClass);
}
/**
* If you have component provides a provider you can mock it using this method.
* @param componentClass the componentClass is the reference of the class of your angular component.
* @param token the provider token that you want to mock
* @param mock the mock
* @param dontSpy optional parameter to prevent the default spy creation on the mock
*/
public componentProviderMock<M>(componentClass: Type<any>, token: any, mock: M, dontSpy = false,
spySource?: AbstractType<any>): M {
return this.uiThingProviderMock('overrideComponent', componentClass, token, mock, dontSpy, spySource);
}
/**
* @ignore
*/
private uiThingProviderMock<M>(methodName: string, uiThingClass: Type<any>, token: any, mock: M, dontSpy = false,
spySource?: AbstractType<any>): M {
this.expectToBePreConfiguration();
if (!dontSpy) {
spyOnFunctionsOf(mock, spySource ? spySource.prototype : undefined);
}
if (!this.config.declarations.includes(uiThingClass)) {
this.config.declarations.push(uiThingClass);
}
this.postConfigureJobs.push(() => {
(TestBed as any)[methodName](uiThingClass, {
add: {
providers: [
{ provide: token, useValue: mock }
]
}
});
});
return mock;
}
public directiveMocks<C>(directiveClass: Type<C>): Array<C> {
return this.componentMocks(directiveClass);
}
/**
* declare that you want to mock a component for a selector and retrieve all created component mock instances after fixture
* creation.
* @param componentClass class of the component that should be used in the fixture for a specific selector you want to mock.
* @returns an arry of all component instances that were found statically inside the fixture. The array's members can only be
* used after calling .fixture(). Before that time the array is initialized like this:
* ['this array can only be used after fixture called'].
*/
public componentMocks<C>(componentClass: Type<C>): Array<C> {
const result: Array<any> = ['this array can only be used after fixture called'];
this.expectToBePreConfiguration();
if (!this.config.declarations.includes(componentClass)) {
this.config.declarations.push(componentClass);
}
this.fixtureJobs.push(() => {
result.length = 0;
const componentDebugElements = this.fixtureInstance?.debugElement.queryAll(By.directive(componentClass));
componentDebugElements?.forEach(componentDebugElement => result.push(componentDebugElement.injector.get(componentClass)));
});
return result;
}
/**
* Use this method to create a component fixture. This method may only be called once per NgMagicTestBed instance.
* @param componentClass class of the root component you want to compile and create.
* @param disableNoErrorSchema by default the NgMagicTestBed uses the NO_ERROR_SCHEMA of angular to prevent the compiler from
* throwing exceptions e.g. for missing or unknown inputs.
* @returns a component fixture like standard TestBed.createComponent(componentClass) would have returned it.
*/
public fixture<C>(componentClass: Type<C>, initialInputs: Partial<C> = {}, disableNoErrorSchema = false): ComponentFixture<C> {
if (this.fixtureInstance) {
throw new Error('.fixture can only be called once per NgMagicTestBed instance');
}
if (!this.config.declarations.includes(componentClass) && this.configured) {
throw new Error('Declaration of component needs to be done before you can create the fixture');
}
if (!this.config.declarations.includes(componentClass) && !this.configured) {
this.config.declarations.push(componentClass);
}
if (!disableNoErrorSchema && !this.config.schemas.includes(NO_ERRORS_SCHEMA)) {
this.config.schemas.push(NO_ERRORS_SCHEMA);
}
if (!this.configured) {
this.configureTestingModule();
}
if (!this.compiled) {
this.compiled = true;
TestBed.compileComponents();
}
this.fixtureInstance = TestBed.createComponent(componentClass);
Object.assign(this.fixtureInstance.componentInstance, initialInputs);
this.fixtureInstance.detectChanges();
this.fixtureJobs.forEach(job => job());
return this.fixtureInstance;
}
/**
* Can be used to create a mock for an object that should not be registered at angular TestBed.
* @param objectClass This class' prototype will be used to extend the result mock by a spy for each method on the prototype.
* @param mock An object that should implement partial of objectClass and contain all methods that you want to return something.
* @param dontSpy optional parameter to prevent the default spy creation on the mock.
* @returns Your mocks methods will be overwritten with spies that call through to the mocks methods like jasmine's spyOn method.
* In addition to that a spy will be added for each additional method that was found on the objectClass' prototype.
*/
public objectMock<O, M extends Partial<O>>(objectClass: AbstractType<O>, mock: M,
dontSpy: true): O & M;
public objectMock<O, M extends Partial<O>>(objectClass: AbstractType<O>, mock: M):
jasmine.SpyObj<O> & M & O;
public objectMock<O, M extends Partial<O>>(objectClass: undefined, mock: M): jasmine.SpyObj<M>;
public objectMock<O, M extends Partial<O>>(objectClass: AbstractType<O> | undefined, mock: M | any, dontSpy = false):
O & M | jasmine.SpyObj<O> & M {
return <O & M | jasmine.SpyObj<O> & M>this.mock(undefined, mock, dontSpy, objectClass);
}
/**
* mocks a provider for a given token with a given mock. If wanted your mock can be extended by spies
* from a given spySource class.
* @param token token for provider provision
* @param mock mock that will be registered for the token
* @param dontSpy optional parameter to prevent the default spy creation on the mock.
* @param spySource for each method in spySources prototype an additional jasmine spy will be created on the mock
* @returns Your mocks methods will be overwritten with spies that call through to the mocks methods like jasmine's spyOn method.
* In addition to that a spy will be added for each additional method that was found on the objectClass' prototype.
*/
public providerMock<M>(token: any, mock: Partial<M>, dontSpy: boolean = false, spySource?: AbstractType<any>) {
return this.mock(token, mock, dontSpy, spySource);
}
/**
* mocks a service that has a "create" method.
* @param factoryClass service that has a "create" method that you want to mock.
* @param instances will be returned by the mock this method return when "create" is called.
* The first call of mock.create() will return the first item in the instances-array and so on.
* @returns a mock for the factory. mock.create will return the one of the given instances every time it is called
*/
public factoryMock<I, F extends IFactory<I>, M>(factoryClass: AbstractType<F>, instances: Array<M & I>): jasmine.SpyObj<Partial<F>> {
let index = -1;
return <any>this.mock(factoryClass, <any>{
create: (...args: any) => {
index++;
return instances[index];
},
}, false, factoryClass);
}
public serviceMock<S, M extends Partial<S>>(serviceClass: AbstractType<S>, mock: M,
dontSpy: true): S & M;
public serviceMock<S, M extends Partial<S>>(serviceClass: AbstractType<S>, mock: M):
jasmine.SpyObj<S> & M;
public serviceMock<S, M extends Partial<S>>(serviceClass: AbstractType<S>): jasmine.SpyObj<S>;
/**
* mocks a service with the given mock
* @param serviceClass service that you want to mock
* @param mock that should mock the service. All methods on the mock will become spies. For each method on serviceClass'
* prototype another spy will be added to the mock.
* @param dontSpy optional parameter to prevent the default spy creation on the mock.
* @returns the mock after creating some spies on it (if not disabled)
*/
public serviceMock<S, M extends Partial<S>>(serviceClass: AbstractType<S>, mock?: M, dontSpy?: boolean):
S & M | jasmine.SpyObj<S> & M | jasmine.SpyObj<S> {
return this.mock(serviceClass, mock, dontSpy, serviceClass);
}
/**
* @ignore
*/
private mock<S, M extends Partial<S>>(token?: any, mock: M = <any>{}, dontSpy?: boolean, spySource?: AbstractType<S>):
S & M | jasmine.SpyObj<S> & M | jasmine.SpyObj<S> {
if (!dontSpy) {
spyOnFunctionsOf(mock, spySource ? spySource.prototype : undefined);
}
if (token) {
this.expectToBePreConfiguration();
this.config.providers.push({
useValue: mock,
provide: token
});
}
return <any> mock;
}
public injection<S>(service: AbstractType<S>): S;
/* tslint:disable */
public injection<S>(token: any): S;
/* tslint:enable */
/**
* return you the service or provider for a given token from the angular dependency injection.
* This will trigger the TestBed configureTestingModule step. After this step you can not create any more mocks.
* Make sure you create all your mocks before calling this mehtod.
* @param serviceClass service that you want to inject
* @param token of the prider that you want to inject
* @return whatever angular dependency injection finds for your token
*/
public injection<S>(token: AbstractType<S> | any): S {
this.configureTestingModule();
return TestBed.get(token);
}
/**
*
* Subscribes to a given observable and spies on its states and emitted values.
* @param observable
* Observable you want to spy
* @param name
* Optional name that prefixes all jasmine spies that are created by the observer. This makes it easier to read the
* test output if anything fails.
* @returns
* observer that can be used to make assertions in your test cases e.g.:
* expect(observer.next).toHaveBeenCalledWith(expectedValue);
* For more information check SpyObserver documentation
*/
public observer<T>(observable: Observable<T>, name?: string): SpyObserver<T> {
return observe(observable, name);
}
}