From 6ac739621498bffe0ee8f06cf257c8e60fe44c68 Mon Sep 17 00:00:00 2001 From: MG Date: Sat, 6 Jun 2020 20:06:33 +0200 Subject: [PATCH] feat: mock-builder + lots of helpers closes #44 --- README.md | 253 +++++++++- e2e/angular5/src/test.ts | 1 + e2e/angular6/src/test.ts | 1 + e2e/angular7/src/test.ts | 1 + e2e/angular8/src/test.ts | 1 + e2e/angular9-ivy-false/src/test.ts | 1 + e2e/angular9-ivy-true/src/test.ts | 1 + .../MockBuilder/MockBuilder.spec.ts | 223 +++++++++ .../MockBuilder/fixtures.components.ts | 159 +++++++ .../MockBuilder/fixtures.directives.ts | 16 + .../MockBuilder/fixtures.modules.ts | 88 ++++ .../MockBuilder/fixtures.pipes.ts | 56 +++ .../MockBuilder/fixtures.services.ts | 100 ++++ .../MockBuilder/fixtures.tokens.ts | 7 + .../MockDirective.spec.ts | 4 +- .../MockReactiveForms.spec.ts | 2 +- examples-jasmine/NG_MOCKS/NG_MOCKS.spec.ts | 122 +++++ .../NG_MOCKS/fixtures.components.ts | 110 +++++ .../NG_MOCKS/fixtures.directives.ts | 16 + examples-jasmine/NG_MOCKS/fixtures.modules.ts | 88 ++++ examples-jasmine/NG_MOCKS/fixtures.pipes.ts | 56 +++ .../NG_MOCKS/fixtures.services.ts | 100 ++++ examples-jasmine/NG_MOCKS/fixtures.tokens.ts | 7 + examples-jest/MockBuilder/MockBuilder.spec.ts | 223 +++++++++ .../MockBuilder/fixtures.components.ts | 159 +++++++ .../MockBuilder/fixtures.directives.ts | 16 + examples-jest/MockBuilder/fixtures.modules.ts | 88 ++++ examples-jest/MockBuilder/fixtures.pipes.ts | 56 +++ .../MockBuilder/fixtures.services.ts | 100 ++++ examples-jest/MockBuilder/fixtures.tokens.ts | 7 + .../MockReactiveForms.spec.ts | 2 +- examples-jest/NG_MOCKS/NG_MOCKS.spec.ts | 122 +++++ examples-jest/NG_MOCKS/fixtures.components.ts | 110 +++++ examples-jest/NG_MOCKS/fixtures.directives.ts | 16 + examples-jest/NG_MOCKS/fixtures.modules.ts | 88 ++++ examples-jest/NG_MOCKS/fixtures.pipes.ts | 56 +++ examples-jest/NG_MOCKS/fixtures.services.ts | 100 ++++ examples-jest/NG_MOCKS/fixtures.tokens.ts | 7 + index.ts | 1 + lib/common/Mock.spec.ts | 2 - lib/common/Mock.ts | 4 +- lib/common/decorate.ts | 4 +- lib/common/index.ts | 3 +- lib/common/lib.ts | 175 +++++++ lib/common/mock-of.decorator.spec.ts | 1 - lib/common/mock-of.decorator.ts | 2 +- lib/common/ng-mocks-universe.ts | 16 + lib/mock-builder/index.ts | 1 + lib/mock-builder/mock-builder.ts | 435 ++++++++++++++++++ lib/mock-component/mock-component.spec.ts | 2 - lib/mock-component/mock-component.ts | 46 +- .../simple-component.component.ts | 2 - lib/mock-declaration/mock-declaration.spec.ts | 2 - lib/mock-declaration/mock-declaration.ts | 31 +- lib/mock-directive/mock-directive.spec.ts | 2 - lib/mock-directive/mock-directive.ts | 49 +- lib/mock-helper/mock-helper.spec.ts | 2 - lib/mock-helper/mock-helper.ts | 32 +- lib/mock-module/mock-module.spec.ts | 75 +-- lib/mock-module/mock-module.ts | 373 ++++++++++----- lib/mock-module/test-fixtures.ts | 17 +- lib/mock-pipe/mock-pipe.spec.ts | 4 - lib/mock-pipe/mock-pipe.ts | 22 +- lib/mock-render/mock-render.fixtures.ts | 5 + lib/mock-render/mock-render.spec.ts | 9 +- lib/mock-render/mock-render.ts | 20 +- .../context-with-directives.spec.ts | 32 +- .../fixtures.ts | 61 +++ .../test.spec.ts | 107 +++++ .../fixtures.ts | 70 +++ .../test.spec.ts | 129 ++++++ .../exports-only/fixtures.components.ts | 7 + .../exports-only/fixtures.modules.ts | 16 + tests-jasmine/exports-only/test.spec.ts | 64 +++ .../custom-injection.component.ts | 2 +- .../fixtures.components.ts | 7 + .../internal-only-nested/fixtures.modules.ts | 25 + .../internal-only-nested/test.spec.ts | 37 ++ .../internal-only/fixtures.components.ts | 7 + .../internal-only/fixtures.modules.ts | 10 + tests-jasmine/internal-only/test.spec.ts | 36 ++ .../fixtures.components.ts | 13 + .../internal-vs-external/fixtures.modules.ts | 11 + .../internal-vs-external/test.spec.ts | 70 +++ .../fixtures.components.ts | 7 + .../fixtures.modules.ts | 9 + .../mock-builder-by-directive/test.spec.ts | 40 ++ .../fixtures.components.ts | 7 + .../fixtures.modules.ts | 26 ++ .../test.spec.ts | 41 ++ .../module-with-factory-tokens/fixtures.ts | 28 ++ .../module-with-factory-tokens/test.spec.ts | 111 +++++ tests-jasmine/module-with-tokens/fixtures.ts | 40 ++ tests-jasmine/module-with-tokens/test.spec.ts | 97 ++++ .../nested-before-each/fixtures.components.ts | 7 + .../nested-before-each/fixtures.modules.ts | 9 + tests-jasmine/nested-before-each/test.spec.ts | 78 ++++ .../fixtures.components.ts | 27 ++ .../fixtures.modules.ts | 18 + .../fixtures.services.ts | 10 + .../test.spec.ts | 44 ++ tests-jasmine/on-push/on-push.spec.ts | 14 +- .../fixtures.components.ts | 15 + .../fixtures.modules.ts | 19 + .../fixtures.services.ts | 24 + .../provider-with-dependency/test.spec.ts | 69 +++ .../fixtures.components.ts | 11 + .../fixtures.module.ts | 11 + .../test.spec.ts | 44 ++ .../fixtures.components.ts | 25 + .../shared-mocked-module/fixtures.modules.ts | 31 ++ .../shared-mocked-module/test.spec.ts | 47 ++ tests-jasmine/spies/test.spec.ts | 11 +- .../structural-directives.spec.ts | 18 +- .../context-with-directives.spec.ts | 2 +- .../fixtures.ts | 61 +++ .../test.spec.ts | 107 +++++ .../fixtures.ts | 70 +++ .../test.spec.ts | 129 ++++++ .../exports-only/fixtures.components.ts | 7 + tests-jest/exports-only/fixtures.modules.ts | 16 + tests-jest/exports-only/test.spec.ts | 64 +++ .../custom-injection.component.ts | 2 +- .../fixtures.components.ts | 7 + .../internal-only-nested/fixtures.modules.ts | 25 + tests-jest/internal-only-nested/test.spec.ts | 37 ++ .../internal-only/fixtures.components.ts | 7 + tests-jest/internal-only/fixtures.modules.ts | 10 + tests-jest/internal-only/test.spec.ts | 36 ++ .../fixtures.components.ts | 13 + .../internal-vs-external/fixtures.modules.ts | 11 + tests-jest/internal-vs-external/test.spec.ts | 70 +++ .../fixtures.components.ts | 7 + .../fixtures.modules.ts | 9 + .../mock-builder-by-directive/test.spec.ts | 40 ++ .../fixtures.components.ts | 7 + .../fixtures.modules.ts | 26 ++ .../test.spec.ts | 41 ++ .../module-with-factory-tokens/fixtures.ts | 28 ++ .../module-with-factory-tokens/test.spec.ts | 111 +++++ tests-jest/module-with-tokens/fixtures.ts | 40 ++ tests-jest/module-with-tokens/test.spec.ts | 97 ++++ .../nested-before-each/fixtures.components.ts | 7 + .../nested-before-each/fixtures.modules.ts | 9 + tests-jest/nested-before-each/test.spec.ts | 78 ++++ .../fixtures.components.ts | 27 ++ .../fixtures.modules.ts | 18 + .../fixtures.services.ts | 10 + .../test.spec.ts | 44 ++ tests-jest/on-push/on-push.spec.ts | 2 - .../fixtures.components.ts | 15 + .../fixtures.modules.ts | 19 + .../fixtures.services.ts | 24 + .../provider-with-dependency/test.spec.ts | 69 +++ .../fixtures.components.ts | 11 + .../fixtures.module.ts | 11 + .../test.spec.ts | 44 ++ .../fixtures.components.ts | 25 + .../shared-mocked-module/fixtures.modules.ts | 31 ++ tests-jest/shared-mocked-module/test.spec.ts | 47 ++ tslint.json | 5 +- 161 files changed, 6812 insertions(+), 306 deletions(-) create mode 100644 examples-jasmine/MockBuilder/MockBuilder.spec.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.components.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.directives.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.modules.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.pipes.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.services.ts create mode 100644 examples-jasmine/MockBuilder/fixtures.tokens.ts create mode 100644 examples-jasmine/NG_MOCKS/NG_MOCKS.spec.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.components.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.directives.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.modules.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.pipes.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.services.ts create mode 100644 examples-jasmine/NG_MOCKS/fixtures.tokens.ts create mode 100644 examples-jest/MockBuilder/MockBuilder.spec.ts create mode 100644 examples-jest/MockBuilder/fixtures.components.ts create mode 100644 examples-jest/MockBuilder/fixtures.directives.ts create mode 100644 examples-jest/MockBuilder/fixtures.modules.ts create mode 100644 examples-jest/MockBuilder/fixtures.pipes.ts create mode 100644 examples-jest/MockBuilder/fixtures.services.ts create mode 100644 examples-jest/MockBuilder/fixtures.tokens.ts create mode 100644 examples-jest/NG_MOCKS/NG_MOCKS.spec.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.components.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.directives.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.modules.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.pipes.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.services.ts create mode 100644 examples-jest/NG_MOCKS/fixtures.tokens.ts create mode 100644 lib/common/lib.ts create mode 100644 lib/common/ng-mocks-universe.ts create mode 100644 lib/mock-builder/index.ts create mode 100644 lib/mock-builder/mock-builder.ts create mode 100644 tests-jasmine/control-value-accessor-form-control/fixtures.ts create mode 100644 tests-jasmine/control-value-accessor-form-control/test.spec.ts create mode 100644 tests-jasmine/control-value-accessor-ng-model/fixtures.ts create mode 100644 tests-jasmine/control-value-accessor-ng-model/test.spec.ts create mode 100644 tests-jasmine/exports-only/fixtures.components.ts create mode 100644 tests-jasmine/exports-only/fixtures.modules.ts create mode 100644 tests-jasmine/exports-only/test.spec.ts create mode 100644 tests-jasmine/internal-only-nested/fixtures.components.ts create mode 100644 tests-jasmine/internal-only-nested/fixtures.modules.ts create mode 100644 tests-jasmine/internal-only-nested/test.spec.ts create mode 100644 tests-jasmine/internal-only/fixtures.components.ts create mode 100644 tests-jasmine/internal-only/fixtures.modules.ts create mode 100644 tests-jasmine/internal-only/test.spec.ts create mode 100644 tests-jasmine/internal-vs-external/fixtures.components.ts create mode 100644 tests-jasmine/internal-vs-external/fixtures.modules.ts create mode 100644 tests-jasmine/internal-vs-external/test.spec.ts create mode 100644 tests-jasmine/mock-builder-by-directive/fixtures.components.ts create mode 100644 tests-jasmine/mock-builder-by-directive/fixtures.modules.ts create mode 100644 tests-jasmine/mock-builder-by-directive/test.spec.ts create mode 100644 tests-jasmine/mock-builder-keeps-application-module/fixtures.components.ts create mode 100644 tests-jasmine/mock-builder-keeps-application-module/fixtures.modules.ts create mode 100644 tests-jasmine/mock-builder-keeps-application-module/test.spec.ts create mode 100644 tests-jasmine/module-with-factory-tokens/fixtures.ts create mode 100644 tests-jasmine/module-with-factory-tokens/test.spec.ts create mode 100644 tests-jasmine/module-with-tokens/fixtures.ts create mode 100644 tests-jasmine/module-with-tokens/test.spec.ts create mode 100644 tests-jasmine/nested-before-each/fixtures.components.ts create mode 100644 tests-jasmine/nested-before-each/fixtures.modules.ts create mode 100644 tests-jasmine/nested-before-each/test.spec.ts create mode 100644 tests-jasmine/normal-usage-after-mock-builder/fixtures.components.ts create mode 100644 tests-jasmine/normal-usage-after-mock-builder/fixtures.modules.ts create mode 100644 tests-jasmine/normal-usage-after-mock-builder/fixtures.services.ts create mode 100644 tests-jasmine/normal-usage-after-mock-builder/test.spec.ts create mode 100644 tests-jasmine/provider-with-dependency/fixtures.components.ts create mode 100644 tests-jasmine/provider-with-dependency/fixtures.modules.ts create mode 100644 tests-jasmine/provider-with-dependency/fixtures.services.ts create mode 100644 tests-jasmine/provider-with-dependency/test.spec.ts create mode 100644 tests-jasmine/rerender-rendered-content-child/fixtures.components.ts create mode 100644 tests-jasmine/rerender-rendered-content-child/fixtures.module.ts create mode 100644 tests-jasmine/rerender-rendered-content-child/test.spec.ts create mode 100644 tests-jasmine/shared-mocked-module/fixtures.components.ts create mode 100644 tests-jasmine/shared-mocked-module/fixtures.modules.ts create mode 100644 tests-jasmine/shared-mocked-module/test.spec.ts create mode 100644 tests-jest/control-value-accessor-form-control/fixtures.ts create mode 100644 tests-jest/control-value-accessor-form-control/test.spec.ts create mode 100644 tests-jest/control-value-accessor-ng-model/fixtures.ts create mode 100644 tests-jest/control-value-accessor-ng-model/test.spec.ts create mode 100644 tests-jest/exports-only/fixtures.components.ts create mode 100644 tests-jest/exports-only/fixtures.modules.ts create mode 100644 tests-jest/exports-only/test.spec.ts create mode 100644 tests-jest/internal-only-nested/fixtures.components.ts create mode 100644 tests-jest/internal-only-nested/fixtures.modules.ts create mode 100644 tests-jest/internal-only-nested/test.spec.ts create mode 100644 tests-jest/internal-only/fixtures.components.ts create mode 100644 tests-jest/internal-only/fixtures.modules.ts create mode 100644 tests-jest/internal-only/test.spec.ts create mode 100644 tests-jest/internal-vs-external/fixtures.components.ts create mode 100644 tests-jest/internal-vs-external/fixtures.modules.ts create mode 100644 tests-jest/internal-vs-external/test.spec.ts create mode 100644 tests-jest/mock-builder-by-directive/fixtures.components.ts create mode 100644 tests-jest/mock-builder-by-directive/fixtures.modules.ts create mode 100644 tests-jest/mock-builder-by-directive/test.spec.ts create mode 100644 tests-jest/mock-builder-keeps-application-module/fixtures.components.ts create mode 100644 tests-jest/mock-builder-keeps-application-module/fixtures.modules.ts create mode 100644 tests-jest/mock-builder-keeps-application-module/test.spec.ts create mode 100644 tests-jest/module-with-factory-tokens/fixtures.ts create mode 100644 tests-jest/module-with-factory-tokens/test.spec.ts create mode 100644 tests-jest/module-with-tokens/fixtures.ts create mode 100644 tests-jest/module-with-tokens/test.spec.ts create mode 100644 tests-jest/nested-before-each/fixtures.components.ts create mode 100644 tests-jest/nested-before-each/fixtures.modules.ts create mode 100644 tests-jest/nested-before-each/test.spec.ts create mode 100644 tests-jest/normal-usage-after-mock-builder/fixtures.components.ts create mode 100644 tests-jest/normal-usage-after-mock-builder/fixtures.modules.ts create mode 100644 tests-jest/normal-usage-after-mock-builder/fixtures.services.ts create mode 100644 tests-jest/normal-usage-after-mock-builder/test.spec.ts create mode 100644 tests-jest/provider-with-dependency/fixtures.components.ts create mode 100644 tests-jest/provider-with-dependency/fixtures.modules.ts create mode 100644 tests-jest/provider-with-dependency/fixtures.services.ts create mode 100644 tests-jest/provider-with-dependency/test.spec.ts create mode 100644 tests-jest/rerender-rendered-content-child/fixtures.components.ts create mode 100644 tests-jest/rerender-rendered-content-child/fixtures.module.ts create mode 100644 tests-jest/rerender-rendered-content-child/test.spec.ts create mode 100644 tests-jest/shared-mocked-module/fixtures.components.ts create mode 100644 tests-jest/shared-mocked-module/fixtures.modules.ts create mode 100644 tests-jest/shared-mocked-module/test.spec.ts diff --git a/README.md b/README.md index 084afef83b..b681012b52 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,33 @@ Tested on: Sure, you could flip a flag on schema errors to make your component dependencies not matter. Or you could use this to mock them out and have the ability to assert on their inputs or emit on an output to assert on a side effect. +For an easy start check the [MockBuilder](#mockbuilder) first. + - [jasmine examples](https://github.com/ike18t/ng-mocks/tree/master/examples-jasmine) - [jest examples](https://github.com/ike18t/ng-mocks/tree/master/examples-jest) * [jasmine e2e tests](https://github.com/ike18t/ng-mocks/tree/master/tests-jasmine) * [jest e2e tests](https://github.com/ike18t/ng-mocks/tree/master/tests-jest) +### Sections: + +- [MockModule](#mockmodule) +- [MockComponent](#mockcomponents) +- [MockDirective](#mockdirectives) +- [MockPipe](#mockpipes) +- [MockDeclaration](#mockdeclarations) + +* [MockBuilder](#mockbuilder) - facilitate creation of a mocked environment +* [MockRender](#mockrender) - facilitate render of components +* [MockHelper](#mockhelper) - facilitate extraction of directives of an element + +- [Reactive Forms Components](#mocked-reactive-forms-components) +- [Structural Components](#usage-example-of-structural-directives) +- [Auto Spy](#auto-spy) +- [More examples](#other-examples-of-tests) + +--- + ## MockComponent(s) - Mocked component with the same selector @@ -37,7 +58,8 @@ Or you could use this to mock them out and have the ability to assert on their i - \_\_simulateTouch - calls `onTouched` on the mocked component bound to a FormControl - exportAs support -### Usage Example +
Click to see a usage example +

```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -113,7 +135,7 @@ describe('MockComponent', () => { `); - // injected ng-content says as it was. + // injected ng-content stays as it was. const mockedNgContent = localFixture.point.nativeElement.innerHTML; expect(mockedNgContent).toContain('

inside content

'); @@ -128,6 +150,11 @@ describe('MockComponent', () => { }); ``` +

+
+ +--- + ## MockDirective(s) - Mocked directive with the same selector @@ -135,7 +162,8 @@ describe('MockComponent', () => { - Each directive instance has its own EventEmitter instances for outputs - exportAs support -### Usage Example of Attribute Directives +
Click to see a usage example of Attribute Directives +

```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -185,9 +213,13 @@ describe('MockDirective', () => { }); ``` -### Usage Example of Structural Directives +

+
-It's important to render a structural directive first with right context, +
Click to see a usage example of Structural Directives +

+ +It's important to render a structural directive first with the right context, when assertions should be done on its nested elements. ```typescript @@ -237,6 +269,11 @@ describe('MockDirective', () => { }); ``` +

+
+ +--- + ## MockPipe(s) - Mocked pipe with the same name. @@ -245,7 +282,8 @@ describe('MockDirective', () => { Personally, I found the best thing to do for assertions is to override the transform to write the args so that I can assert on the arguments. -### Usage Example +
Click to see a usage example +

```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -280,13 +318,19 @@ describe('MockPipe', () => { }); ``` +

+
+ +--- + ## Mocked Reactive Forms Components - Set value on the formControl by calling \_\_simulateChange - Set touched on the formControl by calling \_\_simulateTouch - Use the `MockedComponent` type to stay typesafe: `MockedComponent` -### Usage Example +
Click to see a usage example +

```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -327,10 +371,17 @@ describe('MockReactiveForms', () => { }); ``` +

+
+ +--- + ## MockDeclaration(s) It figures out if it is a component, directive, or pipe and mocks it for you +--- + ## MockModule - Mocks all components, directives, and pipes using MockDeclaration @@ -339,7 +390,8 @@ It figures out if it is a component, directive, or pipe and mocks it for you For providers I typically will use TestBed.get(SomeProvider) and extend it using a library like [ts-mocks](https://www.npmjs.com/package/ts-mocks). -### Usage Example +
Click to see a usage example +

```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -369,6 +421,179 @@ describe('MockModule', () => { }); ``` +

+
+ +--- + +## MockBuilder + +The simplest way to mock everything, but not the component for testing is usage of `MockBuilder`. +Check `examples/MockBuilder/` for real examples. It's useful together with [MockRender](#MockRender). + +
Click to see a usage example +

+ +```typescript +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +describe('MockBuilder:simple', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + // mocking configuration here + .build(); + // now ngModule is + // { + // imports: [MockModule(MyModule)], // but MyComponent wasn't mocked for the testing purposes. + // } + // and we can simply pass it to the TestBed. + return TestBed.configureTestingModule(ngModule).compileComponents(); + }); + + it('should render content ignoring all dependencies', () => { + const fixture = MockRender(MyComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('

My Content
'); + }); +}); +``` + +

+
+ +
Click to see a detailed information +

+ +```typescript +import { MockBuilder } from 'ng-mocks'; + +// Mocks everything in MyModule (imports, declarations, providers) +// but keeps MyComponent as it is. +const ngModule = MockBuilder(MyComponent, MyModule).build(); + +// The same as code above. +const ngModule = MockBuilder().keep(MyComponent, { export: true }).mock(MyModule).build(); + +// If we want to keep a module, component, directive, pipe or provider as it is (not mocking). +// We should use .keep. +const ngModule = MockBuilder(MyComponent, MyModule) + .keep(SomeModule) + .keep(SomeComponent) + .keep(SomeDirective) + .keep(SomePipe) + .keep(SomeDependency) + .keep(SomeInjectionToken) + .build(); +// If we want to mock something, even a part of a kept module we should use .mock. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(SomeModule) + .mock(SomeComponent) + .mock(SomeDirective) + .mock(SomePipe) + .mock(SomeDependency) + .mock(SomeInjectionToken) + .build(); +// If we want to replace something with something we should use .replace. +// The replacement has to be decorated with the same decorator as the source. +// It's impossible to replace a provider or a service, we should use .provide or .mock for that. +const ngModule = MockBuilder(MyComponent, MyModule) + .replace(HttpClientModule, HttpClientTestingModule) + .replace(SomeComponent, SomeOtherComponent) + .replace(SomeDirective, SomeOtherDirective) + .replace(SomePipe, SomeOtherPipe) + .build(); +// For pipes we can set its handler as the 2nd parameter of .mock too. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(SomePipe, value => 'My Custom Content') + .build(); +// If we want to add or replace a provider or a service we should use .provide. +// It has the same interface as a regular provider. +const ngModule = MockBuilder(MyComponent, MyModule) + .provide(MyService) + .provide([SomeService1, SomeService2]) + .provide({ provide: SomeComponent3, useValue: anything1 }) + .provide({ provide: SOME_TOKEN, useFactory: () => anything2 }) + .build(); +// If we need to mock, or to use useValue we can use .mock for that. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(MyService) + .mock(SomeService1) + .mock(SomeService2) + .mock(SomeComponent3, anything1) + .mock(SOME_TOKEN, anything2) + .build(); +// Anytime we can change our decision. +// The last action on the same object wins. +const ngModule = MockBuilder(MyComponent, MyModule) + .keep(SomeModule) + .mock(SomeModule) + .keep(SomeModule) + .mock(SomeModule) + .build(); +// If we want to test a component, directive or pipe which wasn't exported +// we should mark it as an 'export'. +// Doesn't matter how deep it is. It will be exported to the level of TestingModule. +const ngModule = MockBuilder(MyComponent, MyModule) + .keep(SomeModuleComponentDirectivePipeProvider1, { + export: true, + }) + .build(); +// By default all definitions (kept and mocked) are added to the TestingModule +// if they are not dependency of another definition. +// Modules are added as imports to the TestingModule. +// Components, Directive, Pipes are added as declarations to the TestingModule. +// Providers and Services are added as providers to the TestingModule. +// If we don't want something to be added to the TestingModule at all +// we should mark it as a 'dependency'. +const ngModule = MockBuilder(MyComponent, MyModule) + .keep(SomeModuleComponentDirectivePipeProvider1, { + dependency: true, + }) + .mock(SomeModuleComponentDirectivePipeProvider1, { + dependency: true, + }) + .replace(SomeModuleComponentDirectivePipeProvider1, anything1, { + dependency: true, + }) + .build(); +// Imagine we want to render a structural directive by default. +// Now we can do that via adding a 'render' flag in its config. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(MyDirective, { + render: true, + }) + .build(); +// Imagine the directive has own context and variables. +// Then instead of flag we can set its context. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(MyDirective, { + render: { + $implicit: something1, + variables: { something2: something3 }, + }, + }) + .build(); +// If we use ContentChild in a component and we want to render it by default too +// we should use its id for that in the same way as for a mocked directive. +const ngModule = MockBuilder(MyComponent, MyModule) + .mock(MyDirective, { + render: { + blockId: true, + blockWithContext: { + $implicit: something1, + variables: { something2: something3 }, + }, + }, + }) + .build(); +``` + +

+
+ +--- + ## MockRender Provides a simple way to render anything for ease of testing directives, pipes, `@Inputs`, `@Outputs`, `@ContentChild` of a component, etc. @@ -386,7 +611,8 @@ It is useful if you want to mock system tokens / services such as `APP_INITIALIZ And don't forget to call `fixture.detectChanges()` and / or `await fixture.whenStable()` to trigger updates. -### Usage Example +
Click to see a usage example +

```typescript import { TestBed } from '@angular/core/testing'; @@ -455,6 +681,11 @@ describe('MockRender', () => { }); ``` +

