Skip to content

Commit

Permalink
Merge pull request #124 from satanTime/issues/121
Browse files Browse the repository at this point in the history
fix: better coverage for tokens
  • Loading branch information
satanTime authored May 23, 2020
2 parents 830cf1d + 7a3fe48 commit 8ff9094
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 34 deletions.
30 changes: 30 additions & 0 deletions e2e/module-with-factory-tokens/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// tslint:disable:max-classes-per-file no-parameter-properties

import { CommonModule } from '@angular/common';
import { Component, Inject, InjectionToken, NgModule } from '@angular/core';

export const MY_TOKEN_SINGLE = new (InjectionToken as any) /* A5 */('MY_TOKEN_SINGLE', {
factory: () => 'MY_TOKEN_SINGLE',
});

export const MY_TOKEN_MULTI = new (InjectionToken as any) /* A5 */('MY_TOKEN_MULTI', {
factory: () => 'MY_TOKEN_MULTI',
});

@Component({
selector: 'internal-component',
template: '{{ tokenSingle | json }} {{ tokenMulti | json }}',
})
export class TargetComponent {
constructor(
@Inject(MY_TOKEN_SINGLE) public readonly tokenSingle: string,
@Inject(MY_TOKEN_MULTI) public readonly tokenMulti: string[]
) {}
}

@NgModule({
declarations: [TargetComponent],
exports: [TargetComponent],
imports: [CommonModule],
})
export class TargetModule {}
108 changes: 108 additions & 0 deletions e2e/module-with-factory-tokens/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { VERSION } from '@angular/core';

import { MockBuilder } from '../../lib/mock-builder';
import { MockRender } from '../../lib/mock-render';

import { MY_TOKEN_MULTI, MY_TOKEN_SINGLE, TargetComponent, TargetModule } from './fixtures';

// Because all tokens have factories the test should render them correctly.
// There's no way to specify multi in a factory, so we don't get an array.
describe('module-with-factory-tokens:real', () => {
beforeEach(() => MockBuilder().keep(TargetModule));

it('renders all tokens', () => {
// tslint:disable-next-line:no-magic-numbers
if (parseInt(VERSION.major, 10) <= 5) {
pending('Need Angular > 5');
return;
}

const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MY_TOKEN_SINGLE" "MY_TOKEN_MULTI"');
});
});

// Because all tokens are kept the test should render them correctly.
// There's no way to specify multi in a factory, so we don't get an array.
describe('module-with-factory-tokens:keep', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(MY_TOKEN_SINGLE).keep(MY_TOKEN_MULTI));

it('renders all tokens', () => {
// tslint:disable-next-line:no-magic-numbers
if (parseInt(VERSION.major, 10) <= 5) {
pending('Need Angular > 5');
return;
}

const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MY_TOKEN_SINGLE" "MY_TOKEN_MULTI"');
});
});

// Preferred way.
// Because tokens are provided in the testbed module with custom values the test should render them.
describe('module-with-factory-tokens:mock-0', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.provide({
provide: MY_TOKEN_SINGLE,
useValue: 'V1',
})
.provide({
multi: true,
provide: MY_TOKEN_MULTI,
useValue: 'V2',
})
);

it('fails to render all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"V1" [ "V2" ]');
});
});

// Because all tokens are mocked in the module the test should render empty values.
// The tokens will be added to provides with undefined values.
// Result of the render is an empty string because there's no way to pass multi.
describe('module-with-factory-tokens:mock-1', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).mock(MY_TOKEN_SINGLE).mock(MY_TOKEN_MULTI));

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('');
});
});

// Because all tokens are mocked with custom values the test should render them.
// There's no way to specify multi in a factory, so we don't get an array.
describe('module-with-factory-tokens:mock-2', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.mock(MY_TOKEN_SINGLE, 'MOCKED_MY_TOKEN_SINGLE')
.mock(MY_TOKEN_MULTI, 'MOCKED_MY_TOKEN_MULTI')
);

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MOCKED_MY_TOKEN_SINGLE" "MOCKED_MY_TOKEN_MULTI"');
});
});

// And the most interesting case. Because we don't touch tokens at all and mock the module
// the tokens will used as they are with their factories.
// Unfortunately it's quite tough to guess which tokens we can keep, mocks or omit and now
// a user is responsible to specify tokens for his mock.
describe('module-with-factory-tokens:mock-3', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule));

it('renders all tokens', () => {
// tslint:disable-next-line:no-magic-numbers
if (parseInt(VERSION.major, 10) <= 5) {
pending('Need Angular > 5');
return;
}

const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MY_TOKEN_SINGLE" "MY_TOKEN_MULTI"');
});
});
42 changes: 42 additions & 0 deletions e2e/module-with-tokens/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// tslint:disable:max-classes-per-file no-parameter-properties

import { CommonModule } from '@angular/common';
import { Component, Inject, InjectionToken, NgModule } from '@angular/core';