+
+ +--- + ## ngMocks ngMocks provides functions to get attribute and structural directives from an element, find components and mock objects. @@ -473,6 +704,8 @@ ngMocks provides functions to get attribute and structural directives from an el * ngMocks.stub(service, methods) * ngMocks.stub(service, property, 'get' | 'set') +- ngMocks.flushTestBed() + ```typescript // returns attribute or structural directive // which belongs to current element. @@ -580,6 +813,8 @@ In case of jest add it to `src/setupJest.ts`. import 'ng-mocks/dist/jest'; ``` +--- + ## Find an issue or have a request? Report it as an issue or submit a PR. I'm open to contributions. diff --git a/e2e/angular5/src/test.ts b/e2e/angular5/src/test.ts index b39b257312..e660cee085 100644 --- a/e2e/angular5/src/test.ts +++ b/e2e/angular5/src/test.ts @@ -6,6 +6,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@ang import 'ng-mocks/dist/jasmine'; declare const require: any; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/e2e/angular6/src/test.ts b/e2e/angular6/src/test.ts index b39b257312..e660cee085 100644 --- a/e2e/angular6/src/test.ts +++ b/e2e/angular6/src/test.ts @@ -6,6 +6,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@ang import 'ng-mocks/dist/jasmine'; declare const require: any; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/e2e/angular7/src/test.ts b/e2e/angular7/src/test.ts index b39b257312..e660cee085 100644 --- a/e2e/angular7/src/test.ts +++ b/e2e/angular7/src/test.ts @@ -6,6 +6,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@ang import 'ng-mocks/dist/jasmine'; declare const require: any; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/e2e/angular8/src/test.ts b/e2e/angular8/src/test.ts index b39b257312..e660cee085 100644 --- a/e2e/angular8/src/test.ts +++ b/e2e/angular8/src/test.ts @@ -6,6 +6,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@ang import 'ng-mocks/dist/jasmine'; declare const require: any; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/e2e/angular9-ivy-false/src/test.ts b/e2e/angular9-ivy-false/src/test.ts index 99aa337871..ecf8a36b63 100644 --- a/e2e/angular9-ivy-false/src/test.ts +++ b/e2e/angular9-ivy-false/src/test.ts @@ -15,6 +15,7 @@ declare const require: { (id: string): T; }; }; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/e2e/angular9-ivy-true/src/test.ts b/e2e/angular9-ivy-true/src/test.ts index 99aa337871..ecf8a36b63 100644 --- a/e2e/angular9-ivy-true/src/test.ts +++ b/e2e/angular9-ivy-true/src/test.ts @@ -15,6 +15,7 @@ declare const require: { (id: string): T; }; }; +jasmine.getEnv().allowRespy(true); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/examples-jasmine/MockBuilder/MockBuilder.spec.ts b/examples-jasmine/MockBuilder/MockBuilder.spec.ts new file mode 100644 index 0000000000..6ddcc8ae47 --- /dev/null +++ b/examples-jasmine/MockBuilder/MockBuilder.spec.ts @@ -0,0 +1,223 @@ +import { HttpBackend, HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { ModuleWeDontWantToMock, ModuleWeWantToMockBesidesMyModule, MyModule } from './fixtures.modules'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, + TheSameAsAnyProvider, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +describe('MockBuilder:simple', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + // mocking configuration here + .build(); + + // now ngModule is + // { + // imports: [MockModule(MyModule)], // but MyComponent wasn't mocked for the testing purposes. + // } + // and we can simply pass it to the TestBed. + return TestBed.configureTestingModule(ngModule).compileComponents(); + }); + + it('should render content ignoring all dependencies', () => { + const fixture = MockRender(MyComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('
My Content
'); + }); +}); + +describe('MockBuilder:deep', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + .mock(ComponentContentChild, { + render: { + block: { + $implicit: '-$implicit-', + variables: { a: { z: 'b' } }, + }, + }, + }) + + .keep(ModuleWeDontWantToMock, { + dependency: true, + }) + .keep(ComponentWeDontWantToMock, { + dependency: true, + }) + .keep(DirectiveWeDontWantToMock, { + dependency: true, + }) + .keep(PipeWeDontWantToMock, { + dependency: true, + }) + .keep(ServiceWeDontWantToMock) + .keep(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) + + // The same can be done with Components, Directives and Pipes. + // For Providers use .provider() or .mock(). + .replace(HttpClientModule, HttpClientTestingModule, { + dependency: true, + }) + + .mock(ModuleWeWantToMockBesidesMyModule, { + dependency: true, + }) + .mock(ComponentWeWantToMock, { + dependency: true, + }) + .mock(DirectiveWeWantToMock, { + dependency: true, + render: { + $implicit: { a: '$' }, + variables: { a: { b: 'b' } }, + }, + }) + .mock(PipeWeWantToMock, { + dependency: true, + }) + .mock(ServiceWeWantToMock) // makes all methods an empty function + .mock(INJECTION_TOKEN_WE_WANT_TO_MOCK) // makes its value undefined + + .mock(PipeWeWantToCustomize, value => 'My Custom Result') + .mock(PipeWeWantToRestore, value => 'My Restored Pipe') + .mock(ServiceWeWantToCustomize, { prop1: true, getName: () => 'My Customized String' }) + .mock(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, 'My_Token') + + // All providers will be set into the TestModule. + .provide({ + provide: AnythingWeWant1, + useValue: new TheSameAsAnyProvider(), + }) + .provide({ + provide: AnythingWeWant2, + useFactory: () => new TheSameAsAnyProvider(), + }) + .provide(MyCustomProvider1) + .provide([MyCustomProvider2, MyCustomProvider3]) + + // Now the pipe won't be mocked. + .keep(PipeWeWantToRestore) + + // Extra configuration. + .keep(MyDirective) + .keep(MyPipe) + .mock(MyService1) + .keep(MyService2) + + // Even it belongs to the module that is marked as kept, the component will be mocked and replaced. + .mock(MyComponent3) + + // and now we want to build our NgModule. + .build(); + TestBed.configureTestingModule(ngModule); + + // Extra configuration + TestBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + TestBed.overrideTemplate(MyComponent2, 'More callbacks'); + + return TestBed.compileComponents(); + }); + + it('should render', inject([HttpBackend], (httpBackend: HttpBackend) => { + const fixture = MockRender(MyComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML.replace(//gm, ''); + expect(content).toContain('
My Content
'); + + expect(content).toContain('
MyComponent1: If we need to tune testBed
'); + expect(content).toContain('
MyComponent2: More callbacks
'); + expect(content).toContain('
MyComponent3:
'); + expect(content).toContain('
ComponentWeDontWantToMock: ComponentWeDontWantToMock
'); + expect(content).toContain('
ComponentWeWantToMock:
'); + expect(content).toContain('
ComponentStructural: -$implicit- b
'); + + expect(content).toContain('
MyDirective:
'); + expect(content).toContain('
DirectiveWeDontWantToMock:
'); + expect(content).toContain('DirectiveWeWantToMock 1: render b'); + expect(content).toContain('DirectiveWeWantToMock 2: render $'); + + expect(content).toContain('
MyPipe: MyPipe:text
'); + expect(content).toContain('
PipeWeDontWantToMock: PipeWeDontWantToMock:text
'); + expect(content).toContain('
PipeWeWantToMock:
'); + expect(content).toContain('
PipeWeWantToCustomize: My Custom Result
'); + expect(content).toContain('
PipeWeWantToRestore: PipeWeWantToRestore:text
'); + + expect(content).toContain('
INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK
'); + expect(content).toContain('
INJECTION_TOKEN_WE_WANT_TO_MOCK:
'); + expect(content).toContain('
INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE: My_Token
'); + + expect(content).toContain('
anythingWeWant1: TheSameAsAnyProvider
'); + expect(content).toContain('
anythingWeWant2: TheSameAsAnyProvider
'); + expect(content).toContain('
myCustomProvider1: MyCustomProvider1
'); + expect(content).toContain('
myCustomProvider2: MyCustomProvider2
'); + expect(content).toContain('
myCustomProvider3: MyCustomProvider3
'); + + expect(content).toContain('
myService1:
'); + expect(content).toContain('
myService2: MyService2
'); + expect(content).toContain('
serviceWeDontWantToMock: ServiceWeDontWantToMock
'); + expect(content).toContain('
serviceWeWantToCustomize: My Customized String
'); + expect(content).toContain('
serviceWeWantToMock:
'); + + // Checking that replacement works. + expect(httpBackend.constructor).toBeDefined(); + expect(httpBackend.constructor.name).toEqual('HttpClientTestingBackend'); + })); +}); + +describe('MockBuilder:promise', () => { + beforeEach(() => + MockBuilder() + .keep(MyComponent1) + .keep(MyComponent2) + + // In case if you need extra customization of TestBed in promise way. + .beforeCompileComponents(testBed => { + testBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + }) + .beforeCompileComponents(testBed => { + testBed.overrideTemplate(MyComponent2, 'More callbacks'); + }) + ); + + it('should render content ignoring all dependencies', () => { + const fixture = MockRender(''); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('If we need to tune testBed'); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('More callbacks'); + }); +}); diff --git a/examples-jasmine/MockBuilder/fixtures.components.ts b/examples-jasmine/MockBuilder/fixtures.components.ts new file mode 100644 index 0000000000..ac437e30d9 --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.components.ts @@ -0,0 +1,159 @@ +import { Component, ContentChild, Inject, Input, Optional, TemplateRef } from '@angular/core'; + +import { staticFalse } from '../../tests-jasmine'; + +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +@Component({ + selector: 'component-structural', + template: ` +
+ +
+ `, +}) +export class ComponentContentChild { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; + @Input() items?: T[]; +} + +@Component({ + selector: 'my-component', + template: ` +
My Content
+ +
MyComponent1:
+
MyComponent2:
+
MyComponent3:
+
ComponentWeDontWantToMock:
+
ComponentWeWantToMock:
+ +
MyDirective:
+
DirectiveWeDontWantToMock:
+
+ DirectiveWeWantToMock 1: render {{ z.b }} +
+
+ DirectiveWeWantToMock 2: render {{ z.a }} +
+ +
MyPipe: {{ 'text' | MyPipe }}
+
PipeWeDontWantToMock: {{ 'text' | PipeWeDontWantToMock }}
+
PipeWeWantToMock: {{ 'text' | PipeWeWantToMock }}
+
PipeWeWantToCustomize: {{ 'text' | PipeWeWantToCustomize }}
+
PipeWeWantToRestore: {{ 'text' | PipeWeWantToRestore }}
+ +
INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK: {{ t1v }}
+
INJECTION_TOKEN_WE_WANT_TO_MOCK: {{ t2v }}
+
INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE: {{ t3v }}
+ +
anythingWeWant1: {{ anythingWeWant1?.getName() }}
+
anythingWeWant2: {{ anythingWeWant2?.getName() }}
+
myCustomProvider1: {{ myCustomProvider1?.getName() }}
+
myCustomProvider2: {{ myCustomProvider2?.getName() }}
+
myCustomProvider3: {{ myCustomProvider3?.getName() }}
+ +
myService1: {{ myService1?.getName() }}
+
myService2: {{ myService2?.getName() }}
+
serviceWeDontWantToMock: {{ serviceWeDontWantToMock?.getName() }}
+
serviceWeWantToCustomize: {{ serviceWeWantToCustomize?.getName() }}
+
serviceWeWantToMock: {{ serviceWeWantToMock?.getName() }}
+ + + +
ComponentStructural: {{ value }} {{ b.z }}
+
+
+ `, +}) +export class MyComponent { + public readonly anythingWeWant1: AnythingWeWant1; + public readonly anythingWeWant2: AnythingWeWant2; + public readonly myCustomProvider1: MyCustomProvider1; + public readonly myCustomProvider2: MyCustomProvider2; + public readonly myCustomProvider3: MyCustomProvider3; + public readonly myService1: MyService1; + public readonly myService2: MyService2; + public readonly serviceWeDontWantToMock: ServiceWeDontWantToMock; + public readonly serviceWeWantToCustomize: ServiceWeWantToCustomize; + public readonly serviceWeWantToMock: ServiceWeWantToMock; + public readonly t1v: string; + public readonly t2v: string; + public readonly t3v: string; + + constructor( + @Optional() @Inject(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) t1: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_MOCK) t2: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE) t3: string, + @Optional() anythingWeWant1: AnythingWeWant1, + @Optional() anythingWeWant2: AnythingWeWant2, + @Optional() myCustomProvider1: MyCustomProvider1, + @Optional() myCustomProvider2: MyCustomProvider2, + @Optional() myCustomProvider3: MyCustomProvider3, + @Optional() myService1: MyService1, + @Optional() myService2: MyService2, + @Optional() serviceWeDontWantToMock: ServiceWeDontWantToMock, + @Optional() serviceWeWantToMock: ServiceWeWantToMock, + @Optional() serviceWeWantToCustomize: ServiceWeWantToCustomize + ) { + this.t1v = t1; + this.t2v = t2; + this.t3v = t3; + this.anythingWeWant1 = anythingWeWant1; + this.anythingWeWant2 = anythingWeWant2; + this.myCustomProvider1 = myCustomProvider1; + this.myCustomProvider2 = myCustomProvider2; + this.myCustomProvider3 = myCustomProvider3; + this.myService1 = myService1; + this.myService2 = myService2; + this.serviceWeDontWantToMock = serviceWeDontWantToMock; + this.serviceWeWantToCustomize = serviceWeWantToCustomize; + this.serviceWeWantToMock = serviceWeWantToMock; + } +} + +@Component({ + selector: 'component-1', + template: 'MyComponent1', +}) +export class MyComponent1 {} + +@Component({ + selector: 'component-2', + template: 'MyComponent2', +}) +export class MyComponent2 {} + +@Component({ + selector: 'component-3', + template: 'MyComponent3', +}) +export class MyComponent3 {} + +@Component({ + selector: 'dont-want', + template: 'ComponentWeDontWantToMock', +}) +export class ComponentWeDontWantToMock {} + +@Component({ + selector: 'do-want', + template: 'ComponentWeWantToMock', +}) +export class ComponentWeWantToMock {} diff --git a/examples-jasmine/MockBuilder/fixtures.directives.ts b/examples-jasmine/MockBuilder/fixtures.directives.ts new file mode 100644 index 0000000000..6dbb2e86f8 --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.directives.ts @@ -0,0 +1,16 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: 'MyDirective', +}) +export class MyDirective {} + +@Directive({ + selector: 'WeDontWantToMock', +}) +export class DirectiveWeDontWantToMock {} + +@Directive({ + selector: '[WeWantToMock]', +}) +export class DirectiveWeWantToMock {} diff --git a/examples-jasmine/MockBuilder/fixtures.modules.ts b/examples-jasmine/MockBuilder/fixtures.modules.ts new file mode 100644 index 0000000000..264824c22b --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.modules.ts @@ -0,0 +1,88 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + exports: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + providers: [ + ServiceWeDontWantToMock, + ServiceWeWantToMock, + { + provide: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE', + }, + ], +}) +export class ModuleWeWantToMockBesidesMyModule {} + +@NgModule({ + declarations: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + exports: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + imports: [CommonModule], +}) +export class ModuleWeDontWantToMock {} + +@NgModule({ + declarations: [MyComponent, MyDirective, MyPipe], + exports: [MyComponent, MyDirective, MyPipe], + imports: [HttpClientModule, ModuleWeWantToMockBesidesMyModule, ModuleWeDontWantToMock], + providers: [MyService1, MyService2, ServiceWeWantToCustomize], +}) +export class MyModule {} diff --git a/examples-jasmine/MockBuilder/fixtures.pipes.ts b/examples-jasmine/MockBuilder/fixtures.pipes.ts new file mode 100644 index 0000000000..a1034148cb --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.pipes.ts @@ -0,0 +1,56 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'MyPipe', +}) +export class MyPipe implements PipeTransform { + protected prefix = 'MyPipe:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeDontWantToMock', +}) +export class PipeWeDontWantToMock implements PipeTransform { + protected prefix = 'PipeWeDontWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToMock', +}) +export class PipeWeWantToMock implements PipeTransform { + protected prefix = 'PipeWeWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToCustomize', +}) +export class PipeWeWantToCustomize implements PipeTransform { + protected prefix = 'PipeWeWantToCustomize:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToRestore', +}) +export class PipeWeWantToRestore implements PipeTransform { + protected prefix = 'PipeWeWantToRestore:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} diff --git a/examples-jasmine/MockBuilder/fixtures.services.ts b/examples-jasmine/MockBuilder/fixtures.services.ts new file mode 100644 index 0000000000..4fb36c910a --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.services.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MyService1 { + protected value = 'MyService1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyService2 { + protected value = 'MyService2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeDontWantToMock { + protected value = 'ServiceWeDontWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToMock { + protected value = 'ServiceWeWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToCustomize { + protected value = 'ServiceWeWantToCustomize'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant1 { + protected value = 'AnythingWeWant1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class TheSameAsAnyProvider { + protected value = 'TheSameAsAnyProvider'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant2 { + protected value = 'AnythingWeWant2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider1 { + protected value = 'MyCustomProvider1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider2 { + protected value = 'MyCustomProvider2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider3 { + protected value = 'MyCustomProvider3'; + + public getName() { + return this.value; + } +} diff --git a/examples-jasmine/MockBuilder/fixtures.tokens.ts b/examples-jasmine/MockBuilder/fixtures.tokens.ts new file mode 100644 index 0000000000..8d527302f7 --- /dev/null +++ b/examples-jasmine/MockBuilder/fixtures.tokens.ts @@ -0,0 +1,7 @@ +import { InjectionToken } from '@angular/core'; + +export const INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE'); diff --git a/examples-jasmine/MockDirective-Structural/MockDirective.spec.ts b/examples-jasmine/MockDirective-Structural/MockDirective.spec.ts index d24960be6f..c3dcaa886a 100644 --- a/examples-jasmine/MockDirective-Structural/MockDirective.spec.ts +++ b/examples-jasmine/MockDirective-Structural/MockDirective.spec.ts @@ -30,11 +30,11 @@ describe('MockDirective', () => { >; // now we assert that nothing has been rendered inside of the structural directive by default. - expect(fixture.debugElement.nativeElement.innerText).not.toContain('content'); + expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('>content<'); // and now we render it manually. mockedDirectiveInstance.__render(); - expect(fixture.debugElement.nativeElement.innerText).toContain('content'); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('>content<'); // let's pretend Dependency Directive (unmocked) has 'someInput' as an input // the input value will be passed into the mocked directive so you can assert on it diff --git a/examples-jasmine/MockReactiveForms/MockReactiveForms.spec.ts b/examples-jasmine/MockReactiveForms/MockReactiveForms.spec.ts index e2e97f67a0..93f1440b57 100644 --- a/examples-jasmine/MockReactiveForms/MockReactiveForms.spec.ts +++ b/examples-jasmine/MockReactiveForms/MockReactiveForms.spec.ts @@ -31,6 +31,6 @@ describe('MockReactiveForms', () => { spyOn(mockedReactiveFormComponent, 'writeValue'); component.formControl.setValue('bar'); - expect(mockedReactiveFormComponent.writeValue as any).toHaveBeenCalledWith('bar'); + expect(mockedReactiveFormComponent.writeValue).toHaveBeenCalledWith('bar'); }); }); diff --git a/examples-jasmine/NG_MOCKS/NG_MOCKS.spec.ts b/examples-jasmine/NG_MOCKS/NG_MOCKS.spec.ts new file mode 100644 index 0000000000..6c3a7cdf06 --- /dev/null +++ b/examples-jasmine/NG_MOCKS/NG_MOCKS.spec.ts @@ -0,0 +1,122 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { isMockedNgDefOf, MockBuilder, NG_MOCKS } from 'ng-mocks'; + +import { + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock } from './fixtures.directives'; +import { ModuleWeDontWantToMock, ModuleWeWantToMockBesidesMyModule, MyModule } from './fixtures.modules'; +import { PipeWeDontWantToMock, PipeWeWantToMock, PipeWeWantToRestore } from './fixtures.pipes'; +import { ServiceWeDontWantToMock, ServiceWeWantToCustomize, ServiceWeWantToMock } from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +describe('NG_MOCKS:deep', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + .keep(ModuleWeDontWantToMock) + .keep(ComponentWeDontWantToMock) + .keep(DirectiveWeDontWantToMock) + .keep(PipeWeDontWantToMock) + .keep(ServiceWeDontWantToMock) + .keep(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) + + .replace(HttpClientModule, HttpClientTestingModule) + + .mock(ModuleWeWantToMockBesidesMyModule) + .mock(ComponentWeWantToMock) + .mock(DirectiveWeWantToMock) + .mock(PipeWeWantToMock) + .mock(ServiceWeWantToMock) // makes all methods an empty function + .mock(INJECTION_TOKEN_WE_WANT_TO_MOCK) // makes its value undefined + + .mock(ServiceWeWantToCustomize, { prop1: true, getName: () => 'My Customized String' }) + .mock(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, 'My_Token') + + // Now the pipe won't be mocked. + .keep(PipeWeWantToRestore) + + // Even it belongs to the module to keep it still will be mocked and replaced. + .mock(MyComponent3) + + // and now we want to build our NgModule. + .build(); + TestBed.configureTestingModule(ngModule); + + // Extra configuration + TestBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + TestBed.overrideTemplate(MyComponent2, 'More callbacks'); + + return TestBed.compileComponents(); + }); + + it('should contain mocks', inject([NG_MOCKS], (mocks: Map) => { + // main part + const myComponent = mocks.get(MyComponent); + expect(myComponent).toBe(MyComponent); + const myModule = mocks.get(MyModule); + expect(isMockedNgDefOf(myModule, MyModule, 'm')).toBeTruthy(); + + // keep + const componentWeDontWantToMock = mocks.get(ComponentWeDontWantToMock); + expect(componentWeDontWantToMock).toBe(ComponentWeDontWantToMock); + const directiveWeDontWantToMock = mocks.get(DirectiveWeDontWantToMock); + expect(directiveWeDontWantToMock).toBe(DirectiveWeDontWantToMock); + const pipeWeDontWantToMock = mocks.get(PipeWeDontWantToMock); + expect(pipeWeDontWantToMock).toBe(PipeWeDontWantToMock); + const serviceWeDontWantToMock = mocks.get(ServiceWeDontWantToMock); + expect(serviceWeDontWantToMock).toBe(ServiceWeDontWantToMock); + const injectionTokenWeDontWantToMock = mocks.get(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK); + expect(injectionTokenWeDontWantToMock).toBe(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK); + + // replace + const httpClientModule = mocks.get(HttpClientModule); + expect(httpClientModule).toBe(HttpClientTestingModule); + + // mock + const moduleWeWantToMockBesidesMyModule = mocks.get(ModuleWeWantToMockBesidesMyModule); + expect(isMockedNgDefOf(moduleWeWantToMockBesidesMyModule, ModuleWeWantToMockBesidesMyModule, 'm')).toBeTruthy(); + const componentWeWantToMock = mocks.get(ComponentWeWantToMock); + expect(isMockedNgDefOf(componentWeWantToMock, ComponentWeWantToMock, 'c')).toBeTruthy(); + const directiveWeWantToMock = mocks.get(DirectiveWeWantToMock); + expect(isMockedNgDefOf(directiveWeWantToMock, DirectiveWeWantToMock, 'd')).toBeTruthy(); + const pipeWeWantToMock = mocks.get(PipeWeWantToMock); + expect(isMockedNgDefOf(pipeWeWantToMock, PipeWeWantToMock, 'p')).toBeTruthy(); + const serviceWeWantToMock = mocks.get(ServiceWeWantToMock); + expect(serviceWeWantToMock).toBeDefined(); + expect(serviceWeWantToMock.useValue).toBeDefined(); + expect(serviceWeWantToMock.useValue.getName).toBeDefined(); + expect(serviceWeWantToMock.useValue.getName()).toBeUndefined(); + expect(mocks.has(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeDefined(); + expect(mocks.get(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeUndefined(); + + // customize + const serviceWeWantToCustomize = mocks.get(ServiceWeWantToCustomize); + expect(serviceWeWantToCustomize).toBeDefined(); + expect(serviceWeWantToCustomize.useValue).toBeDefined(); + expect(serviceWeWantToCustomize.useValue.getName).toBeDefined(); + expect(serviceWeWantToCustomize.useValue.getName()).toEqual('My Customized String'); + expect(serviceWeWantToCustomize.useValue.prop1).toEqual(true); + const injectionTokenWeWantToCustomize = mocks.get(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE); + expect(injectionTokenWeWantToCustomize).toBeDefined(); + expect(injectionTokenWeWantToCustomize.useValue).toEqual('My_Token'); + + // restore + const pipeWeWantToRestore = mocks.get(PipeWeWantToRestore); + expect(pipeWeWantToRestore).toBe(PipeWeWantToRestore); + + // mock nested + const myComponent3 = mocks.get(MyComponent3); + expect(isMockedNgDefOf(myComponent3, MyComponent3, 'c')).toBeTruthy(); + })); +}); diff --git a/examples-jasmine/NG_MOCKS/fixtures.components.ts b/examples-jasmine/NG_MOCKS/fixtures.components.ts new file mode 100644 index 0000000000..f0c247c9f3 --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.components.ts @@ -0,0 +1,110 @@ +import { Component, ContentChild, Inject, Input, Optional, TemplateRef } from '@angular/core'; + +import { staticFalse } from '../../tests-jasmine'; + +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +@Component({ + selector: 'component-structural', + template: '', +}) +export class ComponentContentChild { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; + @Input() items?: T[]; +} + +@Component({ + selector: 'my-component', + template: '', +}) +export class MyComponent { + public readonly anythingWeWant1: AnythingWeWant1; + public readonly anythingWeWant2: AnythingWeWant2; + public readonly myCustomProvider1: MyCustomProvider1; + public readonly myCustomProvider2: MyCustomProvider2; + public readonly myCustomProvider3: MyCustomProvider3; + public readonly myService1: MyService1; + public readonly myService2: MyService2; + public readonly serviceWeDontWantToMock: ServiceWeDontWantToMock; + public readonly serviceWeWantToCustomize: ServiceWeWantToCustomize; + public readonly serviceWeWantToMock: ServiceWeWantToMock; + public readonly t1v: string; + public readonly t2v: string; + public readonly t3v: string; + + constructor( + @Optional() @Inject(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) t1: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_MOCK) t2: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE) t3: string, + @Optional() anythingWeWant1: AnythingWeWant1, + @Optional() anythingWeWant2: AnythingWeWant2, + @Optional() myCustomProvider1: MyCustomProvider1, + @Optional() myCustomProvider2: MyCustomProvider2, + @Optional() myCustomProvider3: MyCustomProvider3, + @Optional() myService1: MyService1, + @Optional() myService2: MyService2, + @Optional() serviceWeDontWantToMock: ServiceWeDontWantToMock, + @Optional() serviceWeWantToMock: ServiceWeWantToMock, + @Optional() serviceWeWantToCustomize: ServiceWeWantToCustomize + ) { + this.t1v = t1; + this.t2v = t2; + this.t3v = t3; + this.anythingWeWant1 = anythingWeWant1; + this.anythingWeWant2 = anythingWeWant2; + this.myCustomProvider1 = myCustomProvider1; + this.myCustomProvider2 = myCustomProvider2; + this.myCustomProvider3 = myCustomProvider3; + this.myService1 = myService1; + this.myService2 = myService2; + this.serviceWeDontWantToMock = serviceWeDontWantToMock; + this.serviceWeWantToCustomize = serviceWeWantToCustomize; + this.serviceWeWantToMock = serviceWeWantToMock; + } +} + +@Component({ + selector: 'component-1', + template: 'MyComponent1', +}) +export class MyComponent1 {} + +@Component({ + selector: 'component-2', + template: 'MyComponent2', +}) +export class MyComponent2 {} + +@Component({ + selector: 'component-3', + template: 'MyComponent3', +}) +export class MyComponent3 {} + +@Component({ + selector: 'dont-want', + template: 'ComponentWeDontWantToMock', +}) +export class ComponentWeDontWantToMock {} + +@Component({ + selector: 'do-want', + template: 'ComponentWeWantToMock', +}) +export class ComponentWeWantToMock {} diff --git a/examples-jasmine/NG_MOCKS/fixtures.directives.ts b/examples-jasmine/NG_MOCKS/fixtures.directives.ts new file mode 100644 index 0000000000..6dbb2e86f8 --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.directives.ts @@ -0,0 +1,16 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: 'MyDirective', +}) +export class MyDirective {} + +@Directive({ + selector: 'WeDontWantToMock', +}) +export class DirectiveWeDontWantToMock {} + +@Directive({ + selector: '[WeWantToMock]', +}) +export class DirectiveWeWantToMock {} diff --git a/examples-jasmine/NG_MOCKS/fixtures.modules.ts b/examples-jasmine/NG_MOCKS/fixtures.modules.ts new file mode 100644 index 0000000000..264824c22b --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.modules.ts @@ -0,0 +1,88 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + exports: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + providers: [ + ServiceWeDontWantToMock, + ServiceWeWantToMock, + { + provide: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE', + }, + ], +}) +export class ModuleWeWantToMockBesidesMyModule {} + +@NgModule({ + declarations: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + exports: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + imports: [CommonModule], +}) +export class ModuleWeDontWantToMock {} + +@NgModule({ + declarations: [MyComponent, MyDirective, MyPipe], + exports: [MyComponent, MyDirective, MyPipe], + imports: [HttpClientModule, ModuleWeWantToMockBesidesMyModule, ModuleWeDontWantToMock], + providers: [MyService1, MyService2, ServiceWeWantToCustomize], +}) +export class MyModule {} diff --git a/examples-jasmine/NG_MOCKS/fixtures.pipes.ts b/examples-jasmine/NG_MOCKS/fixtures.pipes.ts new file mode 100644 index 0000000000..a1034148cb --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.pipes.ts @@ -0,0 +1,56 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'MyPipe', +}) +export class MyPipe implements PipeTransform { + protected prefix = 'MyPipe:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeDontWantToMock', +}) +export class PipeWeDontWantToMock implements PipeTransform { + protected prefix = 'PipeWeDontWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToMock', +}) +export class PipeWeWantToMock implements PipeTransform { + protected prefix = 'PipeWeWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToCustomize', +}) +export class PipeWeWantToCustomize implements PipeTransform { + protected prefix = 'PipeWeWantToCustomize:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToRestore', +}) +export class PipeWeWantToRestore implements PipeTransform { + protected prefix = 'PipeWeWantToRestore:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} diff --git a/examples-jasmine/NG_MOCKS/fixtures.services.ts b/examples-jasmine/NG_MOCKS/fixtures.services.ts new file mode 100644 index 0000000000..4fb36c910a --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.services.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MyService1 { + protected value = 'MyService1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyService2 { + protected value = 'MyService2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeDontWantToMock { + protected value = 'ServiceWeDontWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToMock { + protected value = 'ServiceWeWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToCustomize { + protected value = 'ServiceWeWantToCustomize'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant1 { + protected value = 'AnythingWeWant1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class TheSameAsAnyProvider { + protected value = 'TheSameAsAnyProvider'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant2 { + protected value = 'AnythingWeWant2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider1 { + protected value = 'MyCustomProvider1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider2 { + protected value = 'MyCustomProvider2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider3 { + protected value = 'MyCustomProvider3'; + + public getName() { + return this.value; + } +} diff --git a/examples-jasmine/NG_MOCKS/fixtures.tokens.ts b/examples-jasmine/NG_MOCKS/fixtures.tokens.ts new file mode 100644 index 0000000000..8d527302f7 --- /dev/null +++ b/examples-jasmine/NG_MOCKS/fixtures.tokens.ts @@ -0,0 +1,7 @@ +import { InjectionToken } from '@angular/core'; + +export const INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE'); diff --git a/examples-jest/MockBuilder/MockBuilder.spec.ts b/examples-jest/MockBuilder/MockBuilder.spec.ts new file mode 100644 index 0000000000..6ddcc8ae47 --- /dev/null +++ b/examples-jest/MockBuilder/MockBuilder.spec.ts @@ -0,0 +1,223 @@ +import { HttpBackend, HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { ModuleWeDontWantToMock, ModuleWeWantToMockBesidesMyModule, MyModule } from './fixtures.modules'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, + TheSameAsAnyProvider, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +describe('MockBuilder:simple', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + // mocking configuration here + .build(); + + // now ngModule is + // { + // imports: [MockModule(MyModule)], // but MyComponent wasn't mocked for the testing purposes. + // } + // and we can simply pass it to the TestBed. + return TestBed.configureTestingModule(ngModule).compileComponents(); + }); + + it('should render content ignoring all dependencies', () => { + const fixture = MockRender(MyComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('
My Content
'); + }); +}); + +describe('MockBuilder:deep', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + .mock(ComponentContentChild, { + render: { + block: { + $implicit: '-$implicit-', + variables: { a: { z: 'b' } }, + }, + }, + }) + + .keep(ModuleWeDontWantToMock, { + dependency: true, + }) + .keep(ComponentWeDontWantToMock, { + dependency: true, + }) + .keep(DirectiveWeDontWantToMock, { + dependency: true, + }) + .keep(PipeWeDontWantToMock, { + dependency: true, + }) + .keep(ServiceWeDontWantToMock) + .keep(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) + + // The same can be done with Components, Directives and Pipes. + // For Providers use .provider() or .mock(). + .replace(HttpClientModule, HttpClientTestingModule, { + dependency: true, + }) + + .mock(ModuleWeWantToMockBesidesMyModule, { + dependency: true, + }) + .mock(ComponentWeWantToMock, { + dependency: true, + }) + .mock(DirectiveWeWantToMock, { + dependency: true, + render: { + $implicit: { a: '$' }, + variables: { a: { b: 'b' } }, + }, + }) + .mock(PipeWeWantToMock, { + dependency: true, + }) + .mock(ServiceWeWantToMock) // makes all methods an empty function + .mock(INJECTION_TOKEN_WE_WANT_TO_MOCK) // makes its value undefined + + .mock(PipeWeWantToCustomize, value => 'My Custom Result') + .mock(PipeWeWantToRestore, value => 'My Restored Pipe') + .mock(ServiceWeWantToCustomize, { prop1: true, getName: () => 'My Customized String' }) + .mock(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, 'My_Token') + + // All providers will be set into the TestModule. + .provide({ + provide: AnythingWeWant1, + useValue: new TheSameAsAnyProvider(), + }) + .provide({ + provide: AnythingWeWant2, + useFactory: () => new TheSameAsAnyProvider(), + }) + .provide(MyCustomProvider1) + .provide([MyCustomProvider2, MyCustomProvider3]) + + // Now the pipe won't be mocked. + .keep(PipeWeWantToRestore) + + // Extra configuration. + .keep(MyDirective) + .keep(MyPipe) + .mock(MyService1) + .keep(MyService2) + + // Even it belongs to the module that is marked as kept, the component will be mocked and replaced. + .mock(MyComponent3) + + // and now we want to build our NgModule. + .build(); + TestBed.configureTestingModule(ngModule); + + // Extra configuration + TestBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + TestBed.overrideTemplate(MyComponent2, 'More callbacks'); + + return TestBed.compileComponents(); + }); + + it('should render', inject([HttpBackend], (httpBackend: HttpBackend) => { + const fixture = MockRender(MyComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML.replace(//gm, ''); + expect(content).toContain('
My Content
'); + + expect(content).toContain('
MyComponent1: If we need to tune testBed
'); + expect(content).toContain('
MyComponent2: More callbacks
'); + expect(content).toContain('
MyComponent3:
'); + expect(content).toContain('
ComponentWeDontWantToMock: ComponentWeDontWantToMock
'); + expect(content).toContain('
ComponentWeWantToMock:
'); + expect(content).toContain('
ComponentStructural: -$implicit- b
'); + + expect(content).toContain('
MyDirective:
'); + expect(content).toContain('
DirectiveWeDontWantToMock:
'); + expect(content).toContain('DirectiveWeWantToMock 1: render b'); + expect(content).toContain('DirectiveWeWantToMock 2: render $'); + + expect(content).toContain('
MyPipe: MyPipe:text
'); + expect(content).toContain('
PipeWeDontWantToMock: PipeWeDontWantToMock:text
'); + expect(content).toContain('
PipeWeWantToMock:
'); + expect(content).toContain('
PipeWeWantToCustomize: My Custom Result
'); + expect(content).toContain('
PipeWeWantToRestore: PipeWeWantToRestore:text
'); + + expect(content).toContain('
INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK
'); + expect(content).toContain('
INJECTION_TOKEN_WE_WANT_TO_MOCK:
'); + expect(content).toContain('
INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE: My_Token
'); + + expect(content).toContain('
anythingWeWant1: TheSameAsAnyProvider
'); + expect(content).toContain('
anythingWeWant2: TheSameAsAnyProvider
'); + expect(content).toContain('
myCustomProvider1: MyCustomProvider1
'); + expect(content).toContain('
myCustomProvider2: MyCustomProvider2
'); + expect(content).toContain('
myCustomProvider3: MyCustomProvider3
'); + + expect(content).toContain('
myService1:
'); + expect(content).toContain('
myService2: MyService2
'); + expect(content).toContain('
serviceWeDontWantToMock: ServiceWeDontWantToMock
'); + expect(content).toContain('
serviceWeWantToCustomize: My Customized String
'); + expect(content).toContain('
serviceWeWantToMock:
'); + + // Checking that replacement works. + expect(httpBackend.constructor).toBeDefined(); + expect(httpBackend.constructor.name).toEqual('HttpClientTestingBackend'); + })); +}); + +describe('MockBuilder:promise', () => { + beforeEach(() => + MockBuilder() + .keep(MyComponent1) + .keep(MyComponent2) + + // In case if you need extra customization of TestBed in promise way. + .beforeCompileComponents(testBed => { + testBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + }) + .beforeCompileComponents(testBed => { + testBed.overrideTemplate(MyComponent2, 'More callbacks'); + }) + ); + + it('should render content ignoring all dependencies', () => { + const fixture = MockRender(''); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('If we need to tune testBed'); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('More callbacks'); + }); +}); diff --git a/examples-jest/MockBuilder/fixtures.components.ts b/examples-jest/MockBuilder/fixtures.components.ts new file mode 100644 index 0000000000..4427415110 --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.components.ts @@ -0,0 +1,159 @@ +import { Component, ContentChild, Inject, Input, Optional, TemplateRef } from '@angular/core'; + +import { staticFalse } from '../../tests-jest'; + +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +@Component({ + selector: 'component-structural', + template: ` +
+ +
+ `, +}) +export class ComponentContentChild { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; + @Input() items?: T[]; +} + +@Component({ + selector: 'my-component', + template: ` +
My Content
+ +
MyComponent1:
+
MyComponent2:
+
MyComponent3:
+
ComponentWeDontWantToMock:
+
ComponentWeWantToMock:
+ +
MyDirective:
+
DirectiveWeDontWantToMock:
+
+ DirectiveWeWantToMock 1: render {{ z.b }} +
+
+ DirectiveWeWantToMock 2: render {{ z.a }} +
+ +
MyPipe: {{ 'text' | MyPipe }}
+
PipeWeDontWantToMock: {{ 'text' | PipeWeDontWantToMock }}
+
PipeWeWantToMock: {{ 'text' | PipeWeWantToMock }}
+
PipeWeWantToCustomize: {{ 'text' | PipeWeWantToCustomize }}
+
PipeWeWantToRestore: {{ 'text' | PipeWeWantToRestore }}
+ +
INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK: {{ t1v }}
+
INJECTION_TOKEN_WE_WANT_TO_MOCK: {{ t2v }}
+
INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE: {{ t3v }}
+ +
anythingWeWant1: {{ anythingWeWant1?.getName() }}
+
anythingWeWant2: {{ anythingWeWant2?.getName() }}
+
myCustomProvider1: {{ myCustomProvider1?.getName() }}
+
myCustomProvider2: {{ myCustomProvider2?.getName() }}
+
myCustomProvider3: {{ myCustomProvider3?.getName() }}
+ +
myService1: {{ myService1?.getName() }}
+
myService2: {{ myService2?.getName() }}
+
serviceWeDontWantToMock: {{ serviceWeDontWantToMock?.getName() }}
+
serviceWeWantToCustomize: {{ serviceWeWantToCustomize?.getName() }}
+
serviceWeWantToMock: {{ serviceWeWantToMock?.getName() }}
+ + + +
ComponentStructural: {{ value }} {{ b.z }}
+
+
+ `, +}) +export class MyComponent { + public readonly anythingWeWant1: AnythingWeWant1; + public readonly anythingWeWant2: AnythingWeWant2; + public readonly myCustomProvider1: MyCustomProvider1; + public readonly myCustomProvider2: MyCustomProvider2; + public readonly myCustomProvider3: MyCustomProvider3; + public readonly myService1: MyService1; + public readonly myService2: MyService2; + public readonly serviceWeDontWantToMock: ServiceWeDontWantToMock; + public readonly serviceWeWantToCustomize: ServiceWeWantToCustomize; + public readonly serviceWeWantToMock: ServiceWeWantToMock; + public readonly t1v: string; + public readonly t2v: string; + public readonly t3v: string; + + constructor( + @Optional() @Inject(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) t1: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_MOCK) t2: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE) t3: string, + @Optional() anythingWeWant1: AnythingWeWant1, + @Optional() anythingWeWant2: AnythingWeWant2, + @Optional() myCustomProvider1: MyCustomProvider1, + @Optional() myCustomProvider2: MyCustomProvider2, + @Optional() myCustomProvider3: MyCustomProvider3, + @Optional() myService1: MyService1, + @Optional() myService2: MyService2, + @Optional() serviceWeDontWantToMock: ServiceWeDontWantToMock, + @Optional() serviceWeWantToMock: ServiceWeWantToMock, + @Optional() serviceWeWantToCustomize: ServiceWeWantToCustomize + ) { + this.t1v = t1; + this.t2v = t2; + this.t3v = t3; + this.anythingWeWant1 = anythingWeWant1; + this.anythingWeWant2 = anythingWeWant2; + this.myCustomProvider1 = myCustomProvider1; + this.myCustomProvider2 = myCustomProvider2; + this.myCustomProvider3 = myCustomProvider3; + this.myService1 = myService1; + this.myService2 = myService2; + this.serviceWeDontWantToMock = serviceWeDontWantToMock; + this.serviceWeWantToCustomize = serviceWeWantToCustomize; + this.serviceWeWantToMock = serviceWeWantToMock; + } +} + +@Component({ + selector: 'component-1', + template: 'MyComponent1', +}) +export class MyComponent1 {} + +@Component({ + selector: 'component-2', + template: 'MyComponent2', +}) +export class MyComponent2 {} + +@Component({ + selector: 'component-3', + template: 'MyComponent3', +}) +export class MyComponent3 {} + +@Component({ + selector: 'dont-want', + template: 'ComponentWeDontWantToMock', +}) +export class ComponentWeDontWantToMock {} + +@Component({ + selector: 'do-want', + template: 'ComponentWeWantToMock', +}) +export class ComponentWeWantToMock {} diff --git a/examples-jest/MockBuilder/fixtures.directives.ts b/examples-jest/MockBuilder/fixtures.directives.ts new file mode 100644 index 0000000000..6dbb2e86f8 --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.directives.ts @@ -0,0 +1,16 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: 'MyDirective', +}) +export class MyDirective {} + +@Directive({ + selector: 'WeDontWantToMock', +}) +export class DirectiveWeDontWantToMock {} + +@Directive({ + selector: '[WeWantToMock]', +}) +export class DirectiveWeWantToMock {} diff --git a/examples-jest/MockBuilder/fixtures.modules.ts b/examples-jest/MockBuilder/fixtures.modules.ts new file mode 100644 index 0000000000..264824c22b --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.modules.ts @@ -0,0 +1,88 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + exports: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + providers: [ + ServiceWeDontWantToMock, + ServiceWeWantToMock, + { + provide: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE', + }, + ], +}) +export class ModuleWeWantToMockBesidesMyModule {} + +@NgModule({ + declarations: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + exports: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + imports: [CommonModule], +}) +export class ModuleWeDontWantToMock {} + +@NgModule({ + declarations: [MyComponent, MyDirective, MyPipe], + exports: [MyComponent, MyDirective, MyPipe], + imports: [HttpClientModule, ModuleWeWantToMockBesidesMyModule, ModuleWeDontWantToMock], + providers: [MyService1, MyService2, ServiceWeWantToCustomize], +}) +export class MyModule {} diff --git a/examples-jest/MockBuilder/fixtures.pipes.ts b/examples-jest/MockBuilder/fixtures.pipes.ts new file mode 100644 index 0000000000..a1034148cb --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.pipes.ts @@ -0,0 +1,56 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'MyPipe', +}) +export class MyPipe implements PipeTransform { + protected prefix = 'MyPipe:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeDontWantToMock', +}) +export class PipeWeDontWantToMock implements PipeTransform { + protected prefix = 'PipeWeDontWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToMock', +}) +export class PipeWeWantToMock implements PipeTransform { + protected prefix = 'PipeWeWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToCustomize', +}) +export class PipeWeWantToCustomize implements PipeTransform { + protected prefix = 'PipeWeWantToCustomize:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToRestore', +}) +export class PipeWeWantToRestore implements PipeTransform { + protected prefix = 'PipeWeWantToRestore:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} diff --git a/examples-jest/MockBuilder/fixtures.services.ts b/examples-jest/MockBuilder/fixtures.services.ts new file mode 100644 index 0000000000..4fb36c910a --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.services.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MyService1 { + protected value = 'MyService1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyService2 { + protected value = 'MyService2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeDontWantToMock { + protected value = 'ServiceWeDontWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToMock { + protected value = 'ServiceWeWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToCustomize { + protected value = 'ServiceWeWantToCustomize'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant1 { + protected value = 'AnythingWeWant1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class TheSameAsAnyProvider { + protected value = 'TheSameAsAnyProvider'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant2 { + protected value = 'AnythingWeWant2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider1 { + protected value = 'MyCustomProvider1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider2 { + protected value = 'MyCustomProvider2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider3 { + protected value = 'MyCustomProvider3'; + + public getName() { + return this.value; + } +} diff --git a/examples-jest/MockBuilder/fixtures.tokens.ts b/examples-jest/MockBuilder/fixtures.tokens.ts new file mode 100644 index 0000000000..8d527302f7 --- /dev/null +++ b/examples-jest/MockBuilder/fixtures.tokens.ts @@ -0,0 +1,7 @@ +import { InjectionToken } from '@angular/core'; + +export const INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE'); diff --git a/examples-jest/MockReactiveForms/MockReactiveForms.spec.ts b/examples-jest/MockReactiveForms/MockReactiveForms.spec.ts index e2e97f67a0..93f1440b57 100644 --- a/examples-jest/MockReactiveForms/MockReactiveForms.spec.ts +++ b/examples-jest/MockReactiveForms/MockReactiveForms.spec.ts @@ -31,6 +31,6 @@ describe('MockReactiveForms', () => { spyOn(mockedReactiveFormComponent, 'writeValue'); component.formControl.setValue('bar'); - expect(mockedReactiveFormComponent.writeValue as any).toHaveBeenCalledWith('bar'); + expect(mockedReactiveFormComponent.writeValue).toHaveBeenCalledWith('bar'); }); }); diff --git a/examples-jest/NG_MOCKS/NG_MOCKS.spec.ts b/examples-jest/NG_MOCKS/NG_MOCKS.spec.ts new file mode 100644 index 0000000000..6c3a7cdf06 --- /dev/null +++ b/examples-jest/NG_MOCKS/NG_MOCKS.spec.ts @@ -0,0 +1,122 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { isMockedNgDefOf, MockBuilder, NG_MOCKS } from 'ng-mocks'; + +import { + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock } from './fixtures.directives'; +import { ModuleWeDontWantToMock, ModuleWeWantToMockBesidesMyModule, MyModule } from './fixtures.modules'; +import { PipeWeDontWantToMock, PipeWeWantToMock, PipeWeWantToRestore } from './fixtures.pipes'; +import { ServiceWeDontWantToMock, ServiceWeWantToCustomize, ServiceWeWantToMock } from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +describe('NG_MOCKS:deep', () => { + beforeEach(async () => { + const ngModule = MockBuilder(MyComponent, MyModule) + .keep(ModuleWeDontWantToMock) + .keep(ComponentWeDontWantToMock) + .keep(DirectiveWeDontWantToMock) + .keep(PipeWeDontWantToMock) + .keep(ServiceWeDontWantToMock) + .keep(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) + + .replace(HttpClientModule, HttpClientTestingModule) + + .mock(ModuleWeWantToMockBesidesMyModule) + .mock(ComponentWeWantToMock) + .mock(DirectiveWeWantToMock) + .mock(PipeWeWantToMock) + .mock(ServiceWeWantToMock) // makes all methods an empty function + .mock(INJECTION_TOKEN_WE_WANT_TO_MOCK) // makes its value undefined + + .mock(ServiceWeWantToCustomize, { prop1: true, getName: () => 'My Customized String' }) + .mock(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, 'My_Token') + + // Now the pipe won't be mocked. + .keep(PipeWeWantToRestore) + + // Even it belongs to the module to keep it still will be mocked and replaced. + .mock(MyComponent3) + + // and now we want to build our NgModule. + .build(); + TestBed.configureTestingModule(ngModule); + + // Extra configuration + TestBed.overrideTemplate(MyComponent1, 'If we need to tune testBed'); + TestBed.overrideTemplate(MyComponent2, 'More callbacks'); + + return TestBed.compileComponents(); + }); + + it('should contain mocks', inject([NG_MOCKS], (mocks: Map) => { + // main part + const myComponent = mocks.get(MyComponent); + expect(myComponent).toBe(MyComponent); + const myModule = mocks.get(MyModule); + expect(isMockedNgDefOf(myModule, MyModule, 'm')).toBeTruthy(); + + // keep + const componentWeDontWantToMock = mocks.get(ComponentWeDontWantToMock); + expect(componentWeDontWantToMock).toBe(ComponentWeDontWantToMock); + const directiveWeDontWantToMock = mocks.get(DirectiveWeDontWantToMock); + expect(directiveWeDontWantToMock).toBe(DirectiveWeDontWantToMock); + const pipeWeDontWantToMock = mocks.get(PipeWeDontWantToMock); + expect(pipeWeDontWantToMock).toBe(PipeWeDontWantToMock); + const serviceWeDontWantToMock = mocks.get(ServiceWeDontWantToMock); + expect(serviceWeDontWantToMock).toBe(ServiceWeDontWantToMock); + const injectionTokenWeDontWantToMock = mocks.get(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK); + expect(injectionTokenWeDontWantToMock).toBe(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK); + + // replace + const httpClientModule = mocks.get(HttpClientModule); + expect(httpClientModule).toBe(HttpClientTestingModule); + + // mock + const moduleWeWantToMockBesidesMyModule = mocks.get(ModuleWeWantToMockBesidesMyModule); + expect(isMockedNgDefOf(moduleWeWantToMockBesidesMyModule, ModuleWeWantToMockBesidesMyModule, 'm')).toBeTruthy(); + const componentWeWantToMock = mocks.get(ComponentWeWantToMock); + expect(isMockedNgDefOf(componentWeWantToMock, ComponentWeWantToMock, 'c')).toBeTruthy(); + const directiveWeWantToMock = mocks.get(DirectiveWeWantToMock); + expect(isMockedNgDefOf(directiveWeWantToMock, DirectiveWeWantToMock, 'd')).toBeTruthy(); + const pipeWeWantToMock = mocks.get(PipeWeWantToMock); + expect(isMockedNgDefOf(pipeWeWantToMock, PipeWeWantToMock, 'p')).toBeTruthy(); + const serviceWeWantToMock = mocks.get(ServiceWeWantToMock); + expect(serviceWeWantToMock).toBeDefined(); + expect(serviceWeWantToMock.useValue).toBeDefined(); + expect(serviceWeWantToMock.useValue.getName).toBeDefined(); + expect(serviceWeWantToMock.useValue.getName()).toBeUndefined(); + expect(mocks.has(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeDefined(); + expect(mocks.get(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeUndefined(); + + // customize + const serviceWeWantToCustomize = mocks.get(ServiceWeWantToCustomize); + expect(serviceWeWantToCustomize).toBeDefined(); + expect(serviceWeWantToCustomize.useValue).toBeDefined(); + expect(serviceWeWantToCustomize.useValue.getName).toBeDefined(); + expect(serviceWeWantToCustomize.useValue.getName()).toEqual('My Customized String'); + expect(serviceWeWantToCustomize.useValue.prop1).toEqual(true); + const injectionTokenWeWantToCustomize = mocks.get(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE); + expect(injectionTokenWeWantToCustomize).toBeDefined(); + expect(injectionTokenWeWantToCustomize.useValue).toEqual('My_Token'); + + // restore + const pipeWeWantToRestore = mocks.get(PipeWeWantToRestore); + expect(pipeWeWantToRestore).toBe(PipeWeWantToRestore); + + // mock nested + const myComponent3 = mocks.get(MyComponent3); + expect(isMockedNgDefOf(myComponent3, MyComponent3, 'c')).toBeTruthy(); + })); +}); diff --git a/examples-jest/NG_MOCKS/fixtures.components.ts b/examples-jest/NG_MOCKS/fixtures.components.ts new file mode 100644 index 0000000000..a407350e61 --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.components.ts @@ -0,0 +1,110 @@ +import { Component, ContentChild, Inject, Input, Optional, TemplateRef } from '@angular/core'; + +import { staticFalse } from '../../tests-jest'; + +import { + AnythingWeWant1, + AnythingWeWant2, + MyCustomProvider1, + MyCustomProvider2, + MyCustomProvider3, + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; + +@Component({ + selector: 'component-structural', + template: '', +}) +export class ComponentContentChild { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; + @Input() items?: T[]; +} + +@Component({ + selector: 'my-component', + template: '', +}) +export class MyComponent { + public readonly anythingWeWant1: AnythingWeWant1; + public readonly anythingWeWant2: AnythingWeWant2; + public readonly myCustomProvider1: MyCustomProvider1; + public readonly myCustomProvider2: MyCustomProvider2; + public readonly myCustomProvider3: MyCustomProvider3; + public readonly myService1: MyService1; + public readonly myService2: MyService2; + public readonly serviceWeDontWantToMock: ServiceWeDontWantToMock; + public readonly serviceWeWantToCustomize: ServiceWeWantToCustomize; + public readonly serviceWeWantToMock: ServiceWeWantToMock; + public readonly t1v: string; + public readonly t2v: string; + public readonly t3v: string; + + constructor( + @Optional() @Inject(INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK) t1: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_MOCK) t2: string, + @Optional() @Inject(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE) t3: string, + @Optional() anythingWeWant1: AnythingWeWant1, + @Optional() anythingWeWant2: AnythingWeWant2, + @Optional() myCustomProvider1: MyCustomProvider1, + @Optional() myCustomProvider2: MyCustomProvider2, + @Optional() myCustomProvider3: MyCustomProvider3, + @Optional() myService1: MyService1, + @Optional() myService2: MyService2, + @Optional() serviceWeDontWantToMock: ServiceWeDontWantToMock, + @Optional() serviceWeWantToMock: ServiceWeWantToMock, + @Optional() serviceWeWantToCustomize: ServiceWeWantToCustomize + ) { + this.t1v = t1; + this.t2v = t2; + this.t3v = t3; + this.anythingWeWant1 = anythingWeWant1; + this.anythingWeWant2 = anythingWeWant2; + this.myCustomProvider1 = myCustomProvider1; + this.myCustomProvider2 = myCustomProvider2; + this.myCustomProvider3 = myCustomProvider3; + this.myService1 = myService1; + this.myService2 = myService2; + this.serviceWeDontWantToMock = serviceWeDontWantToMock; + this.serviceWeWantToCustomize = serviceWeWantToCustomize; + this.serviceWeWantToMock = serviceWeWantToMock; + } +} + +@Component({ + selector: 'component-1', + template: 'MyComponent1', +}) +export class MyComponent1 {} + +@Component({ + selector: 'component-2', + template: 'MyComponent2', +}) +export class MyComponent2 {} + +@Component({ + selector: 'component-3', + template: 'MyComponent3', +}) +export class MyComponent3 {} + +@Component({ + selector: 'dont-want', + template: 'ComponentWeDontWantToMock', +}) +export class ComponentWeDontWantToMock {} + +@Component({ + selector: 'do-want', + template: 'ComponentWeWantToMock', +}) +export class ComponentWeWantToMock {} diff --git a/examples-jest/NG_MOCKS/fixtures.directives.ts b/examples-jest/NG_MOCKS/fixtures.directives.ts new file mode 100644 index 0000000000..6dbb2e86f8 --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.directives.ts @@ -0,0 +1,16 @@ +import { Directive } from '@angular/core'; + +@Directive({ + selector: 'MyDirective', +}) +export class MyDirective {} + +@Directive({ + selector: 'WeDontWantToMock', +}) +export class DirectiveWeDontWantToMock {} + +@Directive({ + selector: '[WeWantToMock]', +}) +export class DirectiveWeWantToMock {} diff --git a/examples-jest/NG_MOCKS/fixtures.modules.ts b/examples-jest/NG_MOCKS/fixtures.modules.ts new file mode 100644 index 0000000000..264824c22b --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.modules.ts @@ -0,0 +1,88 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; + +import { + ComponentContentChild, + ComponentWeDontWantToMock, + ComponentWeWantToMock, + MyComponent, + MyComponent1, + MyComponent2, + MyComponent3, +} from './fixtures.components'; +import { DirectiveWeDontWantToMock, DirectiveWeWantToMock, MyDirective } from './fixtures.directives'; +import { + MyPipe, + PipeWeDontWantToMock, + PipeWeWantToCustomize, + PipeWeWantToMock, + PipeWeWantToRestore, +} from './fixtures.pipes'; +import { + MyService1, + MyService2, + ServiceWeDontWantToMock, + ServiceWeWantToCustomize, + ServiceWeWantToMock, +} from './fixtures.services'; +import { + INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + INJECTION_TOKEN_WE_WANT_TO_MOCK, +} from './fixtures.tokens'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + exports: [ + ComponentWeDontWantToMock, + ComponentWeWantToMock, + DirectiveWeDontWantToMock, + DirectiveWeWantToMock, + PipeWeDontWantToMock, + PipeWeWantToMock, + PipeWeWantToCustomize, + PipeWeWantToRestore, + ], + providers: [ + ServiceWeDontWantToMock, + ServiceWeWantToMock, + { + provide: INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_MOCK, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_MOCK', + }, + { + provide: INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE, + useValue: 'INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE', + }, + ], +}) +export class ModuleWeWantToMockBesidesMyModule {} + +@NgModule({ + declarations: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + exports: [MyComponent1, MyComponent2, MyComponent3, ComponentContentChild], + imports: [CommonModule], +}) +export class ModuleWeDontWantToMock {} + +@NgModule({ + declarations: [MyComponent, MyDirective, MyPipe], + exports: [MyComponent, MyDirective, MyPipe], + imports: [HttpClientModule, ModuleWeWantToMockBesidesMyModule, ModuleWeDontWantToMock], + providers: [MyService1, MyService2, ServiceWeWantToCustomize], +}) +export class MyModule {} diff --git a/examples-jest/NG_MOCKS/fixtures.pipes.ts b/examples-jest/NG_MOCKS/fixtures.pipes.ts new file mode 100644 index 0000000000..a1034148cb --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.pipes.ts @@ -0,0 +1,56 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'MyPipe', +}) +export class MyPipe implements PipeTransform { + protected prefix = 'MyPipe:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeDontWantToMock', +}) +export class PipeWeDontWantToMock implements PipeTransform { + protected prefix = 'PipeWeDontWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToMock', +}) +export class PipeWeWantToMock implements PipeTransform { + protected prefix = 'PipeWeWantToMock:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToCustomize', +}) +export class PipeWeWantToCustomize implements PipeTransform { + protected prefix = 'PipeWeWantToCustomize:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} + +@Pipe({ + name: 'PipeWeWantToRestore', +}) +export class PipeWeWantToRestore implements PipeTransform { + protected prefix = 'PipeWeWantToRestore:'; + + public transform(value: any, ...args: any[]): any { + return this.prefix + value; + } +} diff --git a/examples-jest/NG_MOCKS/fixtures.services.ts b/examples-jest/NG_MOCKS/fixtures.services.ts new file mode 100644 index 0000000000..4fb36c910a --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.services.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MyService1 { + protected value = 'MyService1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyService2 { + protected value = 'MyService2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeDontWantToMock { + protected value = 'ServiceWeDontWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToMock { + protected value = 'ServiceWeWantToMock'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class ServiceWeWantToCustomize { + protected value = 'ServiceWeWantToCustomize'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant1 { + protected value = 'AnythingWeWant1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class TheSameAsAnyProvider { + protected value = 'TheSameAsAnyProvider'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class AnythingWeWant2 { + protected value = 'AnythingWeWant2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider1 { + protected value = 'MyCustomProvider1'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider2 { + protected value = 'MyCustomProvider2'; + + public getName() { + return this.value; + } +} + +@Injectable() +export class MyCustomProvider3 { + protected value = 'MyCustomProvider3'; + + public getName() { + return this.value; + } +} diff --git a/examples-jest/NG_MOCKS/fixtures.tokens.ts b/examples-jest/NG_MOCKS/fixtures.tokens.ts new file mode 100644 index 0000000000..8d527302f7 --- /dev/null +++ b/examples-jest/NG_MOCKS/fixtures.tokens.ts @@ -0,0 +1,7 @@ +import { InjectionToken } from '@angular/core'; + +export const INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_DONT_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_MOCK = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_MOCK'); + +export const INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE = new InjectionToken('INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE'); diff --git a/index.ts b/index.ts index 62a3a89897..2623be0b14 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,5 @@ export * from './lib/common'; +export * from './lib/mock-builder'; export * from './lib/mock-component'; export * from './lib/mock-declaration'; export * from './lib/mock-directive'; diff --git a/lib/common/Mock.spec.ts b/lib/common/Mock.spec.ts index eeddfcda50..34b1e2a3c2 100644 --- a/lib/common/Mock.spec.ts +++ b/lib/common/Mock.spec.ts @@ -1,5 +1,3 @@ -/* tslint:disable: max-classes-per-file */ - import { Component, Directive, NgModule, Pipe, PipeTransform } from '@angular/core'; import { MockComponent } from '../mock-component'; diff --git a/lib/common/Mock.ts b/lib/common/Mock.ts index 6b8824d41a..03bb21bc2b 100644 --- a/lib/common/Mock.ts +++ b/lib/common/Mock.ts @@ -1,5 +1,3 @@ -// tslint:disable:max-classes-per-file - import { EventEmitter } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; @@ -73,5 +71,5 @@ export class MockControlValueAccessor extends Mock implements ControlValueAccess this.__simulateTouch = fn; } - writeValue = () => {}; + writeValue = (value: any) => {}; } diff --git a/lib/common/decorate.ts b/lib/common/decorate.ts index 34527fdd97..b5baa205cd 100644 --- a/lib/common/decorate.ts +++ b/lib/common/decorate.ts @@ -1,4 +1,6 @@ -import { ContentChild, ContentChildren, Input, Output, Query, Type, ViewChild, ViewChildren } from '@angular/core'; +import { ContentChild, ContentChildren, Input, Output, Query, ViewChild, ViewChildren } from '@angular/core'; + +import { Type } from './lib'; // Looks like an A9 bug, that queries from @Component aren't processed. // Also we have to pass prototype, not the class. diff --git a/lib/common/index.ts b/lib/common/index.ts index 7b81190dfd..808b6322d3 100644 --- a/lib/common/index.ts +++ b/lib/common/index.ts @@ -1,2 +1,3 @@ -export * from './mock-of.decorator'; export * from './Mock'; +export * from './lib'; +export * from './mock-of.decorator'; diff --git a/lib/common/lib.ts b/lib/common/lib.ts new file mode 100644 index 0000000000..cf33c38673 --- /dev/null +++ b/lib/common/lib.ts @@ -0,0 +1,175 @@ +import { InjectionToken, ModuleWithProviders, PipeTransform, Provider } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; + +import { MockedComponent } from '../mock-component'; +import { MockedDirective } from '../mock-directive'; +import { MockedModule } from '../mock-module'; +import { MockedPipe } from '../mock-pipe'; + +import { ngMocksUniverse } from './ng-mocks-universe'; +import { jitReflector } from './reflect'; + +// tslint:disable-next-line:interface-name +export interface AbstractType extends Function { + prototype: T; +} + +export type Type = new (...args: any[]) => T; + +// remove after removal of A5 support +// tslint:disable-next-line:interface-name +export interface NgModuleWithProviders extends ModuleWithProviders { + ngModule: Type; + providers?: Provider[]; +} + +export const NG_MOCKS = new InjectionToken>('NG_MOCKS'); + +/** + * Can be changed any time. + * + * @internal + */ +export const getNgMocksFromTestBed = (): Map | undefined => { + const testBed: any = getTestBed(); + try { + return testBed.inject ? testBed.inject(NG_MOCKS) : testBed.get(NG_MOCKS); + } catch (e) { + return undefined; + } +}; + +export const flatten = (values: T | T[], result: T[] = []): T[] => { + if (Array.isArray(values)) { + values.forEach((value: T | T[]) => flatten(value, result)); + } else { + result.push(values); + } + return result; +}; + +export const isNgType = (object: Type, type: string): boolean => + jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type); + +/** + * Checks whether a class was decorated by a ng type. + * m - module. + * c - component. + * d - directive. + * p - pipe. + */ +export function isNgDef(object: any, ngType: 'm' | 'c' | 'd'): object is Type; +export function isNgDef(object: any, ngType: 'p'): object is Type; +export function isNgDef(object: any, ngType: string): object is Type { + if (ngType === 'm') { + return isNgType(object, 'NgModule'); + } + if (ngType === 'c') { + return isNgType(object, 'Component'); + } + if (ngType === 'd') { + return isNgType(object, 'Directive'); + } + if (ngType === 'p') { + return isNgType(object, 'Pipe'); + } + return false; +} + +/** + * Checks whether a class is a mock of a class that was decorated by a ng type. + * m - module. + * c - component. + * d - directive. + * p - pipe. + */ +export function isMockedNgDefOf(object: any, type: Type, ngType: 'm'): object is Type>; +export function isMockedNgDefOf(object: any, type: Type, ngType: 'c'): object is Type>; +export function isMockedNgDefOf(object: any, type: Type, ngType: 'd'): object is Type>; +export function isMockedNgDefOf( + object: any, + type: Type, + ngType: 'p' +): object is Type>; +export function isMockedNgDefOf(object: any, type: Type): object is Type; +export function isMockedNgDefOf(object: any, type: Type, ngType?: any): object is Type { + return typeof object === 'function' && object.mockOf === type && (ngType ? isNgDef(object, ngType) : true); +} + +export const isNgInjectionToken = (object: any): object is InjectionToken => + typeof object === 'object' && object.ngMetadataName === 'InjectionToken'; + +// Checks if an object implements ModuleWithProviders. +export const isNgModuleDefWithProviders = (object: any): object is NgModuleWithProviders => + object.ngModule !== undefined && isNgDef(object.ngModule, 'm'); + +/** + * Checks whether an object is an instance of a mocked class that was decorated by a ng type. + * m - module. + * c - component. + * d - directive. + * p - pipe. + */ +export function isMockOf(object: any, type: Type, ngType: 'm'): object is MockedModule; +export function isMockOf(object: any, type: Type, ngType: 'c'): object is MockedComponent; +export function isMockOf(object: any, type: Type, ngType: 'd'): object is MockedDirective; +export function isMockOf(object: any, type: Type, ngType: 'p'): object is MockedPipe; +export function isMockOf(object: any, type: Type): object is T; +export function isMockOf(object: any, type: Type, ngType?: any): object is T { + return ( + typeof object === 'object' && + (ngType ? isMockedNgDefOf(object.constructor, type, ngType) : isMockedNgDefOf(object.constructor, type)) + ); +} + +/** + * Returns a def of a mocked class based on another mock class or a source class that was decorated by a ng type. + * m - module. + * c - component. + * d - directive. + * p - pipe. + */ +export function getMockedNgDefOf(type: Type, ngType: 'm'): Type>; +export function getMockedNgDefOf(type: Type, ngType: 'c'): Type>; +export function getMockedNgDefOf(type: Type, ngType: 'd'): Type>; +export function getMockedNgDefOf(type: Type, ngType: 'p'): Type>; +export function getMockedNgDefOf(type: Type): Type; +export function getMockedNgDefOf(type: any, ngType?: any): any { + const source = type.mockOf ? type.mockOf : type; + const mocks = getNgMocksFromTestBed(); + + let mock: any; + + // If mocks exists, we are in the MockBuilder env and it's enough for the check. + if (mocks && mocks.has(source)) { + mock = mocks.get(source); + } else if (mocks) { + throw new Error(`There is no mock for ${source.name}`); + } + + // If we are not in the MockBuilder env we can rely on the current cache. + if (!mock && source !== type) { + mock = type; + } else if (!mock && ngMocksUniverse.cache.has(source)) { + mock = ngMocksUniverse.cache.get(source); + } + + if (!ngType) { + return mock; + } + if (ngType && isMockedNgDefOf(mock, type, ngType)) { + return mock; + } + + // Looks like the def hasn't been mocked. + throw new Error(`There is no mock for ${source.name}`); +} + +export function getSourceOfMock(type: Type>): Type; +export function getSourceOfMock(type: Type>): Type; +export function getSourceOfMock(type: Type>): Type; +export function getSourceOfMock(type: Type>): Type; +export function getSourceOfMock(type: Type): Type; +export function getSourceOfMock(type: any): Type { + return typeof type === 'function' && type.mockOf ? type.mockOf : type; +} diff --git a/lib/common/mock-of.decorator.spec.ts b/lib/common/mock-of.decorator.spec.ts index 4ce0ca7626..180bdd6c4b 100644 --- a/lib/common/mock-of.decorator.spec.ts +++ b/lib/common/mock-of.decorator.spec.ts @@ -1,4 +1,3 @@ -/* tslint:disable: max-classes-per-file */ import { MockOf } from './mock-of.decorator'; describe('DebuggableMock', () => { diff --git a/lib/common/mock-of.decorator.ts b/lib/common/mock-of.decorator.ts index 66e4f055ca..479c75dcc5 100644 --- a/lib/common/mock-of.decorator.ts +++ b/lib/common/mock-of.decorator.ts @@ -1,4 +1,4 @@ -import { Type } from '@angular/core'; +import { Type } from './lib'; // This helps with debugging in the browser. Decorating mock classes with this // will change the display-name of the class to 'MockOf-` so our diff --git a/lib/common/ng-mocks-universe.ts b/lib/common/ng-mocks-universe.ts new file mode 100644 index 0000000000..f29e037650 --- /dev/null +++ b/lib/common/ng-mocks-universe.ts @@ -0,0 +1,16 @@ +import { InjectionToken } from '@angular/core'; + +import { Type } from './lib'; + +/** + * Can be changed any time. + * + * @internal + */ +export const ngMocksUniverse = { + builder: new Map(), + cache: new Map(), + config: new Map(), + flags: new Set(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']), + touches: new Set | InjectionToken>(), +}; diff --git a/lib/mock-builder/index.ts b/lib/mock-builder/index.ts new file mode 100644 index 0000000000..babe587833 --- /dev/null +++ b/lib/mock-builder/index.ts @@ -0,0 +1 @@ +export * from './mock-builder'; diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts new file mode 100644 index 0000000000..bc86184ce1 --- /dev/null +++ b/lib/mock-builder/mock-builder.ts @@ -0,0 +1,435 @@ +import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { flatten, isNgDef, isNgInjectionToken, NgModuleWithProviders, NG_MOCKS, Type } from '../common'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; +import { MockComponent } from '../mock-component'; +import { MockDirective } from '../mock-directive'; +import { MockModule, MockProvider } from '../mock-module'; +import { MockPipe } from '../mock-pipe'; + +export interface IMockBuilderResult { + testBed: typeof TestBed; +} + +export interface IMockBuilderConfigAll { + dependency?: boolean; // won't be added to TestBedModule. + export?: boolean; // will be forced for export in its module. +} + +export interface IMockBuilderConfigComponent { + render?: { + [blockName: string]: + | boolean + | { + $implicit?: any; + variables?: { [key: string]: any }; + }; + }; +} + +export interface IMockBuilderConfigDirective { + render?: + | boolean + | { + $implicit?: any; + variables?: { [key: string]: any }; + }; +} + +export type IMockBuilderConfig = IMockBuilderConfigAll | IMockBuilderConfigComponent | IMockBuilderConfigDirective; + +const defaultMock = Symbol(); + +export class MockBuilderPromise implements PromiseLike { + protected beforeCC: Set<(testBed: typeof TestBed) => void> = new Set(); + protected configDef: Map | InjectionToken, any> = new Map(); + + protected keepDef: { + component: Set>; + directive: Set>; + module: Set>; + pipe: Set>; + provider: Set | InjectionToken>; + } = { + component: new Set(), + directive: new Set(), + module: new Set(), + pipe: new Set(), + provider: new Set(), + }; + + protected mockDef: { + component: Set>; + directive: Set>; + module: Set>; + pipe: Set>; + pipeTransform: Map, PipeTransform['transform']>; + provider: Set | InjectionToken>; + providerMock: Map | InjectionToken, any>; + } = { + component: new Set(), + directive: new Set(), + module: new Set(), + pipe: new Set(), + pipeTransform: new Map(), + provider: new Set(), + providerMock: new Map(), + }; + + protected providerDef: Map | InjectionToken, Provider> = new Map(); + + protected replaceDef: { + component: Map, Type>; + directive: Map, Type>; + module: Map, Type>; + pipe: Map, Type>; + } = { + component: new Map(), + directive: new Map(), + module: new Map(), + pipe: new Map(), + }; + + public beforeCompileComponents(callback: (testBed: typeof TestBed) => void): this { + this.beforeCC.add(callback); + return this; + } + + public build(): NgModule { + // tslint:disable-line:cyclomatic-complexity + const backup = { + builder: ngMocksUniverse.builder, + cache: ngMocksUniverse.cache, + config: ngMocksUniverse.config, + flags: ngMocksUniverse.flags, + touches: ngMocksUniverse.touches, + }; + + ngMocksUniverse.builder = new Map(); + ngMocksUniverse.cache = new Map(); + ngMocksUniverse.config = this.configDef; + ngMocksUniverse.flags = new Set([ + 'cacheComponent', + 'cacheDirective', + 'cacheModule', + 'cachePipe', + 'cacheProvider', + 'correctModuleExports', + ]); + ngMocksUniverse.touches = new Set(); + + for (const def of [ + ...this.keepDef.provider.values(), + ...this.keepDef.pipe.values(), + ...this.keepDef.directive.values(), + ...this.keepDef.component.values(), + ...this.keepDef.module.values(), + ]) { + ngMocksUniverse.builder.set(def, def); + } + + for (const [source, destination] of [ + ...this.replaceDef.pipe.entries(), + ...this.replaceDef.directive.entries(), + ...this.replaceDef.component.entries(), + ...this.replaceDef.module.entries(), + ]) { + ngMocksUniverse.builder.set(source, destination); + } + + // mocking requested things. + for (const def of this.mockDef.provider.values()) { + if (this.mockDef.providerMock.has(def)) { + ngMocksUniverse.builder.set(def, { provide: def, useValue: this.mockDef.providerMock.get(def) }); + } else { + ngMocksUniverse.builder.set(def, MockProvider(def)); + } + ngMocksUniverse.touches.delete(def); + } + for (const def of this.mockDef.pipe.values()) { + if (this.mockDef.pipeTransform.has(def)) { + ngMocksUniverse.builder.set(def, MockPipe(def, this.mockDef.pipeTransform.get(def))); + } else { + ngMocksUniverse.builder.set(def, MockPipe(def)); + } + ngMocksUniverse.touches.delete(def); + } + for (const def of this.mockDef.directive.values()) { + ngMocksUniverse.builder.set(def, MockDirective(def)); + ngMocksUniverse.touches.delete(def); + } + for (const def of this.mockDef.component.values()) { + ngMocksUniverse.builder.set(def, MockComponent(def)); + ngMocksUniverse.touches.delete(def); + } + + // Now we need to run through requested modules. + for (const def of [ + ...this.mockDef.module.values(), + ...this.keepDef.module.values(), + ...this.replaceDef.module.keys(), + ]) { + ngMocksUniverse.builder.set(def, MockModule(def)); + ngMocksUniverse.touches.delete(def); + } + + // Setting up TestBed. + const imports: Array | NgModuleWithProviders> = []; + + // Adding suitable leftovers. + for (const def of [ + ...this.mockDef.module.values(), + ...this.keepDef.module.values(), + ...this.replaceDef.module.keys(), + ]) { + if (ngMocksUniverse.touches.has(def)) { + continue; + } + const config = this.configDef.get(def); + if (config && config.dependency) { + continue; + } + imports.push(ngMocksUniverse.builder.get(def)); + } + + const declarations: Array> = []; + + // adding missed declarations to test bed. + for (const def of [ + ...this.keepDef.pipe.values(), + ...this.keepDef.directive.values(), + ...this.keepDef.component.values(), + ...this.replaceDef.pipe.keys(), + ...this.replaceDef.directive.keys(), + ...this.replaceDef.component.keys(), + ...this.mockDef.pipe.values(), + ...this.mockDef.directive.values(), + ...this.mockDef.component.values(), + ]) { + if (ngMocksUniverse.touches.has(def)) { + continue; + } + const config = this.configDef.get(def); + if (config && config.dependency) { + continue; + } + declarations.push(ngMocksUniverse.builder.get(def)); + } + + const providers: Provider[] = []; + + // Adding missed providers to test bed. + for (const def of this.keepDef.provider.values()) { + if (ngMocksUniverse.touches.has(def)) { + continue; + } + const config = this.configDef.get(def); + if (config && config.dependency) { + continue; + } + if (isNgInjectionToken(def)) { + continue; + } + providers.push(def); + } + + // Adding missed providers to test bed. + for (const def of this.mockDef.provider.values()) { + if (ngMocksUniverse.touches.has(def)) { + continue; + } + const config = this.configDef.get(def); + if (config && config.dependency) { + continue; + } + const mock = ngMocksUniverse.builder.get(def); + providers.push( + mock + ? mock + : { + provide: def, + useValue: undefined, + } + ); + } + + // Adding requested providers to test bed. + for (const provider of this.providerDef.values()) { + if (!provider) { + continue; + } + providers.push(provider); + } + + const ngMocks = new Map(); + for (const [key, value] of [...ngMocksUniverse.builder.entries(), ...ngMocksUniverse.cache.entries()]) { + ngMocks.set(key, value); + } + + providers.push({ + provide: NG_MOCKS, + useValue: ngMocks, + }); + + Object.assign(ngMocksUniverse, backup); + + return { + declarations, + imports, + providers, + }; + } + + public keep(def: any, config?: IMockBuilderConfig): this { + if (isNgDef(def, 'm')) { + this.mockDef.module.delete(def); + this.replaceDef.module.delete(def); + this.keepDef.module.add(def); + } else if (isNgDef(def, 'c')) { + this.mockDef.component.delete(def); + this.replaceDef.component.delete(def); + this.keepDef.component.add(def); + } else if (isNgDef(def, 'd')) { + this.mockDef.directive.delete(def); + this.replaceDef.directive.delete(def); + this.keepDef.directive.add(def); + } else if (isNgDef(def, 'p')) { + this.mockDef.pipe.delete(def); + this.mockDef.pipeTransform.delete(def); + this.replaceDef.pipe.delete(def); + this.keepDef.pipe.add(def); + } else { + this.mockDef.provider.delete(def); + this.mockDef.providerMock.delete(def); + this.providerDef.delete(def); + this.keepDef.provider.add(def); + } + if (config) { + this.configDef.set(def, config); + } else { + this.configDef.delete(def); + } + return this; + } + + public mock(pipe: Type, config?: IMockBuilderConfig): this; + public mock(pipe: Type, mock?: PipeTransform['transform'], config?: IMockBuilderConfig): this; + public mock(token: InjectionToken, mock?: any): this; + public mock(def: Type, mock: IMockBuilderConfig): this; + public mock(provider: Type, mock?: any): this; + public mock(def: Type): this; + public mock(def: any, a1: any = defaultMock, a2?: any): this { + let mock: any = a1; + let config: any = a1 === defaultMock ? undefined : a1; + if (isNgDef(def, 'p') && typeof a1 === 'function') { + mock = a1; + config = a2; + } + + if (isNgDef(def, 'm')) { + this.keepDef.module.delete(def); + this.replaceDef.module.delete(def); + this.mockDef.module.add(def); + } else if (isNgDef(def, 'c')) { + this.keepDef.component.delete(def); + this.replaceDef.component.delete(def); + this.mockDef.component.add(def); + } else if (isNgDef(def, 'd')) { + this.keepDef.directive.delete(def); + this.replaceDef.directive.delete(def); + this.mockDef.directive.add(def); + } else if (isNgDef(def, 'p')) { + this.keepDef.pipe.delete(def); + this.replaceDef.pipe.delete(def); + this.mockDef.pipe.add(def); + if (typeof mock === 'function') { + this.mockDef.pipeTransform.set(def, mock); + } + } else { + this.keepDef.provider.delete(def); + this.providerDef.delete(def); + this.mockDef.provider.add(def); + if (mock !== defaultMock) { + this.mockDef.providerMock.set(def, mock); + } + config = undefined; + } + if (config) { + this.configDef.set(def, config); + } else { + this.configDef.delete(def); + } + return this; + } + + 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); + const existing = this.providerDef.has(provide) ? this.providerDef.get(provide) : []; + this.providerDef.set(provide, multi ? [...(Array.isArray(existing) ? existing : []), provider] : provider); + } + return this; + } + + public replace(source: Type, destination: Type, config?: IMockBuilderConfig): this { + if (isNgDef(source, 'm') && isNgDef(destination, 'm')) { + this.keepDef.module.delete(source); + this.mockDef.module.delete(source); + this.replaceDef.module.set(source, destination); + } else if (isNgDef(source, 'c') && isNgDef(destination, 'c')) { + this.keepDef.component.delete(source); + this.mockDef.component.delete(source); + this.replaceDef.component.set(source, destination); + } else if (isNgDef(source, 'd') && isNgDef(destination, 'd')) { + this.keepDef.directive.delete(source); + this.mockDef.directive.delete(source); + this.replaceDef.directive.set(source, destination); + } else if (isNgDef(source, 'p') && isNgDef(destination, 'p')) { + this.keepDef.pipe.delete(source); + this.mockDef.pipe.delete(source); + this.replaceDef.pipe.set(source, destination); + } else { + throw new Error('cannot replace the source by destination destination, wrong types'); + } + if (config) { + this.configDef.set(source, config); + } else { + this.configDef.delete(source); + } + return this; + } + + public then( + fulfill?: (value: IMockBuilderResult) => PromiseLike, + reject?: (reason: any) => PromiseLike + ): PromiseLike { + const promise = new Promise((resolve: (value: IMockBuilderResult) => void): void => { + const testBed = TestBed.configureTestingModule(this.build()); + for (const callback of this.beforeCC.values()) { + callback(testBed); + } + testBed.compileComponents().then(() => { + resolve({ testBed }); + }); + }); + return promise.then(fulfill, reject); + } +} + +export function MockBuilder(componentToTest?: Type, itsModuleToMock?: Type): MockBuilderPromise { + const instance = new MockBuilderPromise(); + + if (componentToTest) { + instance.keep(componentToTest, { + export: true, + }); + } + if (itsModuleToMock) { + instance.mock(itsModuleToMock); + } + return instance; +} diff --git a/lib/mock-component/mock-component.spec.ts b/lib/mock-component/mock-component.spec.ts index 453ca1b31f..cc21e5dbfb 100644 --- a/lib/mock-component/mock-component.spec.ts +++ b/lib/mock-component/mock-component.spec.ts @@ -1,5 +1,3 @@ -// tslint:disable:max-classes-per-file - import { Component, ContentChild, diff --git a/lib/mock-component/mock-component.ts b/lib/mock-component/mock-component.ts index c39b9017df..fa569bd129 100644 --- a/lib/mock-component/mock-component.ts +++ b/lib/mock-component/mock-component.ts @@ -1,22 +1,22 @@ import { core } from '@angular/compiler'; import { + AfterContentInit, ChangeDetectorRef, Component, forwardRef, Query, TemplateRef, - Type, ViewChild, ViewContainerRef, } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { MockControlValueAccessor, MockOf } from '../common'; +import { getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; import { decorateInputs, decorateOutputs, decorateQueries } from '../common/decorate'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { directiveResolver } from '../common/reflect'; -const cache = new Map, Type>>(); - export type MockedComponent = T & MockControlValueAccessor & { /** Helper function to hide rendered @ContentChild() template. */ @@ -34,9 +34,17 @@ export function MockComponent( component: Type, metaData?: core.Directive ): Type> { - const cacheHit = cache.get(component); - if (cacheHit) { - return cacheHit as Type>; + // we are inside of an 'it'. + // It's fine to to return a mock or to throw an exception if it wasn't mocked in TestBed. + if ((getTestBed() as any)._instantiated) { + try { + return getMockedNgDefOf(component, 'c'); + } catch (error) { + // looks like an in-test mock. + } + } + if (ngMocksUniverse.flags.has('cacheComponent') && ngMocksUniverse.cache.has(component)) { + return ngMocksUniverse.cache.get(component); } let meta: core.Directive | undefined = metaData; @@ -99,8 +107,10 @@ export function MockComponent( template, }; + const config = ngMocksUniverse.config.get(component); + @MockOf(component, outputs) - class ComponentMock extends MockControlValueAccessor { + class ComponentMock extends MockControlValueAccessor implements AfterContentInit { constructor(changeDetector: ChangeDetectorRef) { super(); @@ -131,6 +141,22 @@ export function MockComponent( } }; } + + ngAfterContentInit(): void { + if (!(this as any).__rendered && config && config.render) { + for (const block of Object.keys(config.render)) { + const { $implicit, variables } = + config.render[block] !== true + ? config.render[block] + : { + $implicit: undefined, + variables: {}, + }; + (this as any).__render(block, $implicit, variables); + } + (this as any).__rendered = true; + } + } } decorateInputs(ComponentMock, inputs); @@ -138,7 +164,9 @@ export function MockComponent( decorateQueries(ComponentMock, queries); const mockedComponent: Type> = Component(options)(ComponentMock as any); - cache.set(component, mockedComponent); + if (ngMocksUniverse.flags.has('cacheComponent')) { + ngMocksUniverse.cache.set(component, mockedComponent); + } return mockedComponent; } diff --git a/lib/mock-component/test-components/simple-component.component.ts b/lib/mock-component/test-components/simple-component.component.ts index 18d10d8898..14e5e34df4 100644 --- a/lib/mock-component/test-components/simple-component.component.ts +++ b/lib/mock-component/test-components/simple-component.component.ts @@ -8,7 +8,6 @@ export class BaseSimpleComponent { @Output() someOutput2: EventEmitter; } -/* tslint:disable:max-classes-per-file */ @Component({ exportAs: 'seeimple', selector: 'simple-component', @@ -20,4 +19,3 @@ export class SimpleComponent extends BaseSimpleComponent { @HostBinding('class.someClass') @Input() someInput3: boolean; @Output() someOutput1: EventEmitter; } -/* tslint:enable:max-classes-per-file */ diff --git a/lib/mock-declaration/mock-declaration.spec.ts b/lib/mock-declaration/mock-declaration.spec.ts index a29b414a67..2a3329da12 100644 --- a/lib/mock-declaration/mock-declaration.spec.ts +++ b/lib/mock-declaration/mock-declaration.spec.ts @@ -2,13 +2,11 @@ import { Component } from '@angular/core'; import { MockDeclaration } from './mock-declaration'; -// tslint:disable:max-classes-per-file @Component({ selector: 'empty-template-container', template: '', }) export class EmptyTemplateContainer {} -// tslint:enable:max-classes-per-file describe('MockDeclaration', () => { it('should process components with an empty template correctly', () => { diff --git a/lib/mock-declaration/mock-declaration.ts b/lib/mock-declaration/mock-declaration.ts index 520cc51d44..ae1577f6d2 100644 --- a/lib/mock-declaration/mock-declaration.ts +++ b/lib/mock-declaration/mock-declaration.ts @@ -1,23 +1,24 @@ -import { Type } from '@angular/core'; - -import { jitReflector, pipeResolver } from '../common/reflect'; -import { MockComponent } from '../mock-component'; -import { MockDirective } from '../mock-directive'; -import { MockPipe } from '../mock-pipe'; +import { isNgDef, Type } from '../common'; +import { MockComponent, MockedComponent } from '../mock-component'; +import { MockDirective, MockedDirective } from '../mock-directive'; +import { MockedPipe, MockPipe } from '../mock-pipe'; export function MockDeclarations(...declarations: Array>): Array> { return declarations.map(MockDeclaration); } -export function MockDeclaration(declaration: Type): Type { - if (pipeResolver.isPipe(declaration)) { - return MockPipe(declaration as any) as any; +export function MockDeclaration( + declaration: Type +): Type | MockedDirective | MockedComponent> { + if (isNgDef(declaration, 'p')) { + // TODO remove any when support of A5 has been stopped. + return MockPipe(declaration) as any; } - - const annotations = jitReflector.annotations(declaration); - if (annotations.find(annotation => annotation.template !== undefined || annotation.templateUrl !== undefined)) { - return MockComponent(declaration) as any; + if (isNgDef(declaration, 'c')) { + return MockComponent(declaration); } - - return MockDirective(declaration) as any; + if (isNgDef(declaration, 'd')) { + return MockDirective(declaration); + } + return declaration; } diff --git a/lib/mock-directive/mock-directive.spec.ts b/lib/mock-directive/mock-directive.spec.ts index 37657d8d8e..a2fba1fea4 100644 --- a/lib/mock-directive/mock-directive.spec.ts +++ b/lib/mock-directive/mock-directive.spec.ts @@ -1,5 +1,3 @@ -// tslint:disable:max-classes-per-file - import { Component, ContentChild, diff --git a/lib/mock-directive/mock-directive.ts b/lib/mock-directive/mock-directive.ts index d41bd696bf..c40a997771 100644 --- a/lib/mock-directive/mock-directive.ts +++ b/lib/mock-directive/mock-directive.ts @@ -1,12 +1,13 @@ import { core } from '@angular/compiler'; -import { Directive, ElementRef, forwardRef, Optional, TemplateRef, Type, ViewContainerRef } from '@angular/core'; +import { Directive, ElementRef, forwardRef, OnInit, Optional, TemplateRef, ViewContainerRef } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { MockControlValueAccessor, MockOf } from '../common'; +import { getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; import { decorateInputs, decorateOutputs, decorateQueries } from '../common/decorate'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { directiveResolver } from '../common/reflect'; -const cache = new Map, Type>>(); - export type MockedDirective = T & MockControlValueAccessor & { /** Pointer to current element in case of Attribute Directives. */ @@ -30,9 +31,17 @@ export function MockDirectives(...directives: Array>): Array(directive: Type): Type> { - const cacheHit = cache.get(directive); - if (cacheHit) { - return cacheHit as Type>; + // We are inside of an 'it'. + // It's fine to to return a mock or to throw an exception if it wasn't mocked in TestBed. + if ((getTestBed() as any)._instantiated) { + try { + return getMockedNgDefOf(directive, 'd'); + } catch (error) { + // looks like an in-test mock. + } + } + if (ngMocksUniverse.flags.has('cacheDirective') && ngMocksUniverse.cache.has(directive)) { + return ngMocksUniverse.cache.get(directive); } let meta: core.Directive | undefined; @@ -48,6 +57,11 @@ export function MockDirective(directive: Type): Type DirectiveMock), + }, { provide: directive, useExisting: forwardRef(() => DirectiveMock), @@ -56,8 +70,10 @@ export function MockDirective(directive: Type): Type, @@ -80,6 +96,19 @@ export function MockDirective(directive: Type): Type(directive: Type): Type> = Directive(options)(DirectiveMock as any); - cache.set(directive, mockedDirective); + if (ngMocksUniverse.flags.has('cacheDirective')) { + ngMocksUniverse.cache.set(directive, mockedDirective); + } return mockedDirective; } diff --git a/lib/mock-helper/mock-helper.spec.ts b/lib/mock-helper/mock-helper.spec.ts index 05f98b706d..e967f4f09d 100644 --- a/lib/mock-helper/mock-helper.spec.ts +++ b/lib/mock-helper/mock-helper.spec.ts @@ -1,5 +1,3 @@ -// tslint:disable:max-classes-per-file - import { Component, Directive, EventEmitter, Input, Output } from '@angular/core'; import { async, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; diff --git a/lib/mock-helper/mock-helper.ts b/lib/mock-helper/mock-helper.ts index 6b173f6855..b4006554aa 100644 --- a/lib/mock-helper/mock-helper.ts +++ b/lib/mock-helper/mock-helper.ts @@ -1,9 +1,11 @@ /* tslint:disable:variable-name unified-signatures */ import { core } from '@angular/compiler'; -import { EventEmitter, Type } from '@angular/core'; +import { EventEmitter } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { AbstractType, getSourceOfMock, Type } from '../common'; import { directiveResolver } from '../common/reflect'; import { MockedDebugElement, MockedDebugNode } from '../mock-render'; import { MockedFunction, mockServiceHelper } from '../mock-service'; @@ -24,16 +26,24 @@ function nestedCheck(result: T[], node: MockedDebugNode, callback: (node: Moc */ export const MockHelper: { find(debugElement: MockedDebugElement, component: Type): null | MockedDebugElement; + find(debugElement: MockedDebugElement, component: AbstractType): null | MockedDebugElement; find(debugElement: MockedDebugElement, cssSelector: string): null | MockedDebugElement; findAll(debugElement: MockedDebugElement, component: Type): Array>; + findAll(debugElement: MockedDebugElement, component: AbstractType): Array>; findAll(debugElement: MockedDebugElement, cssSelector: string): Array>; findDirective(debugNode: MockedDebugNode, directive: Type): undefined | T; + findDirective(debugNode: MockedDebugNode, directive: AbstractType): undefined | T; findDirectiveOrFail(debugNode: MockedDebugNode, directive: Type): T; + findDirectiveOrFail(debugNode: MockedDebugNode, directive: AbstractType): T; findDirectives(debugNode: MockedDebugNode, directive: Type): T[]; + findDirectives(debugNode: MockedDebugNode, directive: AbstractType): T[]; findOrFail(debugElement: MockedDebugElement, component: Type): MockedDebugElement; + findOrFail(debugElement: MockedDebugElement, component: AbstractType): MockedDebugElement; findOrFail(debugElement: MockedDebugElement, cssSelector: string): MockedDebugElement; getDirective(debugNode: MockedDebugNode, directive: Type): undefined | T; + getDirective(debugNode: MockedDebugNode, directive: AbstractType): undefined | T; getDirectiveOrFail(debugNode: MockedDebugNode, directive: Type): T; + getDirectiveOrFail(debugNode: MockedDebugNode, directive: AbstractType): T; getInput(debugNode: MockedDebugNode, input: string): undefined | T; getInputOrFail(debugNode: MockedDebugNode, input: string): T; getOutput(debugNode: MockedDebugNode, output: string): undefined | EventEmitter; @@ -94,6 +104,8 @@ export const ngMocks: { findInstance(debugNode: MockedDebugNode, instanceClass: Type, notFoundValue: D): D | T; findInstances(debugNode: MockedDebugNode, instanceClass: Type): T[]; + flushTestBed(): void; + get(debugNode: MockedDebugNode, directive: Type): T; get(debugNode: MockedDebugNode, directive: Type, notFoundValue: D): D | T; @@ -111,7 +123,7 @@ export const ngMocks: { const sel: any = args[1]; const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - const term = typeof sel === 'string' ? By.css(sel) : By.directive(sel); + const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); const result = el.query(term); if (result) { return result; @@ -125,7 +137,7 @@ export const ngMocks: { }, findAll: (el: MockedDebugElement, sel: any) => { - const term = typeof sel === 'string' ? By.css(sel) : By.directive(sel); + const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); return el.queryAll(term); }, @@ -134,7 +146,7 @@ export const ngMocks: { const sel: Type = args[1]; const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - const result = ngMocks.findInstances(el, sel); + const result = ngMocks.findInstances(el, getSourceOfMock(sel)); if (result.length) { return result[0]; } @@ -148,7 +160,7 @@ export const ngMocks: { const result: T[] = []; nestedCheck(result, el, node => { try { - return node.injector.get(sel); + return node.injector.get(getSourceOfMock(sel)); } catch (error) { return undefined; } @@ -164,7 +176,7 @@ export const ngMocks: { // Looking for related attribute directive. try { - return el.injector.get(sel); + return el.injector.get(getSourceOfMock(sel)); } catch (error) { // looks like the directive is structural. } @@ -181,7 +193,7 @@ export const ngMocks: { } const matchedNode = matches[0]; try { - return matchedNode.injector.get(sel); + return matchedNode.injector.get(getSourceOfMock(sel)); } catch (error) { notFound = true; } @@ -289,4 +301,10 @@ export const ngMocks: { } return instance; }, + + flushTestBed(): void { + (getTestBed() as any)._instantiated = false; + (getTestBed() as any)._moduleFactory = undefined; + (getTestBed() as any)._testModuleRef = null; + }, }; diff --git a/lib/mock-module/mock-module.spec.ts b/lib/mock-module/mock-module.spec.ts index 31e86af9a0..5dbaa3be1c 100644 --- a/lib/mock-module/mock-module.spec.ts +++ b/lib/mock-module/mock-module.spec.ts @@ -1,15 +1,15 @@ -/* tslint:disable:max-classes-per-file */ - import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { APP_INITIALIZER, ApplicationModule, Component, InjectionToken, NgModule } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ngModuleResolver } from '../common/reflect'; import { MockComponent } from '../mock-component'; +import { MockModule, MockProvider } from '../mock-module'; import { MockRender } from '../mock-render'; -import { MockModule } from './mock-module'; import { AppRoutingModule, CustomWithServiceComponent, @@ -91,7 +91,12 @@ describe('NeverMockModules', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SameImportsComponent], - imports: [MockModule(CommonModule), MockModule(BrowserModule), MockModule(BrowserAnimationsModule)], + imports: [ + MockModule(ApplicationModule), + MockModule(BrowserAnimationsModule), + MockModule(BrowserModule), + MockModule(CommonModule), + ], }) .compileComponents() .then(() => { @@ -184,33 +189,33 @@ describe('WithServiceModule', () => { }); }); -// TODO> Doesn't work because ParentModule doesn't export anything. -// TODO> Basically it's feature of ng-mocks to export declarations of mocked modules. -// describe('RealModule', () => { -// let fixture: ComponentFixture; -// -// beforeEach(async(() => { -// TestBed.configureTestingModule({ -// declarations: [ -// ComponentSubject -// ], -// imports: [ -// ParentModule, -// ], -// }) -// .compileComponents() -// .then(() => { -// fixture = TestBed.createComponent(ComponentSubject); -// fixture.detectChanges(); -// }); -// })); -// -// it('should do stuff', () => { -// expect(fixture.nativeElement.innerHTML) -// .toContain('My Example'); -// expect(fixture.nativeElement.innerHTML) -// .toContain('ExampleDirective'); -// expect(fixture.nativeElement.innerHTML) -// .toContain('Example: test'); -// }); -// }); +describe('MockProvider', () => { + const CUSTOM_TOKEN = new InjectionToken('TOKEN'); + + @NgModule({ + providers: [ + { + multi: true, + provide: HTTP_INTERCEPTORS, + useValue: 'MY_CUSTOM_VALUE', + }, + { + provide: CUSTOM_TOKEN, + useValue: 'MY_CUSTOM_VALUE', + }, + ], + }) + class CustomTokenModule {} + + it('should skip tokens in a mocked module', () => { + const mock = MockModule(CustomTokenModule); + const def = ngModuleResolver.resolve(mock); + expect(def.providers).toEqual([]); + }); + + it('should return undefined on any token', () => { + expect(MockProvider(CUSTOM_TOKEN)).toBeUndefined(); + expect(MockProvider(HTTP_INTERCEPTORS)).toBeUndefined(); + expect(MockProvider(APP_INITIALIZER)).toBeUndefined(); + }); +}); diff --git a/lib/mock-module/mock-module.ts b/lib/mock-module/mock-module.ts index 8fd28383cd..a3553c2eb5 100644 --- a/lib/mock-module/mock-module.ts +++ b/lib/mock-module/mock-module.ts @@ -1,86 +1,75 @@ import { CommonModule } from '@angular/common'; import { core } from '@angular/compiler'; -import { ModuleWithProviders, NgModule, Provider, Type } from '@angular/core'; +import { ApplicationModule, NgModule, Provider } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; -import { Mock, MockOf } from '../common'; -import { jitReflector, ngModuleResolver } from '../common/reflect'; -import { MockDeclaration } from '../mock-declaration'; +import { + flatten, + getMockedNgDefOf, + isNgDef, + isNgModuleDefWithProviders, + Mock, + MockOf, + NgModuleWithProviders, + Type, +} from '../common'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; +import { ngModuleResolver } from '../common/reflect'; +import { MockComponent } from '../mock-component'; +import { MockDirective } from '../mock-directive'; +import { MockPipe } from '../mock-pipe'; import { MockService } from '../mock-service'; -const cache = new Map, Type>(); - export type MockedModule = T & Mock & {}; -// Some modules inject own providers, which don't allow mocks due to conflicts with test env. -// We have to avoid any injection of those providers to mock everything properly. -const neverMockProvidedToken = [ - // RouterModule - 'InjectionToken Application Initializer', - // BrowserModule - 'InjectionToken EventManagerPlugins', - 'InjectionToken HammerGestureConfig', -]; -const neverMockProvidedFunction = [ - // BrowserModule - 'ApplicationInitStatus', - 'DomRendererFactory2', - 'DomSharedStylesHost', - 'EventManager', - // BrowserAnimationsModule - 'RendererFactory2', -]; - -const mockProvider = (provider: any): Provider | undefined => { +const neverMockProvidedFunction = ['DomRendererFactory2', 'RendererFactory2']; + +/** + * Can be changed any time. + * + * @internal + */ +export function MockProvider(provider: any): Provider | undefined { const provide = typeof provider === 'object' && provider.provide ? provider.provide : provider; - const multi = typeof provider === 'object' && provider.multi; + if (ngMocksUniverse.flags.has('cacheProvider') && ngMocksUniverse.cache.has(provide)) { + return ngMocksUniverse.cache.get(provide); + } - if ( - typeof provide === 'object' && - provide.ngMetadataName === 'InjectionToken' && - neverMockProvidedToken.includes(provide.toString()) - ) { - return provider; + // Tokens are special subject, we can skip adding them because in a mocked module they are useless. + // The main problem is providing undefined to HTTP_INTERCEPTORS and others breaks their code. + // If a testing module / component requires omitted tokens then they should be provided manually + // during creation of TestBed module. + if (typeof provide === 'object' && provide.ngMetadataName === 'InjectionToken') { + return undefined; } if (typeof provide === 'function' && neverMockProvidedFunction.includes(provide.name)) { return provider; } - return { - multi, + const mockedProvider: Provider = { provide, useValue: MockService(provide), }; -}; - -const flatten = (values: T | T[], result: T[] = []): T[] => { - if (Array.isArray(values)) { - values.forEach((value: T | T[]) => flatten(value, result)); - } else { - result.push(values); + if (ngMocksUniverse.flags.has('cacheProvider')) { + ngMocksUniverse.cache.set(provide, mockedProvider); } - return result; -}; - -// Checks if an object was decorated by NgModule. -const isModule = (object: any): object is Type => { - const annotations = jitReflector.annotations(object); - const ngMetadataNames = annotations.map(annotation => annotation.__proto__.ngMetadataName); - return ngMetadataNames.indexOf('NgModule') !== -1; -}; -// Checks if an object implements ModuleWithProviders. -const isModuleWithProviders = (object: any): object is ModuleWithProviders => - typeof object.ngModule !== 'undefined' && isModule(object.ngModule); + return mockedProvider; +} -export function MockModule(module: Type): Type>; -export function MockModule(module: ModuleWithProviders): ModuleWithProviders; +export function MockModule(module: Type): Type; +export function MockModule(module: NgModuleWithProviders): NgModuleWithProviders; export function MockModule(module: any): any { + // tslint:disable-line:cyclomatic-complexity let ngModule: Type; let ngModuleProviders: Provider[] | undefined; - let moduleMockPointer: Type; + let mockModule: typeof ngModule | undefined; + let mockModuleProviders: typeof ngModuleProviders; + let mockModuleDef: NgModule | undefined; + let releaseSkipMockFlag = false; - if (isModuleWithProviders(module)) { + if (isNgModuleDefWithProviders(module)) { ngModule = module.ngModule; if (module.providers) { ngModuleProviders = module.providers; @@ -93,98 +82,236 @@ export function MockModule(module: any): any { return module; } + // We are inside of an 'it'. + // It's fine to to return a mock or to throw an exception if it wasn't mocked in TestBed. + if (!ngModuleProviders && (getTestBed() as any)._instantiated) { + try { + return getMockedNgDefOf(ngModule, 'm'); + } catch (error) { + // looks like an in-test mock. + } + } + // Every module should be mocked only once to avoid errors like: // Failed: Type ...Component is part of the declarations of 2 modules: ...Module and ...Module... - const cacheHit = cache.get(ngModule); - if (cacheHit) { - moduleMockPointer = cacheHit; - } else { - @NgModule(MockIt(ngModule)) + if (ngMocksUniverse.flags.has('cacheModule') && ngMocksUniverse.cache.has(ngModule)) { + mockModule = ngMocksUniverse.cache.get(ngModule); + } + + // Now we check if we need to keep the original module or to replace it with some other. + if (!mockModule && ngMocksUniverse.builder.has(ngModule)) { + const instance = ngMocksUniverse.builder.get(ngModule); + if (isNgDef(instance, 'm') && instance !== ngModule) { + mockModule = instance; + } + if (!ngMocksUniverse.flags.has('skipMock')) { + releaseSkipMockFlag = true; + ngMocksUniverse.flags.add('skipMock'); + } + } + + if (!mockModule) { + let meta: core.NgModule | undefined; + if (!meta) { + try { + meta = ngModuleResolver.resolve(ngModule); + } catch (e) { + throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); + } + } + + const [changed, ngModuleDef] = MockNgModuleDef(meta, ngModule); + if (changed) { + mockModuleDef = ngModuleDef; + } + } + + if (mockModuleDef) { + const parent = ngMocksUniverse.flags.has('skipMock') ? ngModule : Mock; + + @NgModule(mockModuleDef) @MockOf(ngModule) - class ModuleMock extends Mock {} + class ModuleMock extends parent {} - moduleMockPointer = ModuleMock; - cache.set(ngModule, moduleMockPointer); + mockModule = ModuleMock; + if (ngMocksUniverse.flags.has('cacheModule')) { + ngMocksUniverse.cache.set(ngModule, mockModule); + } + } + if (!mockModule) { + mockModule = ngModule; } if (ngModuleProviders) { - return { - ngModule: moduleMockPointer, - providers: flatten(ngModuleProviders) - .map(mockProvider) - .filter(provider => !!provider) as Provider[], - }; - } - return moduleMockPointer; + const [changed, ngModuleDef] = MockNgModuleDef({ providers: ngModuleProviders }); + mockModuleProviders = changed ? ngModuleDef.providers : ngModuleProviders; + } + + if (releaseSkipMockFlag) { + ngMocksUniverse.flags.delete('skipMock'); + } + + return ngModuleProviders && ngModuleProviders.length + ? { ngModule: mockModule, providers: mockModuleProviders } + : mockModule; } -const NEVER_MOCK: Array> = [CommonModule]; +const NEVER_MOCK: Array> = [CommonModule, ApplicationModule]; -function MockIt(module: Type): NgModule { - const mockedModule: NgModule = {}; +// tslint:disable-next-line:cyclomatic-complexity +function MockNgModuleDef(ngModuleDef: NgModule, ngModule?: Type): [boolean, NgModule] { + let changed = false; + const mockedModuleDef: NgModule = {}; + const { + bootstrap = [], + declarations = [], + entryComponents = [], + exports = [], + imports = [], + providers = [], + } = ngModuleDef; - let meta: core.NgModule | undefined; - if (!meta) { - try { - meta = ngModuleResolver.resolve(module); - } catch (e) { - throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); + const resolutions = new Map(); + + // resolveProvider is a special case because of the def structure. + const resolveProvider = (def: any) => { + const provider = typeof def === 'object' && def.provide ? def.provide : def; + const multi = def !== provider && !!def.multi; + let mockedDef: typeof def; + if (resolutions.has(provider)) { + mockedDef = resolutions.get(provider); + return multi && typeof mockedDef === 'object' ? { ...mockedDef, multi } : mockedDef; + } + ngMocksUniverse.touches.add(provider); + + // Then we check decisions whether we should keep or replace a def. + if (!mockedDef && ngMocksUniverse.builder.has(provider)) { + mockedDef = ngMocksUniverse.builder.get(provider); + if (mockedDef === provider) { + mockedDef = def; + } else if (mockedDef === undefined) { + mockedDef = { + provide: provider, + useValue: undefined, + }; + } + } + + if (!mockedDef && ngMocksUniverse.flags.has('skipMock')) { + mockedDef = def; + } + if (!mockedDef) { + mockedDef = MockProvider(def); + } + + resolutions.set(provider, mockedDef); + changed = changed || mockedDef !== def; + return multi && typeof mockedDef === 'object' ? { ...mockedDef, multi } : mockedDef; + }; + + const resolve = (def: any) => { + let mockedDef: typeof def; + if (resolutions.has(def)) { + return resolutions.get(def); + } + ngMocksUniverse.touches.add(isNgModuleDefWithProviders(def) ? def.ngModule : def); + + // First we mock modules. + if (!mockedDef && isNgDef(def, 'm')) { + mockedDef = MockModule(def); + } + if (!mockedDef && isNgModuleDefWithProviders(def)) { + mockedDef = MockModule(def); + resolutions.set(def.ngModule, mockedDef.ngModule); + } + + // Then we check decisions whether we should keep or replace a def. + if (!mockedDef && ngMocksUniverse.builder.has(def)) { + mockedDef = ngMocksUniverse.builder.get(def); } + + // And then we mock what we have if it wasn't blocked by the skipMock. + if (!mockedDef && ngMocksUniverse.flags.has('skipMock')) { + mockedDef = def; + } + if (!mockedDef && isNgDef(def, 'c')) { + mockedDef = MockComponent(def); + } + if (!mockedDef && isNgDef(def, 'd')) { + mockedDef = MockDirective(def); + } + if (!mockedDef && isNgDef(def, 'p')) { + mockedDef = MockPipe(def); + } + if (!mockedDef) { + mockedDef = resolveProvider(def); + } + + resolutions.set(def, mockedDef); + changed = changed || mockedDef !== def; + return mockedDef; + }; + + if (imports && imports.length) { + mockedModuleDef.imports = flatten(imports).map(resolve); } - const { declarations = [], entryComponents = [], imports = [], providers = [] } = meta; + if (declarations && declarations.length) { + mockedModuleDef.declarations = flatten(declarations).map(resolve); + } - if (imports.length) { - mockedModule.imports = flatten(imports).map((instance: Type) => { - if (isModule(instance)) { - return MockModule(instance); - } - if (isModuleWithProviders(instance)) { - return MockModule(instance); - } - return MockDeclaration(instance); - }); + if (entryComponents && entryComponents.length) { + mockedModuleDef.entryComponents = flatten(entryComponents).map(resolve); } - if (declarations.length) { - mockedModule.declarations = flatten(declarations).map(MockDeclaration); + if (bootstrap && bootstrap.length) { + mockedModuleDef.bootstrap = flatten(bootstrap).map(resolve); } - if (entryComponents.length) { - mockedModule.entryComponents = flatten(entryComponents).map(MockDeclaration); // tslint:disable-line:deprecation + if (providers && providers.length) { + mockedModuleDef.providers = flatten(providers) + .map(resolveProvider) + .filter(provider => provider); } - if (providers.length) { - mockedModule.providers = flatten(providers) - .map(mockProvider) - .filter(provider => !!provider) as Provider[]; + // Default exports. + if (exports && exports.length) { + mockedModuleDef.exports = flatten(exports).map(resolve); } - // When we mock module only exported declarations are accessible inside of test. - // Because of that we have to export everything what a module imports or declares. - // Unfortunately in that case tests won't fail when some module has missed exports. - if (mockedModule.declarations || mockedModule.imports) { - mockedModule.exports = []; + // if we are in the skipMock mode we need to export only the default exports. + // if we are in the correctModuleExports mode we need to export only default exports. + const correctExports = ngMocksUniverse.flags.has('skipMock') || ngMocksUniverse.flags.has('correctModuleExports'); - if (mockedModule.imports) { - const onlyModules = mockedModule.imports - .map(instance => { - if (isModule(instance)) { - return instance; - } - if (isModuleWithProviders(instance)) { - return instance.ngModule; - } - return undefined; - }) - .filter(instance => instance) as Array>; - mockedModule.exports = [...mockedModule.exports, ...onlyModules]; + // When we mock a module, only exported declarations are accessible inside of a test. + // Because of that we have to export whatever a module imports or declares. + // Unfortunately, in this case tests won't fail when a module has missed exports. + // MockBuilder doesn't have have this issue. + for (const def of flatten([imports || [], declarations || []])) { + const instance = isNgModuleDefWithProviders(def) ? def.ngModule : def; + const mockedDef = resolve(instance); + + // If we export a declaration, then we have to export its module too. + const config = ngMocksUniverse.config.get(instance) || {}; + if (config.export && ngModule) { + const moduleConfig = ngMocksUniverse.config.get(ngModule) || {}; + if (!moduleConfig.export) { + moduleConfig.export = true; + ngMocksUniverse.config.set(ngModule, moduleConfig); + } } - if (mockedModule.declarations) { - mockedModule.exports = [...mockedModule.exports, ...mockedModule.declarations]; + if (correctExports && !config.export) { + continue; } + if (mockedModuleDef.exports && mockedModuleDef.exports.indexOf(mockedDef) !== -1) { + continue; + } + + changed = true; + mockedModuleDef.exports = mockedModuleDef.exports || []; + mockedModuleDef.exports.push(mockedDef); } - return mockedModule; + return [changed, mockedModuleDef]; } diff --git a/lib/mock-module/test-fixtures.ts b/lib/mock-module/test-fixtures.ts index 08abd82871..538cd7e1bd 100644 --- a/lib/mock-module/test-fixtures.ts +++ b/lib/mock-module/test-fixtures.ts @@ -1,18 +1,7 @@ -/* tslint:disable:max-classes-per-file */ - import { CommonModule } from '@angular/common'; -import { - Component, - Directive, - ElementRef, - Injectable, - ModuleWithProviders, - NgModule, - OnInit, - Pipe, - PipeTransform, -} from '@angular/core'; +import { Component, Directive, ElementRef, Injectable, NgModule, OnInit, Pipe, PipeTransform } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { NgModuleWithProviders } from '../common'; @Directive({ selector: '[example-directive]' }) export class ExampleDirective implements OnInit { @@ -111,7 +100,7 @@ class RealModuleWithProvidersModule {} // Factory to setup module with provider. /* tslint:disable:no-unnecessary-class */ class ModuleProvider { - static withFlag(flag: boolean): ModuleWithProviders { + static withFlag(flag: boolean): NgModuleWithProviders { return { ngModule: RealModuleWithProvidersModule, providers: [ diff --git a/lib/mock-pipe/mock-pipe.spec.ts b/lib/mock-pipe/mock-pipe.spec.ts index 4dbadee079..e7f4e266ef 100644 --- a/lib/mock-pipe/mock-pipe.spec.ts +++ b/lib/mock-pipe/mock-pipe.spec.ts @@ -9,8 +9,6 @@ export class ExamplePipe implements PipeTransform { transform = (args: string): string => 'hi'; } -// tslint:disable:max-classes-per-file - @Pipe({ name: 'anotherMockedPipe' }) export class AnotherExamplePipe implements PipeTransform { transform = (args: string): string => 'hi'; @@ -27,8 +25,6 @@ export class ExampleComponent { someStuff = 'bah'; } -// tslint:enable:max-classes-per-file - describe('MockPipe', () => { let fixture: ComponentFixture; diff --git a/lib/mock-pipe/mock-pipe.ts b/lib/mock-pipe/mock-pipe.ts index b5ccc3bb56..31c1cfc836 100644 --- a/lib/mock-pipe/mock-pipe.ts +++ b/lib/mock-pipe/mock-pipe.ts @@ -1,7 +1,9 @@ import { core } from '@angular/compiler'; -import { Pipe, PipeTransform, Type } from '@angular/core'; +import { Pipe, PipeTransform } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; -import { Mock, MockOf } from '../common'; +import { getMockedNgDefOf, Mock, MockOf, Type } from '../common'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { pipeResolver } from '../common/reflect'; export type MockedPipe = T & Mock & {}; @@ -15,6 +17,19 @@ export function MockPipe( pipe: Type, transform: TPipe['transform'] = defaultTransform ): Type> { + // We are inside of an 'it'. + // It's fine to to return a mock or to throw an exception if it wasn't mocked in TestBed. + if ((getTestBed() as any)._instantiated) { + try { + return getMockedNgDefOf(pipe, 'p'); + } catch (error) { + // looks like an in-test mock. + } + } + if (ngMocksUniverse.flags.has('cachePipe') && ngMocksUniverse.cache.has(pipe)) { + return ngMocksUniverse.cache.get(pipe); + } + let meta: core.Pipe | undefined; if (!meta) { try { @@ -36,6 +51,9 @@ export function MockPipe( } const mockedPipe: Type> = Pipe(options)(PipeMock as any); + if (ngMocksUniverse.flags.has('cachePipe')) { + ngMocksUniverse.cache.set(pipe, mockedPipe); + } return mockedPipe; } diff --git a/lib/mock-render/mock-render.fixtures.ts b/lib/mock-render/mock-render.fixtures.ts index c20c300edc..c7a73ef855 100644 --- a/lib/mock-render/mock-render.fixtures.ts +++ b/lib/mock-render/mock-render.fixtures.ts @@ -16,3 +16,8 @@ export class RenderRealComponent { this.document.getElementById('test'); } } + +@Component({ + template: 'WithoutSelectorComponent', +}) +export class WithoutSelectorComponent {} diff --git a/lib/mock-render/mock-render.spec.ts b/lib/mock-render/mock-render.spec.ts index 595f7010d0..f0ef45e742 100644 --- a/lib/mock-render/mock-render.spec.ts +++ b/lib/mock-render/mock-render.spec.ts @@ -6,12 +6,12 @@ import { By } from '@angular/platform-browser'; import { MockService } from '../mock-service'; import { MockedComponentFixture, MockRender } from './mock-render'; -import { RenderRealComponent } from './mock-render.fixtures'; +import { RenderRealComponent, WithoutSelectorComponent } from './mock-render.fixtures'; describe('MockRender', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [RenderRealComponent], + declarations: [RenderRealComponent, WithoutSelectorComponent], }); }); @@ -112,4 +112,9 @@ describe('MockRender', () => { ); expect(document.getElementById).toHaveBeenCalledWith('test'); }); + + it('does not render a component without selector', () => { + const fixture = MockRender(WithoutSelectorComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual(''); + }); }); diff --git a/lib/mock-render/mock-render.ts b/lib/mock-render/mock-render.ts index 7864a8ae29..7cc79c5981 100644 --- a/lib/mock-render/mock-render.ts +++ b/lib/mock-render/mock-render.ts @@ -1,10 +1,10 @@ -// tslint:disable:unified-signatures - import { core } from '@angular/compiler'; -import { Component, DebugElement, DebugNode, Provider, Type } from '@angular/core'; -import { ComponentFixture, getTestBed, TestBed } from '@angular/core/testing'; +import { Component, DebugElement, DebugNode, Provider } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Type } from '../common'; import { directiveResolver } from '../common/reflect'; +import { ngMocks } from '../mock-helper'; // A5 and its TS 2.4 don't support Omit, that's why we need the magic below. // TODO remove it once A5 isn't supported. @@ -86,8 +86,8 @@ function MockRender( } const { inputs, outputs, selector } = meta; - mockedTemplate += `<${selector}`; - if (inputs) { + mockedTemplate += selector ? `<${selector}` : ''; + if (selector && inputs) { inputs.forEach((definition: string) => { const [property, alias] = definition.split(': '); if (alias && params && typeof params[alias]) { @@ -97,7 +97,7 @@ function MockRender( } }); } - if (outputs) { + if (selector && outputs) { outputs.forEach((definition: string) => { const [property, alias] = definition.split(': '); if (alias && params && typeof params[alias]) { @@ -107,7 +107,7 @@ function MockRender( } }); } - mockedTemplate += `>`; + mockedTemplate += selector ? `>` : ''; } const options: Component = { providers: flagsObject.providers, @@ -124,9 +124,7 @@ function MockRender( ); // Soft reset of TestBed. - (getTestBed() as any)._instantiated = false; - (getTestBed() as any)._moduleFactory = undefined; - (getTestBed() as any)._testModuleRef = null; + ngMocks.flushTestBed(); // Injection of our template. TestBed.configureTestingModule({ diff --git a/tests-jasmine/context-with-directives/context-with-directives.spec.ts b/tests-jasmine/context-with-directives/context-with-directives.spec.ts index b7bed105fe..c79db7edcb 100644 --- a/tests-jasmine/context-with-directives/context-with-directives.spec.ts +++ b/tests-jasmine/context-with-directives/context-with-directives.spec.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { MockDirective, MockRender } from 'ng-mocks'; +import { MockDirective, MockRender, ngMocks } from 'ng-mocks'; import { CustomRootComponent } from './custom-root.component'; import { CustomTypeDirective } from './custom-type.directive'; @@ -40,25 +39,30 @@ describe('context-with-directives:real', () => { `); // template should be rendered under .template - expect(fixture.debugElement.query(By.css('.template')).nativeElement.innerText).toContain( - 'template w/ directive w/o binding' + expect(ngMocks.find(fixture.debugElement, '.template').nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain( + ' template w/ directive w/o binding ' ); // template1 should be rendered under .template1 - expect(fixture.debugElement.query(By.css('.template1')).nativeElement.innerText).toContain( - 'template w/ directive w/ binding 1' + expect(ngMocks.find(fixture.debugElement, '.template1').nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain( + ' template w/ directive w/ binding 1 ' ); // template2 should not be rendered - expect(fixture.nativeElement.innerText).not.toContain('template w/ directive w/ binding w/o render'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).not.toContain( + ' template w/ directive w/ binding w/o render ' + ); // unused ng-templates shouldn't be rendered at all - expect(fixture.nativeElement.innerText).not.toContain('template w/o directive w/o binding'); - expect(fixture.nativeElement.innerText).not.toContain('template w/o directive w/ binding'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).not.toContain(' template w/o directive w/o binding '); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).not.toContain(' template w/o directive w/ binding '); // ng-content contains header and footer - expect(fixture.debugElement.query(By.css('.nested')).nativeElement.innerText.replace(/\s+/, ' ')).toContain( - 'header footer' + expect(ngMocks.find(fixture.debugElement, '.nested').nativeElement.innerHTML.replace(/\s+/, ' ')).toContain( + '
header
' + ); + expect(ngMocks.find(fixture.debugElement, '.nested').nativeElement.innerHTML.replace(/\s+/, ' ')).toContain( + '
footer
' ); }); }); @@ -95,12 +99,12 @@ describe('context-with-directives:mock', () => { `); - expect(fixture.nativeElement.innerText).toContain('header'); - expect(fixture.nativeElement.innerText).toContain('footer'); + expect(fixture.nativeElement.innerHTML).toContain('
header
'); + expect(fixture.nativeElement.innerHTML).toContain('
footer
'); // No templates should be rendered when we mock them. // The reason for that is that only directive knows when to render it, that means if we want to render, // we should do that manually. - expect(fixture.nativeElement.innerText).not.toContain('template'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).not.toContain(' template '); }); }); diff --git a/tests-jasmine/control-value-accessor-form-control/fixtures.ts b/tests-jasmine/control-value-accessor-form-control/fixtures.ts new file mode 100644 index 0000000000..5bf4d5500f --- /dev/null +++ b/tests-jasmine/control-value-accessor-form-control/fixtures.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component, forwardRef, NgModule } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'target', + template: '', +}) +export class TargetComponent { + public readonly control = new FormControl(); +} + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ControlComponent), + }, + ], + selector: 'control', + template: '', +}) +export class ControlComponent implements ControlValueAccessor { + public isDisabled = false; + public value: any; + public change: any = () => undefined; + + changeTouch(): void { + this.touch(); + } + + changeValue(obj: any): void { + this.change(obj); + } + + registerOnChange(fn: any): void { + this.change = fn; + } + + registerOnTouched(fn: any): void { + this.touch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public touch: any = () => undefined; + + writeValue(obj: any): void { + this.value = obj; + } +} + +@NgModule({ + declarations: [TargetComponent, ControlComponent], + exports: [TargetComponent], + imports: [CommonModule, ReactiveFormsModule], +}) +export class TargetModule {} diff --git a/tests-jasmine/control-value-accessor-form-control/test.spec.ts b/tests-jasmine/control-value-accessor-form-control/test.spec.ts new file mode 100644 index 0000000000..562ffc7c30 --- /dev/null +++ b/tests-jasmine/control-value-accessor-form-control/test.spec.ts @@ -0,0 +1,107 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { MockBuilder, MockComponent, MockHelper, MockRender } from 'ng-mocks'; + +import { ControlComponent, TargetComponent, TargetModule } from './fixtures'; + +// a real case to check possible behavior. +describe('control-value-accessor-form-control:real', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('respects our formControl', () => { + const fixture = MockRender(TargetComponent, {}, false); + const mock = MockHelper.findOrFail(fixture.debugElement, ControlComponent).componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + fixture.detectChanges(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.control.setValue('test1'); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + fixture.point.componentInstance.control.setValue('test2'); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that touch works + mock.changeTouch(); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + + // checking that reset works + fixture.point.componentInstance.control.markAsUntouched(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that disabled works + fixture.point.componentInstance.control.disable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.control.enable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.changeValue('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test3'); + }); +}); + +// a way that ensures that a mocked component behaves the same way as real one. +describe('control-value-accessor-form-control:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(ReactiveFormsModule)); + + it('respects our formControl', () => { + const fixture = MockRender(TargetComponent, {}, false); + const mock = MockHelper.findOrFail(fixture.debugElement, MockComponent(ControlComponent)).componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + spyOn(mock, 'registerOnChange').and.callThrough(); + spyOn(mock, 'registerOnTouched').and.callThrough(); + fixture.detectChanges(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.control.setValue('test1'); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + fixture.point.componentInstance.control.setValue('test2'); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that touch works + mock.__simulateTouch(); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + fixture.point.componentInstance.control.markAsUntouched(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + // a way through a spy + MockHelper.mockService(mock, 'registerOnTouched').calls.first().args[0](); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + fixture.point.componentInstance.control.markAsUntouched(); + + // checking that disabled works + fixture.point.componentInstance.control.disable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.control.enable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.__simulateChange('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test3'); + // a way through a spy + MockHelper.mockService(mock, 'registerOnChange').calls.first().args[0]('test4'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test4'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test4'); + }); +}); diff --git a/tests-jasmine/control-value-accessor-ng-model/fixtures.ts b/tests-jasmine/control-value-accessor-ng-model/fixtures.ts new file mode 100644 index 0000000000..c423dfcbe3 --- /dev/null +++ b/tests-jasmine/control-value-accessor-ng-model/fixtures.ts @@ -0,0 +1,70 @@ +import { CommonModule } from '@angular/common'; +import { Component, forwardRef, NgModule } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'target', + template: '', +}) +export class TargetComponent { + public disabled = false; + public realValue: null | string = null; + + public get value(): null | string { + return this.realValue; + } + + public set value(value: null | string) { + this.realValue = value; + } +} + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ControlComponent), + }, + ], + selector: 'control', + template: '', +}) +export class ControlComponent implements ControlValueAccessor { + public isDisabled = false; + public value: any; + public change: any = () => undefined; + + changeTouch(): void { + this.touch(); + } + + changeValue(obj: any): void { + this.change(obj); + } + + registerOnChange(fn: any): void { + this.change = fn; + } + + registerOnTouched(fn: any): void { + this.touch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public touch: any = () => undefined; + + writeValue(obj: any): void { + this.value = obj; + } +} + +@NgModule({ + declarations: [TargetComponent, ControlComponent], + exports: [TargetComponent], + imports: [CommonModule, FormsModule], +}) +export class TargetModule {} diff --git a/tests-jasmine/control-value-accessor-ng-model/test.spec.ts b/tests-jasmine/control-value-accessor-ng-model/test.spec.ts new file mode 100644 index 0000000000..0817a7e3b9 --- /dev/null +++ b/tests-jasmine/control-value-accessor-ng-model/test.spec.ts @@ -0,0 +1,129 @@ +import { FormsModule, NgModel } from '@angular/forms'; +import { MockBuilder, MockComponent, MockHelper, MockRender } from 'ng-mocks'; + +import { ControlComponent, TargetComponent, TargetModule } from './fixtures'; + +// a real case to check possible behavior. +describe('control-value-accessor-ng-model:real', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('respects our ngModel', async () => { + const fixture = MockRender(TargetComponent, {}, false); + const mockElement = MockHelper.findOrFail(fixture.debugElement, ControlComponent); + const mock = mockElement.componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + const ngModel = MockHelper.getDirectiveOrFail(mockElement, NgModel); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(ngModel.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.value = 'test1'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(ngModel.touched).toBeFalsy(); + + fixture.point.componentInstance.value = 'test2'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(ngModel.touched).toBeFalsy(); + + // checking that touch works + mock.changeTouch(); + expect(ngModel.touched).toBeTruthy(); + + // checking that reset works + ngModel.control.markAsUntouched(); + expect(ngModel.touched).toBeFalsy(); + + // checking that disabled works + fixture.point.componentInstance.disabled = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.disabled = false; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.changeValue('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(ngModel.touched).toBeFalsy(); + expect(fixture.point.componentInstance.value).toBe('test3'); + }); +}); + +// a way that ensures that a mocked component behaves the same way as real one. +describe('control-value-accessor-ng-model:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(FormsModule)); + + it('respects our ngModel', async () => { + const fixture = MockRender(TargetComponent, {}, false); + const mockElement = MockHelper.findOrFail(fixture.debugElement, MockComponent(ControlComponent)); + const mock = mockElement.componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + spyOn(mock, 'registerOnChange').and.callThrough(); + spyOn(mock, 'registerOnTouched').and.callThrough(); + const ngModel = MockHelper.getDirectiveOrFail(mockElement, NgModel); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(ngModel.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.value = 'test1'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(ngModel.touched).toBeFalsy(); + + fixture.point.componentInstance.value = 'test2'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(ngModel.touched).toBeFalsy(); + + // checking that touch works + mock.__simulateTouch(); + expect(ngModel.touched).toBeTruthy(); + ngModel.control.markAsUntouched(); + expect(ngModel.touched).toBeFalsy(); + // a way through a spy + MockHelper.mockService(mock, 'registerOnTouched').calls.first().args[0](); + expect(ngModel.touched).toBeTruthy(); + ngModel.control.markAsUntouched(); + + // checking that disabled works + fixture.point.componentInstance.disabled = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.disabled = false; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.__simulateChange('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(ngModel.touched).toBeFalsy(); + expect(fixture.point.componentInstance.value).toBe('test3'); + // a way through a spy + MockHelper.mockService(mock, 'registerOnChange').calls.first().args[0]('test4'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test4'); + expect(ngModel.touched).toBeFalsy(); + expect(ngModel.value).toBe('test4'); + }); +}); diff --git a/tests-jasmine/exports-only/fixtures.components.ts b/tests-jasmine/exports-only/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jasmine/exports-only/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jasmine/exports-only/fixtures.modules.ts b/tests-jasmine/exports-only/fixtures.modules.ts new file mode 100644 index 0000000000..95e2906ecc --- /dev/null +++ b/tests-jasmine/exports-only/fixtures.modules.ts @@ -0,0 +1,16 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], + imports: [CommonModule], +}) +export class InternalModule {} + +@NgModule({ + exports: [InternalModule], +}) +export class TargetModule {} diff --git a/tests-jasmine/exports-only/test.spec.ts b/tests-jasmine/exports-only/test.spec.ts new file mode 100644 index 0000000000..cbe0a3d431 --- /dev/null +++ b/tests-jasmine/exports-only/test.spec.ts @@ -0,0 +1,64 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('ExportsOnly:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('internal'); + }); +}); + +describe('ExportsOnly:mock1', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); + +describe('ExportsOnly:mock2', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); + +describe('ExportsOnly:mock3', () => { + beforeEach(async done => { + await MockBuilder().keep(TargetModule); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('internal'); + }); +}); diff --git a/tests-jasmine/injected-ng-templates/custom-injection.component.ts b/tests-jasmine/injected-ng-templates/custom-injection.component.ts index c050f2eb8e..453f894806 100644 --- a/tests-jasmine/injected-ng-templates/custom-injection.component.ts +++ b/tests-jasmine/injected-ng-templates/custom-injection.component.ts @@ -1,6 +1,6 @@ import { Component, ContentChild, Input, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; -import { staticFalse } from '../index'; +import { staticFalse } from '..'; @Component({ selector: 'custom-injection', diff --git a/tests-jasmine/internal-only-nested/fixtures.components.ts b/tests-jasmine/internal-only-nested/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jasmine/internal-only-nested/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jasmine/internal-only-nested/fixtures.modules.ts b/tests-jasmine/internal-only-nested/fixtures.modules.ts new file mode 100644 index 0000000000..643509a790 --- /dev/null +++ b/tests-jasmine/internal-only-nested/fixtures.modules.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + imports: [CommonModule], +}) +export class Nested1Module {} + +@NgModule({ + imports: [Nested1Module], +}) +export class Nested2Module {} + +@NgModule({ + imports: [Nested1Module], +}) +export class Nested3Module {} + +@NgModule({ + imports: [Nested2Module, Nested3Module], +}) +export class TargetModule {} diff --git a/tests-jasmine/internal-only-nested/test.spec.ts b/tests-jasmine/internal-only-nested/test.spec.ts new file mode 100644 index 0000000000..304eecd838 --- /dev/null +++ b/tests-jasmine/internal-only-nested/test.spec.ts @@ -0,0 +1,37 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalOnlyNested:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalOnlyNested:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent, { export: true }); + done(); + }); + + // The expectation is to see that InternalComponent was exported to the level of the TestingModule + // and can be accessed in the test even it was deeply nested. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); diff --git a/tests-jasmine/internal-only/fixtures.components.ts b/tests-jasmine/internal-only/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jasmine/internal-only/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jasmine/internal-only/fixtures.modules.ts b/tests-jasmine/internal-only/fixtures.modules.ts new file mode 100644 index 0000000000..b9d4de82e8 --- /dev/null +++ b/tests-jasmine/internal-only/fixtures.modules.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + imports: [CommonModule], +}) +export class TargetModule {} diff --git a/tests-jasmine/internal-only/test.spec.ts b/tests-jasmine/internal-only/test.spec.ts new file mode 100644 index 0000000000..42d5e9332d --- /dev/null +++ b/tests-jasmine/internal-only/test.spec.ts @@ -0,0 +1,36 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalOnly:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalOnly:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent, { export: true }); + done(); + }); + + // The expectation is to see that InternalComponent was exported and can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); diff --git a/tests-jasmine/internal-vs-external/fixtures.components.ts b/tests-jasmine/internal-vs-external/fixtures.components.ts new file mode 100644 index 0000000000..bc86a0d881 --- /dev/null +++ b/tests-jasmine/internal-vs-external/fixtures.components.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} + +@Component({ + selector: 'external-component', + template: 'external ', +}) +export class ExternalComponent {} diff --git a/tests-jasmine/internal-vs-external/fixtures.modules.ts b/tests-jasmine/internal-vs-external/fixtures.modules.ts new file mode 100644 index 0000000000..f7c1d56d56 --- /dev/null +++ b/tests-jasmine/internal-vs-external/fixtures.modules.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ExternalComponent, InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent, ExternalComponent], + exports: [ExternalComponent], + imports: [CommonModule], +}) +export class TargetModule {} diff --git a/tests-jasmine/internal-vs-external/test.spec.ts b/tests-jasmine/internal-vs-external/test.spec.ts new file mode 100644 index 0000000000..fc2a47279a --- /dev/null +++ b/tests-jasmine/internal-vs-external/test.spec.ts @@ -0,0 +1,70 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockModule, MockRender } from 'ng-mocks'; + +import { ExternalComponent, InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalVsExternal:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toContain('external internal'); + + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalVsExternal:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + // The expectation is to see that ExternalComponent was exported and InternalComponent wasn't. + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalVsExternal:legacy', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [MockModule(TargetModule)], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + + // the code below will fail because the MockModule outside of the MockBuilder exports everything. + // try { + // MockRender(InternalComponent); + // fail('should fail on the internal component'); + // } catch (e) { + // expect(e).toEqual(jasmine.objectContaining({ngSyntaxError: true})); + // } + }); +}); diff --git a/tests-jasmine/mock-builder-by-directive/fixtures.components.ts b/tests-jasmine/mock-builder-by-directive/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jasmine/mock-builder-by-directive/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jasmine/mock-builder-by-directive/fixtures.modules.ts b/tests-jasmine/mock-builder-by-directive/fixtures.modules.ts new file mode 100644 index 0000000000..430a37d058 --- /dev/null +++ b/tests-jasmine/mock-builder-by-directive/fixtures.modules.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], +}) +export class TargetModule {} diff --git a/tests-jasmine/mock-builder-by-directive/test.spec.ts b/tests-jasmine/mock-builder-by-directive/test.spec.ts new file mode 100644 index 0000000000..a0e5e6c877 --- /dev/null +++ b/tests-jasmine/mock-builder-by-directive/test.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockComponent, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('MockBuilderByDirective:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(InternalComponent)); + expect(element).toBeDefined(); + }); +}); + +describe('MockBuilderByDirective:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + it('should find mock', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(MockComponent(InternalComponent))); + expect(element).toBeDefined(); + }); + + it('should find original', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(InternalComponent)); + expect(element).toBeDefined(); + }); +}); diff --git a/tests-jasmine/mock-builder-keeps-application-module/fixtures.components.ts b/tests-jasmine/mock-builder-keeps-application-module/fixtures.components.ts new file mode 100644 index 0000000000..d23ac6421a --- /dev/null +++ b/tests-jasmine/mock-builder-keeps-application-module/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'target-component', + template: 'target', +}) +export class TargetComponent {} diff --git a/tests-jasmine/mock-builder-keeps-application-module/fixtures.modules.ts b/tests-jasmine/mock-builder-keeps-application-module/fixtures.modules.ts new file mode 100644 index 0000000000..dd7e1ea0d1 --- /dev/null +++ b/tests-jasmine/mock-builder-keeps-application-module/fixtures.modules.ts @@ -0,0 +1,26 @@ +import { APP_ID, APP_INITIALIZER, InjectionToken, NgModule } from '@angular/core'; + +import { TargetComponent } from './fixtures.components'; + +export const TARGET_TOKEN = new InjectionToken('TARGET_TOKEN'); + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + providers: [ + { + provide: TARGET_TOKEN, + useValue: 'TARGET_TOKEN', + }, + { + provide: APP_ID, + useValue: 'random', + }, + { + multi: true, + provide: APP_INITIALIZER, + useValue: () => undefined, + }, + ], +}) +export class TargetModule {} diff --git a/tests-jasmine/mock-builder-keeps-application-module/test.spec.ts b/tests-jasmine/mock-builder-keeps-application-module/test.spec.ts new file mode 100644 index 0000000000..b79c18fdf3 --- /dev/null +++ b/tests-jasmine/mock-builder-keeps-application-module/test.spec.ts @@ -0,0 +1,41 @@ +import { APP_ID, APP_INITIALIZER, VERSION } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { TargetComponent } from './fixtures.components'; +import { TARGET_TOKEN, TargetModule } from './fixtures.modules'; + +describe('MockBuilderKeepsApplicationModule:real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + const element = fixture.debugElement.query(By.directive(TargetComponent)); + expect(element).toBeDefined(); + expect(TestBed.get(TARGET_TOKEN)).toBeDefined(); + expect(TestBed.get(APP_INITIALIZER)).toBeDefined(); + expect(TestBed.get(APP_ID)).toBeDefined(); + }); +}); + +describe('MockBuilderKeepsApplicationModule:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + const element = fixture.debugElement.query(By.directive(TargetComponent)); + expect(element).toBeDefined(); + expect(() => TestBed.get(TARGET_TOKEN)).toThrow(); + if (VERSION.major !== '9') { + // somehow ivy doesn't provide APP_INITIALIZER out of the box and this assertion fails. + // our mock logic skips all multi tokens therefore this one isn't present anymore. + expect(TestBed.get(APP_INITIALIZER)).toBeDefined(); + } + expect(TestBed.get(APP_ID)).toBeDefined(); + }); +}); diff --git a/tests-jasmine/module-with-factory-tokens/fixtures.ts b/tests-jasmine/module-with-factory-tokens/fixtures.ts new file mode 100644 index 0000000000..3ad0365843 --- /dev/null +++ b/tests-jasmine/module-with-factory-tokens/fixtures.ts @@ -0,0 +1,28 @@ +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 {} diff --git a/tests-jasmine/module-with-factory-tokens/test.spec.ts b/tests-jasmine/module-with-factory-tokens/test.spec.ts new file mode 100644 index 0000000000..995bc2e767 --- /dev/null +++ b/tests-jasmine/module-with-factory-tokens/test.spec.ts @@ -0,0 +1,111 @@ +import { VERSION } from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).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.innerHTML.replace(/\s+/gm, ' ')).toContain('"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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML).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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toEqual( + '"MY_TOKEN_SINGLE" "MY_TOKEN_MULTI"' + ); + }); +}); diff --git a/tests-jasmine/module-with-tokens/fixtures.ts b/tests-jasmine/module-with-tokens/fixtures.ts new file mode 100644 index 0000000000..e39123fde0 --- /dev/null +++ b/tests-jasmine/module-with-tokens/fixtures.ts @@ -0,0 +1,40 @@ +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 {} diff --git a/tests-jasmine/module-with-tokens/test.spec.ts b/tests-jasmine/module-with-tokens/test.spec.ts new file mode 100644 index 0000000000..22c0984e5c --- /dev/null +++ b/tests-jasmine/module-with-tokens/test.spec.ts @@ -0,0 +1,97 @@ +import { MockBuilder, MockRender } from 'ng-mocks'; + +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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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/); + }); +}); diff --git a/tests-jasmine/nested-before-each/fixtures.components.ts b/tests-jasmine/nested-before-each/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jasmine/nested-before-each/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jasmine/nested-before-each/fixtures.modules.ts b/tests-jasmine/nested-before-each/fixtures.modules.ts new file mode 100644 index 0000000000..430a37d058 --- /dev/null +++ b/tests-jasmine/nested-before-each/fixtures.modules.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], +}) +export class TargetModule {} diff --git a/tests-jasmine/nested-before-each/test.spec.ts b/tests-jasmine/nested-before-each/test.spec.ts new file mode 100644 index 0000000000..1aa18a7b6a --- /dev/null +++ b/tests-jasmine/nested-before-each/test.spec.ts @@ -0,0 +1,78 @@ +import { Type } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; + +describe('nested-before-each', () => { + let level = 0; + let mock: Type; + + beforeEach(() => { + level = 0; + mock = MockComponent(InternalComponent); + TestBed.configureTestingModule({ + declarations: [mock], + }); + return TestBed.compileComponents(); + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + it('should have the same mock after the first run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + + it('should have the same mock after the second run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + }); + }); +}); + +describe('nested-before-all', () => { + let level = 0; + let mock: Type; + + beforeAll(() => { + level = 0; + mock = MockComponent(InternalComponent); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + declarations: [mock], + }); + return TestBed.compileComponents(); + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + it('should have the same mock after the first run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + + it('should have the same mock after the second run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + }); + }); +}); diff --git a/tests-jasmine/normal-usage-after-mock-builder/fixtures.components.ts b/tests-jasmine/normal-usage-after-mock-builder/fixtures.components.ts new file mode 100644 index 0000000000..78929f1109 --- /dev/null +++ b/tests-jasmine/normal-usage-after-mock-builder/fixtures.components.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TargetService } from './fixtures.services'; + +@Component({ + selector: 'root', + template: '{{ service.called }}', +}) +export class TargetComponent { + public readonly service: TargetService; + + constructor(service: TargetService) { + this.service = service; + } +} + +@Component({ + selector: 'internal', + template: 'real', +}) +export class RealComponent {} + +@Component({ + selector: 'internal', + template: 'fake', +}) +export class FakeComponent {} diff --git a/tests-jasmine/normal-usage-after-mock-builder/fixtures.modules.ts b/tests-jasmine/normal-usage-after-mock-builder/fixtures.modules.ts new file mode 100644 index 0000000000..dcf1791fe4 --- /dev/null +++ b/tests-jasmine/normal-usage-after-mock-builder/fixtures.modules.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; + +import { RealComponent, TargetComponent } from './fixtures.components'; +import { TargetService } from './fixtures.services'; + +@NgModule({ + declarations: [TargetComponent, RealComponent], + exports: [TargetComponent], + providers: [TargetService], +}) +export class TargetModule { + protected service: TargetService; + + constructor(service: TargetService) { + this.service = service; + this.service.call(); + } +} diff --git a/tests-jasmine/normal-usage-after-mock-builder/fixtures.services.ts b/tests-jasmine/normal-usage-after-mock-builder/fixtures.services.ts new file mode 100644 index 0000000000..1dcb2be9d6 --- /dev/null +++ b/tests-jasmine/normal-usage-after-mock-builder/fixtures.services.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class TargetService { + public called = 0; + + public call(): void { + this.called += 1; + } +} diff --git a/tests-jasmine/normal-usage-after-mock-builder/test.spec.ts b/tests-jasmine/normal-usage-after-mock-builder/test.spec.ts new file mode 100644 index 0000000000..5603426b8a --- /dev/null +++ b/tests-jasmine/normal-usage-after-mock-builder/test.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { FakeComponent, RealComponent, TargetComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('normal-usage-after-mock-builder:real1', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('renders real component because we did not use MockBuilder.replace yet', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('real1'); + }); +}); + +describe('normal-usage-after-mock-builder:mock', () => { + beforeEach(() => + TestBed.configureTestingModule( + MockBuilder().keep(TargetModule).replace(RealComponent, FakeComponent, { dependency: true }).build() + ).compileComponents() + ); + + it('renders fake component because we used MockBuilder.replace', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('fake1'); + }); +}); + +describe('normal-usage-after-mock-builder:real2', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('has to render real component after MockBuilder.replace', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('real1'); + }); +}); diff --git a/tests-jasmine/on-push/on-push.spec.ts b/tests-jasmine/on-push/on-push.spec.ts index 70130e7c3b..2ad55b5bf1 100644 --- a/tests-jasmine/on-push/on-push.spec.ts +++ b/tests-jasmine/on-push/on-push.spec.ts @@ -12,7 +12,6 @@ export class ItemListComponent { @Input() items: string[]; } -/* tslint:disable:max-classes-per-file */ @Component({ selector: 'item-list-wrapper', template: '', @@ -20,7 +19,6 @@ export class ItemListComponent { export class ItemListWrapperComponent { @Input() items: string[]; } -/* tslint:enable:max-classes-per-file */ describe('ChangeDetectionStrategy.OnPush:real', () => { let wrapper: ComponentFixture; @@ -44,21 +42,21 @@ describe('ChangeDetectionStrategy.OnPush:real', () => { }); it('should show 0 if no items', () => { - expect(component.nativeElement.innerText).toEqual('0'); + expect(component.nativeElement.innerHTML).toEqual('0'); }); it('should show 0 if items pushed to array but not changed reference', () => { wrapper.componentInstance.items.push('demo'); wrapper.detectChanges(); - expect(component.nativeElement.innerText).toEqual('0'); + expect(component.nativeElement.innerHTML).toEqual('0'); }); it('should show 1 if items array changed reference', () => { wrapper.componentInstance.items = ['demo']; wrapper.detectChanges(); - expect(component.nativeElement.innerText).toEqual('1'); + expect(component.nativeElement.innerHTML).toEqual('1'); }); }); @@ -79,20 +77,20 @@ describe('ChangeDetectionStrategy.OnPush:mock', () => { }); it('should show 0 if no items', () => { - expect(fixture.nativeElement.innerText).toEqual('0'); + expect(fixture.nativeElement.innerHTML).toContain('>0<'); }); it('should show 0 if items pushed to array but not changed reference', () => { component.items.push('demo'); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('0'); + expect(fixture.nativeElement.innerHTML).toContain('>0<'); }); it('should show 1 if items array changed reference', () => { component.items = ['demo']; fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('1'); + expect(fixture.nativeElement.innerHTML).toContain('>1<'); }); }); diff --git a/tests-jasmine/provider-with-dependency/fixtures.components.ts b/tests-jasmine/provider-with-dependency/fixtures.components.ts new file mode 100644 index 0000000000..ec2df5b5c2 --- /dev/null +++ b/tests-jasmine/provider-with-dependency/fixtures.components.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +import { ServiceChild } from './fixtures.services'; + +@Component({ + selector: 'internal-component', + template: '{{ child.parent.echo() }}', +}) +export class InternalComponent { + public readonly child: ServiceChild; + + constructor(child: ServiceChild) { + this.child = child; + } +} diff --git a/tests-jasmine/provider-with-dependency/fixtures.modules.ts b/tests-jasmine/provider-with-dependency/fixtures.modules.ts new file mode 100644 index 0000000000..ec07026ec8 --- /dev/null +++ b/tests-jasmine/provider-with-dependency/fixtures.modules.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; +import { ServiceChild, ServiceParent, ServiceReplacedParent } from './fixtures.services'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], + providers: [ + ServiceParent, + ServiceReplacedParent, + { + deps: [ServiceReplacedParent], + provide: ServiceChild, + useFactory: (parent: ServiceParent) => new ServiceChild(parent), + }, + ], +}) +export class TargetModule {} diff --git a/tests-jasmine/provider-with-dependency/fixtures.services.ts b/tests-jasmine/provider-with-dependency/fixtures.services.ts new file mode 100644 index 0000000000..15266dde1d --- /dev/null +++ b/tests-jasmine/provider-with-dependency/fixtures.services.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ServiceParent { + protected value = 'parent'; + + public echo() { + return this.value; + } +} + +@Injectable() +export class ServiceReplacedParent extends ServiceParent { + protected value = 'replaced'; +} + +@Injectable() +export class ServiceChild { + public readonly parent: ServiceParent; + + constructor(parent: ServiceParent) { + this.parent = parent; + } +} diff --git a/tests-jasmine/provider-with-dependency/test.spec.ts b/tests-jasmine/provider-with-dependency/test.spec.ts new file mode 100644 index 0000000000..7720fc7457 --- /dev/null +++ b/tests-jasmine/provider-with-dependency/test.spec.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; +import { ServiceReplacedParent } from './fixtures.services'; + +@Injectable() +class ServiceMock { + protected value = 'mock'; + + public echo() { + return this.value; + } +} + +describe('provider-with-dependency:real', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TargetModule], + }); + return TestBed.compileComponents(); + }); + + it('should render "parent"', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('replaced'); + }); +}); + +describe('provider-with-dependency:provided', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TargetModule], + providers: [ + { + provide: ServiceReplacedParent, + useClass: ServiceMock, + }, + ], + }); + return TestBed.compileComponents(); + }); + + it('should render "parent"', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('mock'); + }); +}); + +describe('provider-with-dependency:mock', () => { + beforeEach(() => { + const ngModule = MockBuilder() + .keep(TargetModule) + .provide({ + provide: ServiceReplacedParent, + useClass: ServiceMock, + }) + .build(); + TestBed.configureTestingModule(ngModule); + return TestBed.compileComponents(); + }); + + it('should render "parent" even the providers where patched', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('mock'); + }); +}); diff --git a/tests-jasmine/rerender-rendered-content-child/fixtures.components.ts b/tests-jasmine/rerender-rendered-content-child/fixtures.components.ts new file mode 100644 index 0000000000..53bac0df8a --- /dev/null +++ b/tests-jasmine/rerender-rendered-content-child/fixtures.components.ts @@ -0,0 +1,11 @@ +import { Component, ContentChild, TemplateRef } from '@angular/core'; + +import { staticFalse } from '..'; + +@Component({ + selector: 'ccc', + template: ``, +}) +export class ContentChildComponent { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; +} diff --git a/tests-jasmine/rerender-rendered-content-child/fixtures.module.ts b/tests-jasmine/rerender-rendered-content-child/fixtures.module.ts new file mode 100644 index 0000000000..df543f0a0c --- /dev/null +++ b/tests-jasmine/rerender-rendered-content-child/fixtures.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ContentChildComponent } from './fixtures.components'; + +@NgModule({ + declarations: [ContentChildComponent], + exports: [ContentChildComponent], + imports: [CommonModule], +}) +export class ContentChildModule {} diff --git a/tests-jasmine/rerender-rendered-content-child/test.spec.ts b/tests-jasmine/rerender-rendered-content-child/test.spec.ts new file mode 100644 index 0000000000..65e5783152 --- /dev/null +++ b/tests-jasmine/rerender-rendered-content-child/test.spec.ts @@ -0,0 +1,44 @@ +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockedComponent, MockRender } from 'ng-mocks'; + +import { ContentChildComponent } from './fixtures.components'; + +describe('Rerender of a rendered @ContentChild', () => { + beforeEach(async () => { + await MockBuilder().mock(ContentChildComponent, { + render: { + block: { + $implicit: '$implicit', + }, + }, + }); + }); + + it('should rerender everything correctly', () => { + const fixture = MockRender( + ` + {{ value }} {{ outside }} + `, + { + outside: '1', + } + ); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('$implicit 1'); + + fixture.componentInstance.outside = '2'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('$implicit 2'); + + const component = fixture.debugElement.query(By.directive(ContentChildComponent)) + .componentInstance as MockedComponent; + + component.__render('block', 'updated'); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('updated 2'); + + fixture.componentInstance.outside = '3'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('updated 3'); + }); +}); diff --git a/tests-jasmine/shared-mocked-module/fixtures.components.ts b/tests-jasmine/shared-mocked-module/fixtures.components.ts new file mode 100644 index 0000000000..16c311267e --- /dev/null +++ b/tests-jasmine/shared-mocked-module/fixtures.components.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-component', + template: 'real content', +}) +export class MyComponent {} + +@Component({ + selector: 'child-1-component', + template: 'child:1 ', +}) +export class Child1Component {} + +@Component({ + selector: 'child-2-component', + template: 'child:2 ', +}) +export class Child2Component {} + +@Component({ + selector: 'target-component', + template: ' - ', +}) +export class TargetComponent {} diff --git a/tests-jasmine/shared-mocked-module/fixtures.modules.ts b/tests-jasmine/shared-mocked-module/fixtures.modules.ts new file mode 100644 index 0000000000..23252afd6b --- /dev/null +++ b/tests-jasmine/shared-mocked-module/fixtures.modules.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { Child1Component, Child2Component, MyComponent, TargetComponent } from './fixtures.components'; + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], +}) +export class MyModule {} + +@NgModule({ + declarations: [Child1Component], + exports: [Child1Component], + imports: [MyModule], +}) +export class Child1Module {} + +@NgModule({ + declarations: [Child2Component], + exports: [Child2Component], + imports: [MyModule], +}) +export class Child2Module {} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + imports: [CommonModule, Child1Module, Child2Module], +}) +export class TargetModule {} diff --git a/tests-jasmine/shared-mocked-module/test.spec.ts b/tests-jasmine/shared-mocked-module/test.spec.ts new file mode 100644 index 0000000000..f58e2fd15d --- /dev/null +++ b/tests-jasmine/shared-mocked-module/test.spec.ts @@ -0,0 +1,47 @@ +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockedComponent, MockRender } from 'ng-mocks'; + +import { MyComponent, TargetComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('SharedMockedModule:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toContain( + 'child:1 real content' + ); + expect(content).toContain( + 'child:2 real content' + ); + }); +}); + +describe('SharedMockedModule:mock', () => { + beforeEach(async done => { + await MockBuilder(TargetComponent).keep(TargetModule).mock(MyComponent); + done(); + }); + + // The expectation is to verify that only MyComponent was mocked, even it was deeply nested. + it('should render', () => { + const fixture = MockRender(TargetComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + const component = fixture.debugElement.query(By.directive(MyComponent)).componentInstance as MockedComponent< + MyComponent + >; + expect(component).toBeDefined(); + expect(content).toContain('child:1 '); + expect(content).toContain('child:2 '); + }); +}); diff --git a/tests-jasmine/spies/test.spec.ts b/tests-jasmine/spies/test.spec.ts index a447d17289..134521902b 100644 --- a/tests-jasmine/spies/test.spec.ts +++ b/tests-jasmine/spies/test.spec.ts @@ -1,6 +1,5 @@ import { inject, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { MockModule, MockRender } from 'ng-mocks'; +import { MockModule, MockRender, ngMocks } from 'ng-mocks'; import { TargetComponent } from './fixtures.components'; import { TargetModule } from './fixtures.modules'; @@ -16,7 +15,7 @@ describe('spies:real', () => { it('should render', () => { const fixture = MockRender(TargetComponent); - const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + const component = ngMocks.find(fixture.debugElement, TargetComponent).componentInstance; expect(component).toBeDefined(); expect(component.echo()).toEqual('TargetComponent'); }); @@ -41,7 +40,7 @@ describe('spies:manual-mock', () => { it('should get manually mocked service', inject([TargetService], (targetService: TargetService) => { const fixture = MockRender(TargetComponent); - const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + const component = ngMocks.find(fixture.debugElement, TargetComponent).componentInstance; expect(component).toBeDefined(); expect(targetService.echo).toHaveBeenCalledTimes(1); expect(targetService.echo).toHaveBeenCalledWith('constructor'); @@ -60,11 +59,11 @@ describe('spies:auto-mock', () => { it('should get already mocked service', inject([TargetService], (targetService: TargetService) => { const fixture = MockRender(TargetComponent); - const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + const component = ngMocks.find(fixture.debugElement, TargetComponent).componentInstance; expect(component).toBeDefined(); expect(targetService.echo).toHaveBeenCalledTimes(1); expect(targetService.echo).toHaveBeenCalledWith('constructor'); - (targetService.echo as jasmine.Spy).and.returnValue('faked'); + ngMocks.stub(targetService, 'echo').and.returnValue('faked'); expect(component.echo()).toEqual('faked'); expect(targetService.echo).toHaveBeenCalledTimes(2); })); diff --git a/tests-jasmine/structural-directives/structural-directives.spec.ts b/tests-jasmine/structural-directives/structural-directives.spec.ts index b1b2adf5a5..4e6e1d75bf 100644 --- a/tests-jasmine/structural-directives/structural-directives.spec.ts +++ b/tests-jasmine/structural-directives/structural-directives.spec.ts @@ -105,7 +105,7 @@ describe('structural-directive-as-ng-for:mock', () => { fixture.detectChanges(); // By default mocked structural directives are rendered with undefined variables. - expect(fixture.nativeElement.innerText).toEqual('$implicit: fromDirective:'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' $implicit: fromDirective: '); // Extracting mock. const debugElement = fixture.debugElement.query(By.css('div')); @@ -123,14 +123,14 @@ describe('structural-directive-as-ng-for:mock', () => { fromDirective: false, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('$implicit:true fromDirective:false'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' $implicit:true fromDirective:false '); // And we want dynamically change variables for render. directive.__render(false, { fromDirective: true, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('$implicit:false fromDirective:true'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' $implicit:false fromDirective:true '); }); it('mocks CustomNgForWithOfDirective properly', () => { @@ -154,7 +154,7 @@ describe('structural-directive-as-ng-for:mock', () => { fixture.detectChanges(); // By default mocked structural directives are rendered with undefined variables. - expect(fixture.nativeElement.innerText).toEqual('w/ 00'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/ 00 '); const debugElement = fixture.debugElement.query(By.css('div')); expect(debugElement).toBeTruthy(); @@ -176,7 +176,7 @@ describe('structural-directive-as-ng-for:mock', () => { myLast: false, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('w/ MainValueMyIndex10'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/ MainValueMyIndex10 '); // And we want dynamically change variables for render. directive.__render('MainValue2', { @@ -185,7 +185,7 @@ describe('structural-directive-as-ng-for:mock', () => { myLast: true, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('w/ MainValue2MyIndex201'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/ MainValue2MyIndex201 '); }); it('mocks customNgForWithoutOf properly', () => { @@ -209,7 +209,7 @@ describe('structural-directive-as-ng-for:mock', () => { fixture.detectChanges(); // By default mocked structural directives are rendered with undefined variables. - expect(fixture.nativeElement.innerText).toEqual('w/o 00'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/o 00 '); const debugElement = fixture.debugElement.query(By.css('div')); expect(debugElement).toBeTruthy(); @@ -231,7 +231,7 @@ describe('structural-directive-as-ng-for:mock', () => { myLast: false, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('w/o MainValueMyIndex10'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/o MainValueMyIndex10 '); // And we want dynamically change variables for render. directive.__render('MainValue2', { @@ -240,7 +240,7 @@ describe('structural-directive-as-ng-for:mock', () => { myLast: true, }); fixture.detectChanges(); - expect(fixture.nativeElement.innerText).toEqual('w/o MainValue2MyIndex201'); + expect(fixture.nativeElement.innerHTML.replace(/\s+/gm, ' ')).toContain(' w/o MainValue2MyIndex201 '); }); it('searches for related directive', () => { diff --git a/tests-jest/context-with-directives/context-with-directives.spec.ts b/tests-jest/context-with-directives/context-with-directives.spec.ts index 4ce06ec042..c79db7edcb 100644 --- a/tests-jest/context-with-directives/context-with-directives.spec.ts +++ b/tests-jest/context-with-directives/context-with-directives.spec.ts @@ -15,7 +15,7 @@ describe('context-with-directives:real', () => { .then(done); }); - it('renders everything right', async () => { + it('renders everything right', () => { const fixture = MockRender(`
header
diff --git a/tests-jest/control-value-accessor-form-control/fixtures.ts b/tests-jest/control-value-accessor-form-control/fixtures.ts new file mode 100644 index 0000000000..5bf4d5500f --- /dev/null +++ b/tests-jest/control-value-accessor-form-control/fixtures.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component, forwardRef, NgModule } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'target', + template: '', +}) +export class TargetComponent { + public readonly control = new FormControl(); +} + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ControlComponent), + }, + ], + selector: 'control', + template: '', +}) +export class ControlComponent implements ControlValueAccessor { + public isDisabled = false; + public value: any; + public change: any = () => undefined; + + changeTouch(): void { + this.touch(); + } + + changeValue(obj: any): void { + this.change(obj); + } + + registerOnChange(fn: any): void { + this.change = fn; + } + + registerOnTouched(fn: any): void { + this.touch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public touch: any = () => undefined; + + writeValue(obj: any): void { + this.value = obj; + } +} + +@NgModule({ + declarations: [TargetComponent, ControlComponent], + exports: [TargetComponent], + imports: [CommonModule, ReactiveFormsModule], +}) +export class TargetModule {} diff --git a/tests-jest/control-value-accessor-form-control/test.spec.ts b/tests-jest/control-value-accessor-form-control/test.spec.ts new file mode 100644 index 0000000000..562ffc7c30 --- /dev/null +++ b/tests-jest/control-value-accessor-form-control/test.spec.ts @@ -0,0 +1,107 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { MockBuilder, MockComponent, MockHelper, MockRender } from 'ng-mocks'; + +import { ControlComponent, TargetComponent, TargetModule } from './fixtures'; + +// a real case to check possible behavior. +describe('control-value-accessor-form-control:real', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('respects our formControl', () => { + const fixture = MockRender(TargetComponent, {}, false); + const mock = MockHelper.findOrFail(fixture.debugElement, ControlComponent).componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + fixture.detectChanges(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.control.setValue('test1'); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + fixture.point.componentInstance.control.setValue('test2'); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that touch works + mock.changeTouch(); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + + // checking that reset works + fixture.point.componentInstance.control.markAsUntouched(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that disabled works + fixture.point.componentInstance.control.disable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.control.enable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.changeValue('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test3'); + }); +}); + +// a way that ensures that a mocked component behaves the same way as real one. +describe('control-value-accessor-form-control:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(ReactiveFormsModule)); + + it('respects our formControl', () => { + const fixture = MockRender(TargetComponent, {}, false); + const mock = MockHelper.findOrFail(fixture.debugElement, MockComponent(ControlComponent)).componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + spyOn(mock, 'registerOnChange').and.callThrough(); + spyOn(mock, 'registerOnTouched').and.callThrough(); + fixture.detectChanges(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.control.setValue('test1'); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + fixture.point.componentInstance.control.setValue('test2'); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + + // checking that touch works + mock.__simulateTouch(); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + fixture.point.componentInstance.control.markAsUntouched(); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + // a way through a spy + MockHelper.mockService(mock, 'registerOnTouched').calls.first().args[0](); + expect(fixture.point.componentInstance.control.touched).toBeTruthy(); + fixture.point.componentInstance.control.markAsUntouched(); + + // checking that disabled works + fixture.point.componentInstance.control.disable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.control.enable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.__simulateChange('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test3'); + // a way through a spy + MockHelper.mockService(mock, 'registerOnChange').calls.first().args[0]('test4'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test4'); + expect(fixture.point.componentInstance.control.touched).toBeFalsy(); + expect(fixture.point.componentInstance.control.value).toBe('test4'); + }); +}); diff --git a/tests-jest/control-value-accessor-ng-model/fixtures.ts b/tests-jest/control-value-accessor-ng-model/fixtures.ts new file mode 100644 index 0000000000..c423dfcbe3 --- /dev/null +++ b/tests-jest/control-value-accessor-ng-model/fixtures.ts @@ -0,0 +1,70 @@ +import { CommonModule } from '@angular/common'; +import { Component, forwardRef, NgModule } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'target', + template: '', +}) +export class TargetComponent { + public disabled = false; + public realValue: null | string = null; + + public get value(): null | string { + return this.realValue; + } + + public set value(value: null | string) { + this.realValue = value; + } +} + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ControlComponent), + }, + ], + selector: 'control', + template: '', +}) +export class ControlComponent implements ControlValueAccessor { + public isDisabled = false; + public value: any; + public change: any = () => undefined; + + changeTouch(): void { + this.touch(); + } + + changeValue(obj: any): void { + this.change(obj); + } + + registerOnChange(fn: any): void { + this.change = fn; + } + + registerOnTouched(fn: any): void { + this.touch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public touch: any = () => undefined; + + writeValue(obj: any): void { + this.value = obj; + } +} + +@NgModule({ + declarations: [TargetComponent, ControlComponent], + exports: [TargetComponent], + imports: [CommonModule, FormsModule], +}) +export class TargetModule {} diff --git a/tests-jest/control-value-accessor-ng-model/test.spec.ts b/tests-jest/control-value-accessor-ng-model/test.spec.ts new file mode 100644 index 0000000000..0817a7e3b9 --- /dev/null +++ b/tests-jest/control-value-accessor-ng-model/test.spec.ts @@ -0,0 +1,129 @@ +import { FormsModule, NgModel } from '@angular/forms'; +import { MockBuilder, MockComponent, MockHelper, MockRender } from 'ng-mocks'; + +import { ControlComponent, TargetComponent, TargetModule } from './fixtures'; + +// a real case to check possible behavior. +describe('control-value-accessor-ng-model:real', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('respects our ngModel', async () => { + const fixture = MockRender(TargetComponent, {}, false); + const mockElement = MockHelper.findOrFail(fixture.debugElement, ControlComponent); + const mock = mockElement.componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + const ngModel = MockHelper.getDirectiveOrFail(mockElement, NgModel); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(ngModel.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.value = 'test1'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(ngModel.touched).toBeFalsy(); + + fixture.point.componentInstance.value = 'test2'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(ngModel.touched).toBeFalsy(); + + // checking that touch works + mock.changeTouch(); + expect(ngModel.touched).toBeTruthy(); + + // checking that reset works + ngModel.control.markAsUntouched(); + expect(ngModel.touched).toBeFalsy(); + + // checking that disabled works + fixture.point.componentInstance.disabled = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.disabled = false; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.changeValue('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(ngModel.touched).toBeFalsy(); + expect(fixture.point.componentInstance.value).toBe('test3'); + }); +}); + +// a way that ensures that a mocked component behaves the same way as real one. +describe('control-value-accessor-ng-model:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(FormsModule)); + + it('respects our ngModel', async () => { + const fixture = MockRender(TargetComponent, {}, false); + const mockElement = MockHelper.findOrFail(fixture.debugElement, MockComponent(ControlComponent)); + const mock = mockElement.componentInstance; + spyOn(mock, 'writeValue').and.callThrough(); + spyOn(mock, 'setDisabledState').and.callThrough(); + spyOn(mock, 'registerOnChange').and.callThrough(); + spyOn(mock, 'registerOnTouched').and.callThrough(); + const ngModel = MockHelper.getDirectiveOrFail(mockElement, NgModel); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mock.writeValue).toHaveBeenCalledWith(null); + expect(mock.setDisabledState).not.toHaveBeenCalled(); + expect(ngModel.touched).toBeFalsy(); + + // checking via original component + fixture.point.componentInstance.value = 'test1'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test1'); + expect(ngModel.touched).toBeFalsy(); + + fixture.point.componentInstance.value = 'test2'; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.writeValue).toHaveBeenCalledWith('test2'); + expect(ngModel.touched).toBeFalsy(); + + // checking that touch works + mock.__simulateTouch(); + expect(ngModel.touched).toBeTruthy(); + ngModel.control.markAsUntouched(); + expect(ngModel.touched).toBeFalsy(); + // a way through a spy + MockHelper.mockService(mock, 'registerOnTouched').calls.first().args[0](); + expect(ngModel.touched).toBeTruthy(); + ngModel.control.markAsUntouched(); + + // checking that disabled works + fixture.point.componentInstance.disabled = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(true); + fixture.point.componentInstance.disabled = false; + fixture.detectChanges(); + await fixture.whenStable(); + expect(mock.setDisabledState).toHaveBeenCalledWith(false); + + // changeValue doesn't trigger anything else but the callback. Therefore it doesn't render new value. + // It only updates the original control's value. + mock.__simulateChange('test3'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test3'); + expect(ngModel.touched).toBeFalsy(); + expect(fixture.point.componentInstance.value).toBe('test3'); + // a way through a spy + MockHelper.mockService(mock, 'registerOnChange').calls.first().args[0]('test4'); + expect(mock.writeValue).not.toHaveBeenCalledWith('test4'); + expect(ngModel.touched).toBeFalsy(); + expect(ngModel.value).toBe('test4'); + }); +}); diff --git a/tests-jest/exports-only/fixtures.components.ts b/tests-jest/exports-only/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jest/exports-only/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jest/exports-only/fixtures.modules.ts b/tests-jest/exports-only/fixtures.modules.ts new file mode 100644 index 0000000000..95e2906ecc --- /dev/null +++ b/tests-jest/exports-only/fixtures.modules.ts @@ -0,0 +1,16 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], + imports: [CommonModule], +}) +export class InternalModule {} + +@NgModule({ + exports: [InternalModule], +}) +export class TargetModule {} diff --git a/tests-jest/exports-only/test.spec.ts b/tests-jest/exports-only/test.spec.ts new file mode 100644 index 0000000000..cbe0a3d431 --- /dev/null +++ b/tests-jest/exports-only/test.spec.ts @@ -0,0 +1,64 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('ExportsOnly:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('internal'); + }); +}); + +describe('ExportsOnly:mock1', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); + +describe('ExportsOnly:mock2', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); + +describe('ExportsOnly:mock3', () => { + beforeEach(async done => { + await MockBuilder().keep(TargetModule); + done(); + }); + + // The expectation is to see that InternalModule was exported and it can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('internal'); + }); +}); diff --git a/tests-jest/injected-ng-templates/custom-injection.component.ts b/tests-jest/injected-ng-templates/custom-injection.component.ts index c050f2eb8e..453f894806 100644 --- a/tests-jest/injected-ng-templates/custom-injection.component.ts +++ b/tests-jest/injected-ng-templates/custom-injection.component.ts @@ -1,6 +1,6 @@ import { Component, ContentChild, Input, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; -import { staticFalse } from '../index'; +import { staticFalse } from '..'; @Component({ selector: 'custom-injection', diff --git a/tests-jest/internal-only-nested/fixtures.components.ts b/tests-jest/internal-only-nested/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jest/internal-only-nested/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jest/internal-only-nested/fixtures.modules.ts b/tests-jest/internal-only-nested/fixtures.modules.ts new file mode 100644 index 0000000000..643509a790 --- /dev/null +++ b/tests-jest/internal-only-nested/fixtures.modules.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + imports: [CommonModule], +}) +export class Nested1Module {} + +@NgModule({ + imports: [Nested1Module], +}) +export class Nested2Module {} + +@NgModule({ + imports: [Nested1Module], +}) +export class Nested3Module {} + +@NgModule({ + imports: [Nested2Module, Nested3Module], +}) +export class TargetModule {} diff --git a/tests-jest/internal-only-nested/test.spec.ts b/tests-jest/internal-only-nested/test.spec.ts new file mode 100644 index 0000000000..304eecd838 --- /dev/null +++ b/tests-jest/internal-only-nested/test.spec.ts @@ -0,0 +1,37 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalOnlyNested:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalOnlyNested:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent, { export: true }); + done(); + }); + + // The expectation is to see that InternalComponent was exported to the level of the TestingModule + // and can be accessed in the test even it was deeply nested. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); diff --git a/tests-jest/internal-only/fixtures.components.ts b/tests-jest/internal-only/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jest/internal-only/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jest/internal-only/fixtures.modules.ts b/tests-jest/internal-only/fixtures.modules.ts new file mode 100644 index 0000000000..b9d4de82e8 --- /dev/null +++ b/tests-jest/internal-only/fixtures.modules.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + imports: [CommonModule], +}) +export class TargetModule {} diff --git a/tests-jest/internal-only/test.spec.ts b/tests-jest/internal-only/test.spec.ts new file mode 100644 index 0000000000..42d5e9332d --- /dev/null +++ b/tests-jest/internal-only/test.spec.ts @@ -0,0 +1,36 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalOnly:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalOnly:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule).mock(InternalComponent, { export: true }); + done(); + }); + + // The expectation is to see that InternalComponent was exported and can be accessed from the test. + it('should render', () => { + const fixture = MockRender(InternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + }); +}); diff --git a/tests-jest/internal-vs-external/fixtures.components.ts b/tests-jest/internal-vs-external/fixtures.components.ts new file mode 100644 index 0000000000..bc86a0d881 --- /dev/null +++ b/tests-jest/internal-vs-external/fixtures.components.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} + +@Component({ + selector: 'external-component', + template: 'external ', +}) +export class ExternalComponent {} diff --git a/tests-jest/internal-vs-external/fixtures.modules.ts b/tests-jest/internal-vs-external/fixtures.modules.ts new file mode 100644 index 0000000000..f7c1d56d56 --- /dev/null +++ b/tests-jest/internal-vs-external/fixtures.modules.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ExternalComponent, InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent, ExternalComponent], + exports: [ExternalComponent], + imports: [CommonModule], +}) +export class TargetModule {} diff --git a/tests-jest/internal-vs-external/test.spec.ts b/tests-jest/internal-vs-external/test.spec.ts new file mode 100644 index 0000000000..fc2a47279a --- /dev/null +++ b/tests-jest/internal-vs-external/test.spec.ts @@ -0,0 +1,70 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockModule, MockRender } from 'ng-mocks'; + +import { ExternalComponent, InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('InternalVsExternal:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toContain('external internal'); + + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalVsExternal:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + // The expectation is to see that ExternalComponent was exported and InternalComponent wasn't. + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + + expect(() => { + MockRender(InternalComponent); + TestBed.get(InternalComponent); // Thanks Ivy True, it doesn't throw an error and we have to use injector. + }).toThrowError(); + }); +}); + +describe('InternalVsExternal:legacy', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [MockModule(TargetModule)], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(ExternalComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toEqual(''); + + // the code below will fail because the MockModule outside of the MockBuilder exports everything. + // try { + // MockRender(InternalComponent); + // fail('should fail on the internal component'); + // } catch (e) { + // expect(e).toEqual(jasmine.objectContaining({ngSyntaxError: true})); + // } + }); +}); diff --git a/tests-jest/mock-builder-by-directive/fixtures.components.ts b/tests-jest/mock-builder-by-directive/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jest/mock-builder-by-directive/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jest/mock-builder-by-directive/fixtures.modules.ts b/tests-jest/mock-builder-by-directive/fixtures.modules.ts new file mode 100644 index 0000000000..430a37d058 --- /dev/null +++ b/tests-jest/mock-builder-by-directive/fixtures.modules.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], +}) +export class TargetModule {} diff --git a/tests-jest/mock-builder-by-directive/test.spec.ts b/tests-jest/mock-builder-by-directive/test.spec.ts new file mode 100644 index 0000000000..a0e5e6c877 --- /dev/null +++ b/tests-jest/mock-builder-by-directive/test.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockComponent, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('MockBuilderByDirective:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(InternalComponent)); + expect(element).toBeDefined(); + }); +}); + +describe('MockBuilderByDirective:mock', () => { + beforeEach(async done => { + await MockBuilder().mock(TargetModule); + done(); + }); + + it('should find mock', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(MockComponent(InternalComponent))); + expect(element).toBeDefined(); + }); + + it('should find original', () => { + const fixture = MockRender(InternalComponent); + const element = fixture.debugElement.query(By.directive(InternalComponent)); + expect(element).toBeDefined(); + }); +}); diff --git a/tests-jest/mock-builder-keeps-application-module/fixtures.components.ts b/tests-jest/mock-builder-keeps-application-module/fixtures.components.ts new file mode 100644 index 0000000000..d23ac6421a --- /dev/null +++ b/tests-jest/mock-builder-keeps-application-module/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'target-component', + template: 'target', +}) +export class TargetComponent {} diff --git a/tests-jest/mock-builder-keeps-application-module/fixtures.modules.ts b/tests-jest/mock-builder-keeps-application-module/fixtures.modules.ts new file mode 100644 index 0000000000..dd7e1ea0d1 --- /dev/null +++ b/tests-jest/mock-builder-keeps-application-module/fixtures.modules.ts @@ -0,0 +1,26 @@ +import { APP_ID, APP_INITIALIZER, InjectionToken, NgModule } from '@angular/core'; + +import { TargetComponent } from './fixtures.components'; + +export const TARGET_TOKEN = new InjectionToken('TARGET_TOKEN'); + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + providers: [ + { + provide: TARGET_TOKEN, + useValue: 'TARGET_TOKEN', + }, + { + provide: APP_ID, + useValue: 'random', + }, + { + multi: true, + provide: APP_INITIALIZER, + useValue: () => undefined, + }, + ], +}) +export class TargetModule {} diff --git a/tests-jest/mock-builder-keeps-application-module/test.spec.ts b/tests-jest/mock-builder-keeps-application-module/test.spec.ts new file mode 100644 index 0000000000..b79c18fdf3 --- /dev/null +++ b/tests-jest/mock-builder-keeps-application-module/test.spec.ts @@ -0,0 +1,41 @@ +import { APP_ID, APP_INITIALIZER, VERSION } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { TargetComponent } from './fixtures.components'; +import { TARGET_TOKEN, TargetModule } from './fixtures.modules'; + +describe('MockBuilderKeepsApplicationModule:real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + const element = fixture.debugElement.query(By.directive(TargetComponent)); + expect(element).toBeDefined(); + expect(TestBed.get(TARGET_TOKEN)).toBeDefined(); + expect(TestBed.get(APP_INITIALIZER)).toBeDefined(); + expect(TestBed.get(APP_ID)).toBeDefined(); + }); +}); + +describe('MockBuilderKeepsApplicationModule:mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + const element = fixture.debugElement.query(By.directive(TargetComponent)); + expect(element).toBeDefined(); + expect(() => TestBed.get(TARGET_TOKEN)).toThrow(); + if (VERSION.major !== '9') { + // somehow ivy doesn't provide APP_INITIALIZER out of the box and this assertion fails. + // our mock logic skips all multi tokens therefore this one isn't present anymore. + expect(TestBed.get(APP_INITIALIZER)).toBeDefined(); + } + expect(TestBed.get(APP_ID)).toBeDefined(); + }); +}); diff --git a/tests-jest/module-with-factory-tokens/fixtures.ts b/tests-jest/module-with-factory-tokens/fixtures.ts new file mode 100644 index 0000000000..3ad0365843 --- /dev/null +++ b/tests-jest/module-with-factory-tokens/fixtures.ts @@ -0,0 +1,28 @@ +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 {} diff --git a/tests-jest/module-with-factory-tokens/test.spec.ts b/tests-jest/module-with-factory-tokens/test.spec.ts new file mode 100644 index 0000000000..995bc2e767 --- /dev/null +++ b/tests-jest/module-with-factory-tokens/test.spec.ts @@ -0,0 +1,111 @@ +import { VERSION } from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).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.innerHTML.replace(/\s+/gm, ' ')).toContain('"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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML).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', () => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + return; + } + + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toEqual( + '"MY_TOKEN_SINGLE" "MY_TOKEN_MULTI"' + ); + }); +}); diff --git a/tests-jest/module-with-tokens/fixtures.ts b/tests-jest/module-with-tokens/fixtures.ts new file mode 100644 index 0000000000..e39123fde0 --- /dev/null +++ b/tests-jest/module-with-tokens/fixtures.ts @@ -0,0 +1,40 @@ +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 {} diff --git a/tests-jest/module-with-tokens/test.spec.ts b/tests-jest/module-with-tokens/test.spec.ts new file mode 100644 index 0000000000..22c0984e5c --- /dev/null +++ b/tests-jest/module-with-tokens/test.spec.ts @@ -0,0 +1,97 @@ +import { MockBuilder, MockRender } from 'ng-mocks'; + +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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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.innerHTML.replace(/\s+/gm, ' ')).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/); + }); +}); diff --git a/tests-jest/nested-before-each/fixtures.components.ts b/tests-jest/nested-before-each/fixtures.components.ts new file mode 100644 index 0000000000..9fd4b87b11 --- /dev/null +++ b/tests-jest/nested-before-each/fixtures.components.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'internal-component', + template: 'internal', +}) +export class InternalComponent {} diff --git a/tests-jest/nested-before-each/fixtures.modules.ts b/tests-jest/nested-before-each/fixtures.modules.ts new file mode 100644 index 0000000000..430a37d058 --- /dev/null +++ b/tests-jest/nested-before-each/fixtures.modules.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], +}) +export class TargetModule {} diff --git a/tests-jest/nested-before-each/test.spec.ts b/tests-jest/nested-before-each/test.spec.ts new file mode 100644 index 0000000000..1aa18a7b6a --- /dev/null +++ b/tests-jest/nested-before-each/test.spec.ts @@ -0,0 +1,78 @@ +import { Type } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; + +describe('nested-before-each', () => { + let level = 0; + let mock: Type; + + beforeEach(() => { + level = 0; + mock = MockComponent(InternalComponent); + TestBed.configureTestingModule({ + declarations: [mock], + }); + return TestBed.compileComponents(); + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + it('should have the same mock after the first run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + + it('should have the same mock after the second run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + }); + }); +}); + +describe('nested-before-all', () => { + let level = 0; + let mock: Type; + + beforeAll(() => { + level = 0; + mock = MockComponent(InternalComponent); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + declarations: [mock], + }); + return TestBed.compileComponents(); + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + describe('tested', () => { + beforeEach(() => { + level += 1; + }); + + it('should have the same mock after the first run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + + it('should have the same mock after the second run', () => { + expect(level).toBeGreaterThan(0); + expect(MockComponent(InternalComponent)).toBe(mock); + }); + }); + }); +}); diff --git a/tests-jest/normal-usage-after-mock-builder/fixtures.components.ts b/tests-jest/normal-usage-after-mock-builder/fixtures.components.ts new file mode 100644 index 0000000000..78929f1109 --- /dev/null +++ b/tests-jest/normal-usage-after-mock-builder/fixtures.components.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TargetService } from './fixtures.services'; + +@Component({ + selector: 'root', + template: '{{ service.called }}', +}) +export class TargetComponent { + public readonly service: TargetService; + + constructor(service: TargetService) { + this.service = service; + } +} + +@Component({ + selector: 'internal', + template: 'real', +}) +export class RealComponent {} + +@Component({ + selector: 'internal', + template: 'fake', +}) +export class FakeComponent {} diff --git a/tests-jest/normal-usage-after-mock-builder/fixtures.modules.ts b/tests-jest/normal-usage-after-mock-builder/fixtures.modules.ts new file mode 100644 index 0000000000..dcf1791fe4 --- /dev/null +++ b/tests-jest/normal-usage-after-mock-builder/fixtures.modules.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; + +import { RealComponent, TargetComponent } from './fixtures.components'; +import { TargetService } from './fixtures.services'; + +@NgModule({ + declarations: [TargetComponent, RealComponent], + exports: [TargetComponent], + providers: [TargetService], +}) +export class TargetModule { + protected service: TargetService; + + constructor(service: TargetService) { + this.service = service; + this.service.call(); + } +} diff --git a/tests-jest/normal-usage-after-mock-builder/fixtures.services.ts b/tests-jest/normal-usage-after-mock-builder/fixtures.services.ts new file mode 100644 index 0000000000..1dcb2be9d6 --- /dev/null +++ b/tests-jest/normal-usage-after-mock-builder/fixtures.services.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class TargetService { + public called = 0; + + public call(): void { + this.called += 1; + } +} diff --git a/tests-jest/normal-usage-after-mock-builder/test.spec.ts b/tests-jest/normal-usage-after-mock-builder/test.spec.ts new file mode 100644 index 0000000000..5603426b8a --- /dev/null +++ b/tests-jest/normal-usage-after-mock-builder/test.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { FakeComponent, RealComponent, TargetComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('normal-usage-after-mock-builder:real1', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('renders real component because we did not use MockBuilder.replace yet', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('real1'); + }); +}); + +describe('normal-usage-after-mock-builder:mock', () => { + beforeEach(() => + TestBed.configureTestingModule( + MockBuilder().keep(TargetModule).replace(RealComponent, FakeComponent, { dependency: true }).build() + ).compileComponents() + ); + + it('renders fake component because we used MockBuilder.replace', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('fake1'); + }); +}); + +describe('normal-usage-after-mock-builder:real2', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('has to render real component after MockBuilder.replace', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('real1'); + }); +}); diff --git a/tests-jest/on-push/on-push.spec.ts b/tests-jest/on-push/on-push.spec.ts index 1a49600c61..2ad55b5bf1 100644 --- a/tests-jest/on-push/on-push.spec.ts +++ b/tests-jest/on-push/on-push.spec.ts @@ -12,7 +12,6 @@ export class ItemListComponent { @Input() items: string[]; } -/* tslint:disable:max-classes-per-file */ @Component({ selector: 'item-list-wrapper', template: '', @@ -20,7 +19,6 @@ export class ItemListComponent { export class ItemListWrapperComponent { @Input() items: string[]; } -/* tslint:enable:max-classes-per-file */ describe('ChangeDetectionStrategy.OnPush:real', () => { let wrapper: ComponentFixture; diff --git a/tests-jest/provider-with-dependency/fixtures.components.ts b/tests-jest/provider-with-dependency/fixtures.components.ts new file mode 100644 index 0000000000..ec2df5b5c2 --- /dev/null +++ b/tests-jest/provider-with-dependency/fixtures.components.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +import { ServiceChild } from './fixtures.services'; + +@Component({ + selector: 'internal-component', + template: '{{ child.parent.echo() }}', +}) +export class InternalComponent { + public readonly child: ServiceChild; + + constructor(child: ServiceChild) { + this.child = child; + } +} diff --git a/tests-jest/provider-with-dependency/fixtures.modules.ts b/tests-jest/provider-with-dependency/fixtures.modules.ts new file mode 100644 index 0000000000..ec07026ec8 --- /dev/null +++ b/tests-jest/provider-with-dependency/fixtures.modules.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; + +import { InternalComponent } from './fixtures.components'; +import { ServiceChild, ServiceParent, ServiceReplacedParent } from './fixtures.services'; + +@NgModule({ + declarations: [InternalComponent], + exports: [InternalComponent], + providers: [ + ServiceParent, + ServiceReplacedParent, + { + deps: [ServiceReplacedParent], + provide: ServiceChild, + useFactory: (parent: ServiceParent) => new ServiceChild(parent), + }, + ], +}) +export class TargetModule {} diff --git a/tests-jest/provider-with-dependency/fixtures.services.ts b/tests-jest/provider-with-dependency/fixtures.services.ts new file mode 100644 index 0000000000..15266dde1d --- /dev/null +++ b/tests-jest/provider-with-dependency/fixtures.services.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ServiceParent { + protected value = 'parent'; + + public echo() { + return this.value; + } +} + +@Injectable() +export class ServiceReplacedParent extends ServiceParent { + protected value = 'replaced'; +} + +@Injectable() +export class ServiceChild { + public readonly parent: ServiceParent; + + constructor(parent: ServiceParent) { + this.parent = parent; + } +} diff --git a/tests-jest/provider-with-dependency/test.spec.ts b/tests-jest/provider-with-dependency/test.spec.ts new file mode 100644 index 0000000000..7720fc7457 --- /dev/null +++ b/tests-jest/provider-with-dependency/test.spec.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +import { InternalComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; +import { ServiceReplacedParent } from './fixtures.services'; + +@Injectable() +class ServiceMock { + protected value = 'mock'; + + public echo() { + return this.value; + } +} + +describe('provider-with-dependency:real', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TargetModule], + }); + return TestBed.compileComponents(); + }); + + it('should render "parent"', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('replaced'); + }); +}); + +describe('provider-with-dependency:provided', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TargetModule], + providers: [ + { + provide: ServiceReplacedParent, + useClass: ServiceMock, + }, + ], + }); + return TestBed.compileComponents(); + }); + + it('should render "parent"', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('mock'); + }); +}); + +describe('provider-with-dependency:mock', () => { + beforeEach(() => { + const ngModule = MockBuilder() + .keep(TargetModule) + .provide({ + provide: ServiceReplacedParent, + useClass: ServiceMock, + }) + .build(); + TestBed.configureTestingModule(ngModule); + return TestBed.compileComponents(); + }); + + it('should render "parent" even the providers where patched', () => { + const fixture = MockRender(InternalComponent); + expect(fixture.debugElement.nativeElement.innerHTML).toEqual('mock'); + }); +}); diff --git a/tests-jest/rerender-rendered-content-child/fixtures.components.ts b/tests-jest/rerender-rendered-content-child/fixtures.components.ts new file mode 100644 index 0000000000..53bac0df8a --- /dev/null +++ b/tests-jest/rerender-rendered-content-child/fixtures.components.ts @@ -0,0 +1,11 @@ +import { Component, ContentChild, TemplateRef } from '@angular/core'; + +import { staticFalse } from '..'; + +@Component({ + selector: 'ccc', + template: ``, +}) +export class ContentChildComponent { + @ContentChild('block', { ...staticFalse }) injectedBlock: TemplateRef; +} diff --git a/tests-jest/rerender-rendered-content-child/fixtures.module.ts b/tests-jest/rerender-rendered-content-child/fixtures.module.ts new file mode 100644 index 0000000000..df543f0a0c --- /dev/null +++ b/tests-jest/rerender-rendered-content-child/fixtures.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ContentChildComponent } from './fixtures.components'; + +@NgModule({ + declarations: [ContentChildComponent], + exports: [ContentChildComponent], + imports: [CommonModule], +}) +export class ContentChildModule {} diff --git a/tests-jest/rerender-rendered-content-child/test.spec.ts b/tests-jest/rerender-rendered-content-child/test.spec.ts new file mode 100644 index 0000000000..65e5783152 --- /dev/null +++ b/tests-jest/rerender-rendered-content-child/test.spec.ts @@ -0,0 +1,44 @@ +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockedComponent, MockRender } from 'ng-mocks'; + +import { ContentChildComponent } from './fixtures.components'; + +describe('Rerender of a rendered @ContentChild', () => { + beforeEach(async () => { + await MockBuilder().mock(ContentChildComponent, { + render: { + block: { + $implicit: '$implicit', + }, + }, + }); + }); + + it('should rerender everything correctly', () => { + const fixture = MockRender( + ` + {{ value }} {{ outside }} + `, + { + outside: '1', + } + ); + expect(fixture).toBeDefined(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('$implicit 1'); + + fixture.componentInstance.outside = '2'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('$implicit 2'); + + const component = fixture.debugElement.query(By.directive(ContentChildComponent)) + .componentInstance as MockedComponent; + + component.__render('block', 'updated'); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('updated 2'); + + fixture.componentInstance.outside = '3'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML).toContain('updated 3'); + }); +}); diff --git a/tests-jest/shared-mocked-module/fixtures.components.ts b/tests-jest/shared-mocked-module/fixtures.components.ts new file mode 100644 index 0000000000..16c311267e --- /dev/null +++ b/tests-jest/shared-mocked-module/fixtures.components.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-component', + template: 'real content', +}) +export class MyComponent {} + +@Component({ + selector: 'child-1-component', + template: 'child:1 ', +}) +export class Child1Component {} + +@Component({ + selector: 'child-2-component', + template: 'child:2 ', +}) +export class Child2Component {} + +@Component({ + selector: 'target-component', + template: ' - ', +}) +export class TargetComponent {} diff --git a/tests-jest/shared-mocked-module/fixtures.modules.ts b/tests-jest/shared-mocked-module/fixtures.modules.ts new file mode 100644 index 0000000000..23252afd6b --- /dev/null +++ b/tests-jest/shared-mocked-module/fixtures.modules.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { Child1Component, Child2Component, MyComponent, TargetComponent } from './fixtures.components'; + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], +}) +export class MyModule {} + +@NgModule({ + declarations: [Child1Component], + exports: [Child1Component], + imports: [MyModule], +}) +export class Child1Module {} + +@NgModule({ + declarations: [Child2Component], + exports: [Child2Component], + imports: [MyModule], +}) +export class Child2Module {} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + imports: [CommonModule, Child1Module, Child2Module], +}) +export class TargetModule {} diff --git a/tests-jest/shared-mocked-module/test.spec.ts b/tests-jest/shared-mocked-module/test.spec.ts new file mode 100644 index 0000000000..f58e2fd15d --- /dev/null +++ b/tests-jest/shared-mocked-module/test.spec.ts @@ -0,0 +1,47 @@ +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockBuilder, MockedComponent, MockRender } from 'ng-mocks'; + +import { MyComponent, TargetComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; + +describe('SharedMockedModule:real', () => { + beforeEach(async done => { + await TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(); + done(); + }); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + expect(content).toContain( + 'child:1 real content' + ); + expect(content).toContain( + 'child:2 real content' + ); + }); +}); + +describe('SharedMockedModule:mock', () => { + beforeEach(async done => { + await MockBuilder(TargetComponent).keep(TargetModule).mock(MyComponent); + done(); + }); + + // The expectation is to verify that only MyComponent was mocked, even it was deeply nested. + it('should render', () => { + const fixture = MockRender(TargetComponent); + expect(fixture).toBeDefined(); + const content = fixture.debugElement.nativeElement.innerHTML; + const component = fixture.debugElement.query(By.directive(MyComponent)).componentInstance as MockedComponent< + MyComponent + >; + expect(component).toBeDefined(); + expect(content).toContain('child:1 '); + expect(content).toContain('child:2 '); + }); +}); diff --git a/tslint.json b/tslint.json index 79b353cfed..818a4e36ca 100644 --- a/tslint.json +++ b/tslint.json @@ -5,6 +5,7 @@ "arrow-parens": false, "comment-format": false, "completed-docs": false, + "deprecation": false, "file-name-casing": false, "max-classes-per-file": false, "member-access": false, @@ -16,6 +17,7 @@ "no-floating-promises": false, "no-focused-tests": true, "no-implicit-dependencies": [true, "dev", ["ng-mocks"]], + "no-inferred-empty-object-type": false, "no-magic-numbers": false, "no-null-keyword": false, "no-submodule-imports": false, @@ -26,9 +28,10 @@ "quotemark": [true, "single"], "semicolon": [true, "always", "strict-bound-class-methods"], "strict-boolean-expressions": false, - "strict-comparisons": false, + "strict-comparisons": [true, { "allow-object-equal-comparison": true }], "trailing-comma": [true, { "esSpecCompliant": true }], "typedef": false, + "unified-signatures": false, "variable-name": [true, "allow-leading-underscore"] } }