export const MY_TOKEN_SINGLE = new InjectionToken('MY_TOKEN_SINGLE');

export const MY_TOKEN_MULTI = new InjectionToken('MY_TOKEN_MULTI');

@Component({
selector: 'internal-component',
template: '{{ tokenSingle | json }} {{ tokenMulti | json }}',
})
export class TargetComponent {
constructor(
@Inject(MY_TOKEN_SINGLE) public readonly tokenSingle: string,
@Inject(MY_TOKEN_MULTI) public readonly tokenMulti: string[]
) {}
}

@NgModule({
declarations: [TargetComponent],
exports: [TargetComponent],
imports: [CommonModule],
providers: [
{
provide: MY_TOKEN_SINGLE,
useValue: 'MY_TOKEN_SINGLE',
},
{
multi: true,
provide: MY_TOKEN_MULTI,
useValue: 'MY_TOKEN_MULTI',
},
{
multi: true,
provide: MY_TOKEN_MULTI,
useValue: 'MY_TOKEN_MULTI_2',
},
],
})
export class TargetModule {}
90 changes: 90 additions & 0 deletions e2e/module-with-tokens/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { MockBuilder } from '../../lib/mock-builder';
import { MockRender } from '../../lib/mock-render';

import { MY_TOKEN_MULTI, MY_TOKEN_SINGLE, TargetComponent, TargetModule } from './fixtures';

// Because all tokens are provided in the module the test should render them correctly.
describe('module-with-tokens:real', () => {
beforeEach(() => MockBuilder().keep(TargetModule));

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MY_TOKEN_SINGLE" [ "MY_TOKEN_MULTI", "MY_TOKEN_MULTI_2" ]');
});
});

// Because all tokens are kept in the module the test should render them correctly.
describe('module-with-tokens:keep', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(MY_TOKEN_SINGLE).keep(MY_TOKEN_MULTI));

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"MY_TOKEN_SINGLE" [ "MY_TOKEN_MULTI", "MY_TOKEN_MULTI_2" ]');
});
});

// Preferred way.
// Because tokens are provided in the testbed module with custom values the test should render them.
describe('module-with-tokens:mock-0', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.provide({
provide: MY_TOKEN_SINGLE,
useValue: 'V1',
})
.provide({
multi: true,
provide: MY_TOKEN_MULTI,
useValue: 'V2',
})
.provide({
multi: true,
provide: MY_TOKEN_MULTI,
useValue: 'V3',
})
);

it('fails to render all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('"V1" [ "V2", "V3" ]');
});
});

// Because all tokens are mocked in the module the test should render empty values.
// interesting is that for multi it's null, not undefined.
describe('module-with-tokens:mock-1', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).mock(MY_TOKEN_SINGLE).mock(MY_TOKEN_MULTI));

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual('[ null, null ]');
});
});

// Because all tokens are mocked in the module with custom values the test should render them.
describe('module-with-tokens:mock-2', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.mock(MY_TOKEN_SINGLE, 'MOCKED_MY_TOKEN_SINGLE')
.mock(MY_TOKEN_MULTI, 'MOCKED_MY_TOKEN_MULTI')
);

it('renders all tokens', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerText).toEqual(
'"MOCKED_MY_TOKEN_SINGLE" [ "MOCKED_MY_TOKEN_MULTI", "MOCKED_MY_TOKEN_MULTI" ]'
);
});
});

// And the most complicated case. Because we don't touch tokens at all and mock the module
// the tokens will be omitted from the final mock and injection will fail.
// Unfortunately it's quite tough to guess which tokens we can keep, mocks or omit and now
// a user is responsible to specify tokens for his mock.
describe('module-with-tokens:mock-3', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule));

it('fails to render all tokens', () => {
expect(() => MockRender(TargetComponent)).toThrowError(/InjectionToken/);
});
});
14 changes: 12 additions & 2 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,15 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
if (config && config.dependency) {
continue;
}
providers.push(ngMocksUniverse.builder.get(def));
const mock = ngMocksUniverse.builder.get(def);
providers.push(
mock
? mock
: {
provide: def,
useValue: undefined,
}
);
}

// Adding requested providers to test bed.
Expand Down Expand Up @@ -360,9 +368,11 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
public provide(def: Provider): this {
for (const provider of flatten(def)) {
const provide = typeof provider === 'object' && provider.provide ? provider.provide : provider;
const multi = typeof provider === 'object' && provider.provide && provider.multi;
this.keepDef.provider.delete(provide);
this.mockDef.provider.delete(provide);
this.providerDef.set(provide, provider);
const existing = this.providerDef.has(provide) ? this.providerDef.get(provide) : [];
this.providerDef.set(provide, multi ? [...(Array.isArray(existing) ? existing : []), provider] : provider);
}
return this;
}
Expand Down
Loading

0 comments on commit 8ff9094

Please sign in to comment.