From 797cec34c9afa94298fdfa3a971d92d54439969b Mon Sep 17 00:00:00 2001 From: satanTime Date: Thu, 16 Jun 2022 21:32:40 +0200 Subject: [PATCH] feat(core): Support of standalone declarations #2687 --- .codeclimate.yml | 1 + .eslintrc.yml | 8 +- docs/articles/api/MockBuilder.md | 52 ++++ docs/articles/api/MockComponent.md | 26 ++ docs/articles/api/MockDirective.md | 28 ++- docs/articles/api/MockPipe.md | 22 ++ docs/articles/api/ngMocks/get.md | 11 +- docs/articles/guides/component-standalone.md | 144 +++++++++++ docs/articles/guides/directive-standalone.md | 125 ++++++++++ docs/articles/guides/pipe-standalone.md | 122 +++++++++ docs/sidebars.js | 3 + e2e/a5es2015/src/app/app.module.ts | 2 +- e2e/a5es5/src/app/app.module.ts | 2 +- e2e/min/src/app/app.module.ts | 2 +- examples/MockComponent/test.spec.ts | 2 +- examples/MockInstance/test.spec.ts | 2 +- examples/TestStandaloneComponent/test.spec.ts | 87 +++++++ examples/TestStandaloneDirective/test.spec.ts | 68 +++++ examples/TestStandalonePipe/test.spec.ts | 85 +++++++ karma.conf.ts | 2 +- libs/ng-mocks/src/lib/common/core.helpers.ts | 4 +- .../common/core.reflect.directive-resolve.ts | 2 +- .../lib/common/core.reflect.pipe-resolve.ts | 2 +- .../src/lib/common/func.extract-deps.ts | 35 +++ .../src/lib/common/func.get-ng-type.ts | 44 ++++ .../src/lib/common/func.is-mock-ng-def.ts | 12 +- .../func.is-ng-module-def-with-providers.ts | 6 +- .../src/lib/common/func.is-ng-type.ts | 20 +- .../src/lib/common/func.is-standalone.ts | 15 ++ .../src/lib/mock-builder/mock-builder.ts | 2 + .../create-ng-mocks-overrides-token.ts | 9 +- .../mock-builder/promise/init-exclude-def.ts | 9 +- .../lib/mock-builder/promise/init-keep-def.ts | 20 +- .../promise/init-mock-declarations.ts | 3 +- .../mock-builder/promise/init-ng-modules.ts | 9 +- .../mock-builder/promise/init-replace-def.ts | 6 +- .../lib/mock-builder/promise/init-universe.ts | 16 +- libs/ng-mocks/src/lib/mock-builder/types.ts | 5 + .../src/lib/mock-component/mock-component.ts | 7 +- .../mock-helper.find-instance.ts | 4 +- .../mock-helper.find-instances.ts | 4 +- .../src/lib/mock-helper/mock-helper.get.ts | 15 +- .../src/lib/mock-helper/mock-helper.ts | 12 + .../src/lib/mock-module/mock-module.ts | 8 +- .../lib/mock-render/func.generate-template.ts | 2 +- .../lib/mock-render/mock-render-factory.ts | 4 +- .../src/lib/mock/decorate-declaration.ts | 50 ++-- libs/ng-mocks/src/lib/mock/get-mock.ts | 9 + .../src/lib/resolve/collect-declarations.ts | 46 ++-- tests-e2e/src/ngrx/provide-mock-store.spec.ts | 8 +- tests-e2e/src/ngxs/test.spec.ts | 4 +- tests/issue-2687/legacy.spec.ts | 236 ++++++++++++++++++ tests/issue-2687/test.spec.ts | 203 +++++++++++++++ tests/ng-mocks-get/test.spec.ts | 34 +++ 54 files changed, 1558 insertions(+), 101 deletions(-) create mode 100644 docs/articles/guides/component-standalone.md create mode 100644 docs/articles/guides/directive-standalone.md create mode 100644 docs/articles/guides/pipe-standalone.md create mode 100644 examples/TestStandaloneComponent/test.spec.ts create mode 100644 examples/TestStandaloneDirective/test.spec.ts create mode 100644 examples/TestStandalonePipe/test.spec.ts create mode 100644 libs/ng-mocks/src/lib/common/func.extract-deps.ts create mode 100644 libs/ng-mocks/src/lib/common/func.get-ng-type.ts create mode 100644 libs/ng-mocks/src/lib/common/func.is-standalone.ts create mode 100644 tests/issue-2687/legacy.spec.ts create mode 100644 tests/issue-2687/test.spec.ts create mode 100644 tests/ng-mocks-get/test.spec.ts diff --git a/.codeclimate.yml b/.codeclimate.yml index e8039ccb1a..a47defd4e0 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -46,6 +46,7 @@ exclude_patterns: - 'karma.conf.ts' - 'karma.ie.sh' - 'renovate.json' + - 'webpack.config.js' - '**/*.json' - '**/*.md' - '**/*.yml' diff --git a/.eslintrc.yml b/.eslintrc.yml index e43d3e2ba2..53efd38ee2 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -151,14 +151,12 @@ overrides: - single - avoidEscape: true allowTemplateLiterals: true - sort-imports: - - error - - ignoreCase: true - ignoreDeclarationSort: true - allowSeparatedGroups: true import/order: - error - newlines-between: always + alphabetize: + order: asc + caseInsensitive: true groups: - builtin - external diff --git a/docs/articles/api/MockBuilder.md b/docs/articles/api/MockBuilder.md index 0b88449f08..5cea5234cb 100644 --- a/docs/articles/api/MockBuilder.md +++ b/docs/articles/api/MockBuilder.md @@ -17,6 +17,26 @@ where **everything in the module will be replaced with their mocks**, except the beforeEach(() => { return MockBuilder(TheThing, ItsModule); }); + +// To test a component +beforeEach(() => { + return MockBuilder(TheComponent, ItsModule); +}); + +// To test a directive +beforeEach(() => { + return MockBuilder(TheDirective, ItsModule); +}); + +// To test a pipe +beforeEach(() => { + return MockBuilder(ThePipe, ItsModule); +}); + +// To test a standalone declarations +beforeEach(() => { + return MockBuilder(TheStandaloneDeclaration).mock(OneOfItsImports); +}); ``` `MockBuilder` tends to provide **a simple instrument to turn Angular dependencies into their mocks**, @@ -26,6 +46,7 @@ and has a rich toolkit that supports: - detection and creation of mocks for root providers - replacement of modules and declarations at any depth - exclusion of modules, declarations and providers at any depth +- shallow testing of [standalone declarations](#shallow-flag) ## Simple example @@ -375,6 +396,30 @@ beforeEach(() => { }); ``` +### `shallow` flag + +The `shallow` flag works with kept standalone declarations. +It signals `MockBuilder` to mock all imports of the declaration, whereas the declaration itself won't be mocked. + +```ts +beforeEach(() => { + return MockBuilder() + .keep(StandaloneComponent, { + shallow: true, // all imports of StandaloneComponent will be mocks. + }); +}); +``` + +Also, if a standalone declaration has been passed as the first parameter of `MockBuilder`, +then the `shallow` flag will be automatically set. It allows smooth shallow testing of them. + +```ts +beforeEach(() => { + // All imports, apart from OneOfItsDependenciesPipe, of StandaloneComponent will be mocks. + return MockBuilder(StandaloneComponent).keep(OneOfItsDependenciesPipe); +}); +``` + ### `render` flag When we want to render a structural directive by default, we can do that via adding the `render` flag in its config. @@ -474,6 +519,13 @@ All other root providers will be replaced with their mocks, even for kept declar ## Factory function +You might be using other [testing frameworks for angular](../extra/with-3rd-party), +such as `@ngneat/spectator` or `@testing-library/angular`. + +This is a use-case for the factory function. + +The factory function allows you to get a preconfigured `TestBed` declarations which can be passed wherever else. + ```ts const ngModule = MockBuilder(MyComponent, MyModule) .build(); diff --git a/docs/articles/api/MockComponent.md b/docs/articles/api/MockComponent.md index f68d307ccf..54dcf17770 100644 --- a/docs/articles/api/MockComponent.md +++ b/docs/articles/api/MockComponent.md @@ -30,6 +30,7 @@ The class of a mock component has: - support for `@ContentChild` and `@ContentChildren` - support for `ControlValueAccessor`, `Validator` and `AsyncValidator` - support for `exportAs` +- support for [standalone components](#standalone-components) :::tip Information about mocking FormControl, ControlValueAccessor, Validator and AsyncValidator @@ -84,6 +85,7 @@ and [`MockRender`](MockRender.md): ```ts describe('Test', () => { beforeEach(() => { + // DependencyComponent is a declaration or imported somewhere in ItsModule. return MockBuilder(TargetComponent, ItsModule); }); @@ -94,6 +96,30 @@ describe('Test', () => { }); ``` +## Standalone components + +Starting Angular 14, it provides support for standalone components. +They are supported by `ng-mocks`. To mock a standalone component, you need to call `MockComponent` in imports: + +```ts +TestBed.configureTestingModule({ + imports: [ + // for a single component + MockComponent(StandaloneComponent), + + // for a set of components + ...MockComponents(Standalone1Component, Standalone2Component), + ], + declarations: [ + // our component for testing + TargetComponent, + ], +}); +``` + +[`MockBuilder`](./MockBuilder.md) also supports standalone components +and allows to mock their imports only for shallow rendering. + ## Advanced example An advanced example about **mocking components**. diff --git a/docs/articles/api/MockDirective.md b/docs/articles/api/MockDirective.md index d2378b244f..eb3fbfff48 100644 --- a/docs/articles/api/MockDirective.md +++ b/docs/articles/api/MockDirective.md @@ -30,6 +30,7 @@ A mock directive has: - support for `@ContentChild` and `@ContentChildren` - support for `ControlValueAccessor`, `Validator` and `AsyncValidator` - supports `exportAs` +- support for [standalone directives](#standalone-directives) :::tip Information about mocking FormControl, ControlValueAccessor, Validator and AsyncValidator @@ -84,7 +85,7 @@ and [`MockRender`](MockRender.md): ```ts describe('Test', () => { beforeEach(() => { - // DependencyDirective is a declaration in ItsModule. + // DependencyDirective is a declaration or imported somewhere in ItsModule. return MockBuilder(TargetComponent, ItsModule); }); @@ -95,6 +96,31 @@ describe('Test', () => { }); ``` +## Standalone directives + +Angular 14 has introduced support for standalone directives. +`ng-mocks` recognizes and properly mocks them. +To mock a standalone directive, you need to call `MockDirective` in imports: + +```ts +TestBed.configureTestingModule({ + imports: [ + // for a single directive + MockDirective(StandaloneDirective), + + // for a set of directives + ...MockDirectives(Standalone1Directive, Standalone2Directive), + ], + declarations: [ + // our component for testing + TargetComponent, + ], +}); +``` + +[`MockBuilder`](./MockBuilder.md) recognizes and handles standalone directives. +Also, it allows to mock their imports only for shallow testing. + ## Advanced example with attribute directives An advanced example about **mocking attribute directives**. diff --git a/docs/articles/api/MockPipe.md b/docs/articles/api/MockPipe.md index c1c1540907..7f1ecdabaf 100644 --- a/docs/articles/api/MockPipe.md +++ b/docs/articles/api/MockPipe.md @@ -30,6 +30,7 @@ A mock pipe has: - the same `name` - default transform is `() => undefined` to prevent problems with chaining +- support for [standalone pipes](#standalone-pipes) ## Simple example @@ -96,6 +97,27 @@ describe('Test', () => { }); ``` +## Standalone pipes + +Since Angular 14, pipes can be implemented as a standalone declaration. +`ng-mocks` detects and correctly mocks them. +To mock a standalone pipe, you need to call `MockPipe` in imports: + +```ts +TestBed.configureTestingModule({ + imports: [ + // for a single pipe + MockPipe(StandalonePipe), + ], + declarations: [ + // our component for testing + TargetComponent, + ], +}); +``` + +[`MockBuilder`](./MockBuilder.md) also supports and correctly works with standalone pipes. + ## Advanced example An advanced example of **mocking pipes** in Angular tests. diff --git a/docs/articles/api/ngMocks/get.md b/docs/articles/api/ngMocks/get.md index 948541e49a..e8cebcca93 100644 --- a/docs/articles/api/ngMocks/get.md +++ b/docs/articles/api/ngMocks/get.md @@ -3,7 +3,8 @@ title: ngMocks.get description: Documentation about ngMocks.get from ng-mocks library --- -Returns an attribute or structural directive which belongs to the current element. +Returns a declaration, service or token, which can be attribute or structural directives, +which belongs to the current element. - `ngMocks.get( debugElement, directive, notFoundValue? )` @@ -16,3 +17,11 @@ or simply with selectors which are supported by [`ngMocks.find`](./find.md). ```ts const directive = ngMocks.get('app-component', Directive); ``` + +## Root providers + +If you need to get a root provider, then `ngMocks.get` should be called without the first parameter: + +```ts +const appId = ngMocks.get(APP_ID); +``` diff --git a/docs/articles/guides/component-standalone.md b/docs/articles/guides/component-standalone.md new file mode 100644 index 0000000000..f0a0ce8289 --- /dev/null +++ b/docs/articles/guides/component-standalone.md @@ -0,0 +1,144 @@ +--- +title: How to test a standalone component in Angular and mock its imports +description: Covering an Angular standalone component with tests +sidebar_label: Standalone Component +--- + +This section describes how to test a standalone component. + +Usually, developers want to mock all dependencies. +For a standalone component, it means all its imports. +This behavior is possible to achieve with [`MockBuilder`](../api/MockBuilder.md#shallow-flag). + +Let's image we have the next standalone component: + +```ts +@Component({ + selector: 'target', + template: `{{ name | standalone }}`, + standalone: true, + imports: [DependencyModule, StandalonePipe], +}) +class StandaloneComponent { + @Input() public readonly name: string | null = null; +} +``` + +As we can see, it imports `DependencyModule`, which provides `DependencyComponent`, and StandalonePipe, +and, ideally, they should be mocked. + +The answer is: + +```ts +beforeEach(() => { + return MockBuilder(StandaloneComponent); +}); +``` + +Under the hood it marks `StandaloneComponent` as [kept](../api/MockBuilder.md#keep) +and sets [shallow](../api/MockBuilder.md#shallow-flag) and [export](../api/MockBuilder.md#export-flag) flags: + +```ts +beforeEach(() => { + return MockBuilder().keep(StandaloneComponent, { + shallow: true, + export: true, + }); +}); +``` + +That's it. Now all imports of `StandaloneComponent` are mocks, +and its properties, methods, injections and template are available for testing. + +If you need to keep an import, simply call [`.keep`](../api/MockBuilder.md#keep) with it. +For example, if we wanted to keep `StandalonePipe` then the code would look like: + +```ts +beforeEach(() => { + return MockBuilder(StandaloneComponent).keep(StandalonePipe); +}); +``` + +## Live example + +- [Try it on StackBlitz](https://stackblitz.com/github/ng-mocks/examples/tree/tests?file=src/examples/TestStandaloneComponent/test.spec.ts&initialpath=%3Fspec%3DTestStandaloneComponent) +- [Try it on CodeSandbox](https://codesandbox.io/s/github/ng-mocks/examples/tree/tests?file=/src/examples/TestStandaloneComponent/test.spec.ts&initialpath=%3Fspec%3DTestStandaloneComponent) + +```ts title="https://github.com/ike18t/ng-mocks/tree/master/examples/TestStandaloneComponent/test.spec.ts" +import { + Component, + Input, + NgModule, + Pipe, + PipeTransform, +} from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// A simple standalone pipe we are going to mock. +@Pipe({ + name: 'standalone', + standalone: true, +}) +class StandalonePipe implements PipeTransform { + transform(value: string | null): string { + return `${value}:${this.constructor.name}`; + } +} + +// A simple dependency component we are going to mock. +@Component({ + selector: 'dependency', + template: '', +}) +class DependencyComponent { + @Input() public readonly name: string | null = null; +} + +// A module which declares and exports the dependency component. +@NgModule({ + declarations: [DependencyComponent], + exports: [DependencyComponent], +}) +class DependencyModule {} + +// A standalone component we are going to test. +@Component({ + selector: 'standalone', + template: `{{ + name | standalone + }}`, + standalone: true, + imports: [DependencyModule, StandalonePipe], +}) +class StandaloneComponent { + @Input() public readonly name: string | null = null; +} + +describe('TestStandaloneComponent', () => { + beforeEach(() => { + return MockBuilder(StandaloneComponent); + }); + + it('renders dependencies', () => { + const fixture = MockRender(StandaloneComponent, { + name: 'test', + }); + + // asserting that we passed the input + const dependencyComponent = ngMocks.findInstance( + DependencyComponent, + ); + expect(dependencyComponent.name).toEqual('test'); + + // asserting how we called the pipe + const standalonePipe = ngMocks.findInstance(StandalonePipe); + // it's possible because of autoSpy. + expect(standalonePipe.transform).toHaveBeenCalledWith('test'); + + // or asserting the generated html + expect(ngMocks.formatHtml(fixture)).toEqual( + '', + ); + }); +}); +``` diff --git a/docs/articles/guides/directive-standalone.md b/docs/articles/guides/directive-standalone.md new file mode 100644 index 0000000000..86513e99f4 --- /dev/null +++ b/docs/articles/guides/directive-standalone.md @@ -0,0 +1,125 @@ +--- +title: How to test a standalone directive in Angular application +description: Covering an Angular standalone directive with tests +sidebar_label: Standalone Directive +--- + +Here you can find how to test a standalone directive. + +A standalone directive has the same feature set as a regular directive. +The only possible dependencies for a standalone directive are root services and tokens. +In a unit test, developers prefer to mock such dependencies. +[`MockBuilder`](../api/MockBuilder.md#shallow-flag) helps to configure `TestBed` in such the way. + +Let's image we have the next standalone directive: + +```ts +@Directive({ + selector: 'standalone', + standalone: true, +}) +class StandaloneDirective implements OnInit { + @Input() public readonly name: string | null = null; + + constructor(public readonly rootService: RootService) {} + + ngOnInit(): void { + this.rootService.trigger(this.name); + } +} +``` + +As we can see, the standalone directive injects `RootService`, and, ideally, the service should be mocked. + +To configure `TestBed` for that you need to use the next code: + +```ts +beforeEach(() => { + return MockBuilder(StandaloneDirective); +}); +``` + +Under the hood it marks `StandaloneDirective` as [kept](../api/MockBuilder.md#keep) +and sets [shallow](../api/MockBuilder.md#shallow-flag) and [export](../api/MockBuilder.md#export-flag) flags: + +```ts +beforeEach(() => { + return MockBuilder().keep(StandaloneDirective, { + shallow: true, + export: true, + }); +}); +``` + +Now all dependencies of `StandaloneDirective` are mocks, +and the properties, methods, injections of the directive are available for testing. + +If you need to keep a dependency, simply call [`.keep`](../api/MockBuilder.md#keep) with it. +For example, if we wanted to keep `RootService` then the code would look like: + +```ts +beforeEach(() => { + return MockBuilder(StandaloneDirective).keep(RootService); +}); +``` + +## Live example + +- [Try it on StackBlitz](https://stackblitz.com/github/ng-mocks/examples/tree/tests?file=src/examples/TestStandaloneDirective/test.spec.ts&initialpath=%3Fspec%3DTestStandaloneDirective) +- [Try it on CodeSandbox](https://codesandbox.io/s/github/ng-mocks/examples/tree/tests?file=/src/examples/TestStandaloneDirective/test.spec.ts&initialpath=%3Fspec%3DTestStandaloneDirective) + +```ts title="https://github.com/ike18t/ng-mocks/tree/master/examples/TestStandaloneDirective/test.spec.ts" +import { + Directive, + Injectable, + Input, + OnInit, +} from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// A root service we want to mock. +@Injectable({ + providedIn: 'root', +}) +class RootService { + trigger(name: string | null) { + // does something very cool + + return name; + } +} + +// A standalone directive we are going to test. +@Directive({ + selector: 'standalone', + standalone: true, +}) +class StandaloneDirective implements OnInit { + @Input() public readonly name: string | null = null; + + constructor(public readonly rootService: RootService) {} + + ngOnInit(): void { + this.rootService.trigger(this.name); + } +} + +describe('TestStandaloneDirective', () => { + beforeEach(() => { + return MockBuilder(StandaloneDirective); + }); + + it('renders dependencies', () => { + // Rendering the directive. + MockRender(StandaloneDirective, { + name: 'test', + }); + + // Asserting that StandaloneDirective calls RootService.trigger. + const rootService = ngMocks.findInstance(RootService); + // it's possible because of autoSpy. + expect(rootService.trigger).toHaveBeenCalledWith('test'); + }); +}); +``` diff --git a/docs/articles/guides/pipe-standalone.md b/docs/articles/guides/pipe-standalone.md new file mode 100644 index 0000000000..16d2fa6b3c --- /dev/null +++ b/docs/articles/guides/pipe-standalone.md @@ -0,0 +1,122 @@ +--- +title: How to test a standalone pipe in Angular application +description: Covering an Angular standalone pipe with tests +sidebar_label: Standalone Pipe +--- + +A standalone pipe doesn't have many differences comparing to a regular pipe. +It cannot import any dependencies, it can only inject root providers. +In order to mock root providers, [`MockBuilder`](../api/MockBuilder.md#shallow-flag) can be used. + +Let's image we have the next standalone pipe: + +```ts +@Pipe({ + name: 'standalone', + standalone: true, +}) +class StandalonePipe implements PipeTransform { + constructor(public readonly rootService: RootService) {} + + transform(value: string): string { + return this.rootService.trigger(value); + } +} +``` + +As we see, it injects `RootService`, which should be mocked in unit tests. + +It's possible to configure with the next code: + +```ts +beforeEach(() => { + return MockBuilder(StandalonePipe); +}); +``` + +Now all root dependencies of `StandalonePipe` are mocks, +and the properties, methods, injections of the pipe are available for testing. + +If you need to keep a dependency, simply call [`.keep`](../api/MockBuilder.md#keep) with it. +For example, if we wanted to keep `RootService` then the code would look like: + +```ts +beforeEach(() => { + return MockBuilder(StandalonePipe).keep(RootService); +}); +``` + +## Live example + +- [Try it on StackBlitz](https://stackblitz.com/github/ng-mocks/examples/tree/tests?file=src/examples/TestStandalonePipe/test.spec.ts&initialpath=%3Fspec%3DTestStandalonePipe) +- [Try it on CodeSandbox](https://codesandbox.io/s/github/ng-mocks/examples/tree/tests?file=/src/examples/TestStandalonePipe/test.spec.ts&initialpath=%3Fspec%3DTestStandalonePipe) + +```ts title="https://github.com/ike18t/ng-mocks/tree/master/examples/TestStandalonePipe/test.spec.ts" +import { + Injectable, + Pipe, + PipeTransform, +} from '@angular/core'; +import { + MockBuilder, + MockInstance, + MockRender, + ngMocks, +} from 'ng-mocks'; + +// A root service we want to mock. +@Injectable({ + providedIn: 'root', +}) +class RootService { + trigger(name: string) { + // does something very cool + + return name; + } +} + +// A standalone pipe we are going to test. +@Pipe({ + name: 'standalone', + standalone: true, +}) +class StandalonePipe implements PipeTransform { + constructor(public readonly rootService: RootService) {} + + transform(value: string): string { + return this.rootService.trigger(value); + } +} + +describe('TestStandalonePipe', () => { + // It creates a context for mocks which will be reset after each test. + MockInstance.scope(); + + beforeEach(() => { + return MockBuilder(StandalonePipe); + }); + + it('renders dependencies', () => { + // Customizing what RootService does. + MockInstance( + RootService, + 'trigger', + jasmine.createSpy(), + ).and.returnValue('mock'); + + // Rendering the pipe. + const fixture = MockRender(StandalonePipe, { + $implicit: 'test', + }); + + // Asserting that StandalonePipe calls RootService.trigger. + const rootService = ngMocks.findInstance(RootService); + // It's possible because of autoSpy. + expect(rootService.trigger).toHaveBeenCalledWith('test'); + + // Asserting that StandalonePipe has rendered the result of the RootService + expect(ngMocks.formatText(fixture)).toEqual('mock'); + }); +}); +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 8652bfafb4..858f93693c 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -134,11 +134,14 @@ module.exports = { items: [ 'guides/component', 'guides/component-provider', + 'guides/component-standalone', 'guides/directive-attribute', 'guides/directive-provider', 'guides/directive-structural', 'guides/directive-structural-context', + 'guides/directive-standalone', 'guides/pipe', + 'guides/pipe-standalone', 'guides/view-child', 'guides/ngonchanges', 'guides/provider', diff --git a/e2e/a5es2015/src/app/app.module.ts b/e2e/a5es2015/src/app/app.module.ts index 5b99e2486a..bde0672855 100644 --- a/e2e/a5es2015/src/app/app.module.ts +++ b/e2e/a5es2015/src/app/app.module.ts @@ -1,5 +1,5 @@ -import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; diff --git a/e2e/a5es5/src/app/app.module.ts b/e2e/a5es5/src/app/app.module.ts index 5b99e2486a..bde0672855 100644 --- a/e2e/a5es5/src/app/app.module.ts +++ b/e2e/a5es5/src/app/app.module.ts @@ -1,5 +1,5 @@ -import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; diff --git a/e2e/min/src/app/app.module.ts b/e2e/min/src/app/app.module.ts index 5ce7811a4c..7bcb9e9a51 100644 --- a/e2e/min/src/app/app.module.ts +++ b/e2e/min/src/app/app.module.ts @@ -1,5 +1,5 @@ -import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; diff --git a/examples/MockComponent/test.spec.ts b/examples/MockComponent/test.spec.ts index 3aa612d0ed..8bbf06d72e 100644 --- a/examples/MockComponent/test.spec.ts +++ b/examples/MockComponent/test.spec.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { Component, ContentChild, @@ -7,7 +8,6 @@ import { Output, TemplateRef, } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; diff --git a/examples/MockInstance/test.spec.ts b/examples/MockInstance/test.spec.ts index 17c78dc4e5..c6ddc4ba88 100644 --- a/examples/MockInstance/test.spec.ts +++ b/examples/MockInstance/test.spec.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { AfterViewInit, Component, @@ -6,7 +7,6 @@ import { ViewChild, } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { CommonModule } from '@angular/common'; import { MockBuilder, MockInstance, MockRender } from 'ng-mocks'; diff --git a/examples/TestStandaloneComponent/test.spec.ts b/examples/TestStandaloneComponent/test.spec.ts new file mode 100644 index 0000000000..d2aeec0811 --- /dev/null +++ b/examples/TestStandaloneComponent/test.spec.ts @@ -0,0 +1,87 @@ +import { + Component, + Input, + NgModule, + Pipe, + PipeTransform, + VERSION, +} from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// A simple standalone pipe we are going to mock. +@Pipe({ + name: 'standalone', + standalone: true, +} as never) +class StandalonePipe implements PipeTransform { + transform(value: string | null): string { + return `${value}:${this.constructor.name}`; + } +} + +// A simple dependency component we are going to mock. +@Component({ + selector: 'dependency', + template: '', +}) +class DependencyComponent { + @Input() public readonly name: string | null = null; +} + +// A module which declares and exports the dependency component. +@NgModule({ + declarations: [DependencyComponent], + exports: [DependencyComponent], +}) +class DependencyModule {} + +// A standalone component we are going to test. +@Component({ + selector: 'standalone', + template: `{{ + name | standalone + }}`, + standalone: true, + imports: [DependencyModule, StandalonePipe], +} as never) +class StandaloneComponent { + @Input() public readonly name: string | null = null; +} + +describe('TestStandaloneComponent', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs a14', () => { + // pending('Need Angular > 5'); + expect(true).toBeTruthy(); + }); + + return; + } + + beforeEach(() => { + return MockBuilder(StandaloneComponent); + }); + + it('renders dependencies', () => { + const fixture = MockRender(StandaloneComponent, { + name: 'test', + }); + + // asserting that we passed the input + const dependencyComponent = ngMocks.findInstance( + DependencyComponent, + ); + expect(dependencyComponent.name).toEqual('test'); + + // asserting how we called the pipe + const standalonePipe = ngMocks.findInstance(StandalonePipe); + // it's possible because of autoSpy. + expect(standalonePipe.transform).toHaveBeenCalledWith('test'); + + // or asserting the generated html + expect(ngMocks.formatHtml(fixture)).toEqual( + '', + ); + }); +}); diff --git a/examples/TestStandaloneDirective/test.spec.ts b/examples/TestStandaloneDirective/test.spec.ts new file mode 100644 index 0000000000..2a222dc4ef --- /dev/null +++ b/examples/TestStandaloneDirective/test.spec.ts @@ -0,0 +1,68 @@ +import { + Directive, + Injectable, + Input, + OnInit, + VERSION, +} from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// @TODO remove with A5 support +const injectableRootServiceArgs = [ + { + providedIn: 'root', + } as never, +]; + +// A root service we want to mock. +@Injectable(...injectableRootServiceArgs) +class RootService { + trigger(name: string | null) { + // does something very cool + + return name; + } +} + +// A standalone directive we are going to test. +@Directive({ + selector: 'standalone', + standalone: true, +} as never) +class StandaloneDirective implements OnInit { + @Input() public readonly name: string | null = null; + + constructor(public readonly rootService: RootService) {} + + ngOnInit(): void { + this.rootService.trigger(this.name); + } +} + +describe('TestStandaloneDirective', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs a14', () => { + // pending('Need Angular > 5'); + expect(true).toBeTruthy(); + }); + + return; + } + + beforeEach(() => { + return MockBuilder(StandaloneDirective); + }); + + it('renders dependencies', () => { + // Rendering the directive. + MockRender(StandaloneDirective, { + name: 'test', + }); + + // Asserting that StandaloneDirective calls RootService.trigger. + const rootService = ngMocks.findInstance(RootService); + // it's possible because of autoSpy. + expect(rootService.trigger).toHaveBeenCalledWith('test'); + }); +}); diff --git a/examples/TestStandalonePipe/test.spec.ts b/examples/TestStandalonePipe/test.spec.ts new file mode 100644 index 0000000000..35e4cbc439 --- /dev/null +++ b/examples/TestStandalonePipe/test.spec.ts @@ -0,0 +1,85 @@ +import { + Injectable, + Pipe, + PipeTransform, + VERSION, +} from '@angular/core'; + +import { + MockBuilder, + MockInstance, + MockRender, + ngMocks, +} from 'ng-mocks'; + +// @TODO remove with A5 support +const injectableRootServiceArgs = [ + { + providedIn: 'root', + } as never, +]; + +// A root service we want to mock. +@Injectable(...injectableRootServiceArgs) +class RootService { + trigger(name: string) { + // does something very cool + + return name; + } +} + +// A standalone pipe we are going to test. +@Pipe({ + name: 'standalone', + standalone: true, +} as never) +class StandalonePipe implements PipeTransform { + constructor(public readonly rootService: RootService) {} + + transform(value: string): string { + return this.rootService.trigger(value); + } +} + +describe('TestStandalonePipe', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs a14', () => { + // pending('Need Angular > 5'); + expect(true).toBeTruthy(); + }); + + return; + } + + // It creates a context for mocks which will be reset after each test. + MockInstance.scope(); + + beforeEach(() => { + return MockBuilder(StandalonePipe); + }); + + it('renders dependencies', () => { + // Customizing what RootService does. + MockInstance( + RootService, + 'trigger', + typeof jest === 'undefined' + ? jasmine.createSpy().and.returnValue('mock') + : jest.fn().mockReturnValue('mock'), + ); + + // Rendering the pipe. + const fixture = MockRender(StandalonePipe, { + $implicit: 'test', + }); + + // Asserting that StandalonePipe calls RootService.trigger. + const rootService = ngMocks.findInstance(RootService); + // It's possible because of autoSpy. + expect(rootService.trigger).toHaveBeenCalledWith('test'); + + // Asserting that StandalonePipe has rendered the result of the RootService + expect(ngMocks.formatText(fixture)).toEqual('mock'); + }); +}); diff --git a/karma.conf.ts b/karma.conf.ts index ebc631965b..5969f38876 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -3,8 +3,8 @@ import { join } from 'node:path'; import { Config } from 'karma'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; import puppeteer from 'puppeteer'; +import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; process.env.CHROME_BIN = (puppeteer as any as puppeteer.PuppeteerNode).executablePath(); diff --git a/libs/ng-mocks/src/lib/common/core.helpers.ts b/libs/ng-mocks/src/lib/common/core.helpers.ts index 43e52105cf..e624266b4c 100644 --- a/libs/ng-mocks/src/lib/common/core.helpers.ts +++ b/libs/ng-mocks/src/lib/common/core.helpers.ts @@ -110,7 +110,7 @@ export const extractDependency = (deps: any[], set?: Set): void => { } }; -const extendClassicClass = (base: AnyType): Type => { +export const extendClassicClass = (base: AnyType): Type => { let child: any; const glb = funcGetGlobal(); @@ -134,7 +134,7 @@ const extendClassicClass = (base: AnyType): Type => { return child; }; -export const extendClass = (base: AnyType): Type => { +export const extendClass = (base: AnyType): Type => { const child: Type = extendClassicClass(base); coreDefineProperty(child, 'name', `MockMiddleware${funcGetName(base)}`, true); diff --git a/libs/ng-mocks/src/lib/common/core.reflect.directive-resolve.ts b/libs/ng-mocks/src/lib/common/core.reflect.directive-resolve.ts index 21a5cca9f7..7dba672e0c 100644 --- a/libs/ng-mocks/src/lib/common/core.reflect.directive-resolve.ts +++ b/libs/ng-mocks/src/lib/common/core.reflect.directive-resolve.ts @@ -4,7 +4,7 @@ import collectDeclarations from '../resolve/collect-declarations'; import coreReflectBodyCatch from './core.reflect.body-catch'; -export default (def: any): Directive & Partial => +export default (def: any): Directive & Partial & { standalone?: boolean } => coreReflectBodyCatch((arg: any) => { const declaration = collectDeclarations(arg); if (declaration.Component) { diff --git a/libs/ng-mocks/src/lib/common/core.reflect.pipe-resolve.ts b/libs/ng-mocks/src/lib/common/core.reflect.pipe-resolve.ts index 3f8e1b781b..4f97c0052c 100644 --- a/libs/ng-mocks/src/lib/common/core.reflect.pipe-resolve.ts +++ b/libs/ng-mocks/src/lib/common/core.reflect.pipe-resolve.ts @@ -4,7 +4,7 @@ import collectDeclarations from '../resolve/collect-declarations'; import coreReflectBodyCatch from './core.reflect.body-catch'; -export default (def: any): Pipe => +export default (def: any): Pipe & { standalone?: boolean } => coreReflectBodyCatch((arg: any) => { const declaration = collectDeclarations(arg); if (declaration.Pipe) { diff --git a/libs/ng-mocks/src/lib/common/func.extract-deps.ts b/libs/ng-mocks/src/lib/common/func.extract-deps.ts new file mode 100644 index 0000000000..a85e7f62f0 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.extract-deps.ts @@ -0,0 +1,35 @@ +import collectDeclarations from '../resolve/collect-declarations'; + +import coreConfig from './core.config'; +import { flatten } from './core.helpers'; +import { AnyDeclaration } from './core.types'; +import { getNgType } from './func.get-ng-type'; +import funcGetProvider from './func.get-provider'; +import { isNgModuleDefWithProviders } from './func.is-ng-module-def-with-providers'; + +export const funcExtractDeps = (def: any, result: Set>): Set> => { + const meta = collectDeclarations(def); + const type = getNgType(def); + // istanbul ignore if + if (!type) { + return result; + } + + const decorator = meta[type]; + for (const field of coreConfig.dependencies) { + if (!decorator[field]) { + continue; + } + + for (const item of flatten(decorator[field])) { + // istanbul ignore if: it is here for standalone things, however they don't support modules with providers. + if (isNgModuleDefWithProviders(item)) { + result.add(item.ngModule); + } else { + result.add(funcGetProvider(item)); + } + } + } + + return result; +}; diff --git a/libs/ng-mocks/src/lib/common/func.get-ng-type.ts b/libs/ng-mocks/src/lib/common/func.get-ng-type.ts new file mode 100644 index 0000000000..993b49bf84 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.get-ng-type.ts @@ -0,0 +1,44 @@ +import collectDeclarations from '../resolve/collect-declarations'; + +import { AnyDeclaration } from './core.types'; +import { isNgInjectionToken } from './func.is-ng-injection-token'; +import { NgModuleWithProviders } from './func.is-ng-module-def-with-providers'; + +/** + * Returns how the class has been decorated. + * It doesn't work well, because multi decorations and extensions of decorated classes can bring strange behavior. + * Because of that, we simply take the last decoration as the expected, if the decorator is not Injectable. + * Services have the lowest priority. + * + * @internal + * + * ```ts + * getNgType(MockModule); // returns 'NgModule' | 'Component' | 'Directive' | 'Pipe' | 'Injectable' + * ``` + */ +export const getNgType = ( + declaration: AnyDeclaration | NgModuleWithProviders, +): 'NgModule' | 'Component' | 'Directive' | 'Pipe' | 'Injectable' | undefined => { + if (typeof declaration === 'string') { + return undefined; + } + if (isNgInjectionToken(declaration)) { + return 'Injectable'; + } + + const { decorators } = collectDeclarations(declaration); + + for (let index = decorators.length - 1; index >= 0; index -= 1) { + if (decorators[index] === 'Injectable') { + continue; + } + + return decorators[index]; + } + + if (decorators.length > 0) { + return 'Injectable'; + } + + return undefined; +}; diff --git a/libs/ng-mocks/src/lib/common/func.is-mock-ng-def.ts b/libs/ng-mocks/src/lib/common/func.is-mock-ng-def.ts index 8413543a0f..b3eeb91c7b 100644 --- a/libs/ng-mocks/src/lib/common/func.is-mock-ng-def.ts +++ b/libs/ng-mocks/src/lib/common/func.is-mock-ng-def.ts @@ -3,7 +3,7 @@ import { MockedDirective } from '../mock-directive/types'; import { MockedModule } from '../mock-module/types'; import { MockedPipe } from '../mock-pipe/types'; -import { Type } from './core.types'; +import { AnyType, Type } from './core.types'; import { isNgDef } from './func.is-ng-def'; /** @@ -17,7 +17,7 @@ import { isNgDef } from './func.is-ng-def'; * isMockNgDef(ArbitraryClass, 'c'); // returns false * ``` */ -export function isMockNgDef(component: Type, ngType: 'c'): component is Type>; +export function isMockNgDef(component: AnyType, ngType: 'c'): component is Type>; /** * isMockNgDef verifies whether a class is a mock directive class. @@ -30,7 +30,7 @@ export function isMockNgDef(component: Type, ngType: 'c'): component is Ty * isMockNgDef(ArbitraryClass, 'd'); // returns false * ``` */ -export function isMockNgDef(directive: Type, ngType: 'd'): directive is Type>; +export function isMockNgDef(directive: AnyType, ngType: 'd'): directive is Type>; /** * isMockNgDef verifies whether a class is a mock pipe class. @@ -43,7 +43,7 @@ export function isMockNgDef(directive: Type, ngType: 'd'): directive is Ty * isMockNgDef(ArbitraryClass, 'p'); // returns false * ``` */ -export function isMockNgDef(pipe: Type, ngType: 'p'): pipe is Type>; +export function isMockNgDef(pipe: AnyType, ngType: 'p'): pipe is Type>; /** * isMockNgDef verifies whether a class is a mock module class. @@ -56,7 +56,7 @@ export function isMockNgDef(pipe: Type, ngType: 'p'): pipe is Type(module: Type, ngType: 'm'): module is Type>; +export function isMockNgDef(module: AnyType, ngType: 'm'): module is Type>; /** * isMockNgDef verifies whether a class is a mock class. @@ -72,7 +72,7 @@ export function isMockNgDef(module: Type, ngType: 'm'): module is Type(module: Type): module is Type; export function isMockNgDef( - component: Type & { mockOf?: any }, + component: AnyType & { mockOf?: any }, type?: 'c' | 'd' | 'p' | 'm', ): component is Type { if (!(component as any).mockOf) { diff --git a/libs/ng-mocks/src/lib/common/func.is-ng-module-def-with-providers.ts b/libs/ng-mocks/src/lib/common/func.is-ng-module-def-with-providers.ts index 38e0dc28cc..0556d0a38d 100644 --- a/libs/ng-mocks/src/lib/common/func.is-ng-module-def-with-providers.ts +++ b/libs/ng-mocks/src/lib/common/func.is-ng-module-def-with-providers.ts @@ -1,7 +1,6 @@ import { Provider } from '@angular/core'; import { Type } from './core.types'; -import { isNgDef } from './func.is-ng-def'; /** * NgModuleWithProviders helps to support ModuleWithProviders in all angular versions. @@ -20,7 +19,4 @@ export interface NgModuleWithProviders { * @internal */ export const isNgModuleDefWithProviders = (declaration: any): declaration is NgModuleWithProviders => - declaration && - typeof declaration === 'object' && - declaration.ngModule !== undefined && - isNgDef(declaration.ngModule, 'm'); + declaration && typeof declaration === 'object' && typeof declaration.ngModule === 'function'; diff --git a/libs/ng-mocks/src/lib/common/func.is-ng-type.ts b/libs/ng-mocks/src/lib/common/func.is-ng-type.ts index 21fd8565a0..05dcde52bd 100644 --- a/libs/ng-mocks/src/lib/common/func.is-ng-type.ts +++ b/libs/ng-mocks/src/lib/common/func.is-ng-type.ts @@ -4,6 +4,7 @@ import { AnyType } from './core.types'; /** * Checks whether a class has been decorated with a specific Angular decorator. + * Due to the extension / multi decoration, we rely on the last used decorator. * * @internal * @@ -14,7 +15,22 @@ import { AnyType } from './core.types'; * ``` */ export const isNgType = (declaration: AnyType, type: string): boolean => { - const declarations = collectDeclarations(declaration); + const { decorators } = collectDeclarations(declaration); + if (decorators.length === 0) { + return false; + } - return !!declarations[type]; + let offset = 1; + + // Injectable works well if the declaration is in providers. + if (type === 'Injectable' && decorators.indexOf('Injectable') !== -1) { + return true; + } + + // Skipping Injectable. + while (decorators[decorators.length - offset] === 'Injectable') { + offset += 1; + } + + return decorators[decorators.length - offset] === type; }; diff --git a/libs/ng-mocks/src/lib/common/func.is-standalone.ts b/libs/ng-mocks/src/lib/common/func.is-standalone.ts new file mode 100644 index 0000000000..621d1415b9 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.is-standalone.ts @@ -0,0 +1,15 @@ +import collectDeclarations from '../resolve/collect-declarations'; + +import { getNgType } from './func.get-ng-type'; + +/** + * Checks whether a class has been decorated with the standalone flag. + */ +export function isStandalone(declaration: any): boolean { + const type = getNgType(declaration); + if (!type || type === 'Injectable') { + return false; + } + + return collectDeclarations(declaration)[type].standalone === true; +} diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts index 2838cf454a..c194f7f01e 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts @@ -1,6 +1,7 @@ import { flatten } from '../common/core.helpers'; import { AnyDeclaration } from '../common/core.types'; import { NgModuleWithProviders } from '../common/func.is-ng-module-def-with-providers'; +import { isStandalone } from '../common/func.is-standalone'; import { MockBuilderPerformance } from './mock-builder.performance'; import { IMockBuilder } from './types'; @@ -27,6 +28,7 @@ export function MockBuilder(...args: Array, defValue: Map): ValueProvider => if (!override) { continue; } - overrides.set(value, [{ set: override }, { set: original }]); + + // We need to delete standalone, because Angular was too lazy to check whether it has been really changed. + const patchedOriginal: Partial = {}; + for (const key of Object.keys(override)) { + patchedOriginal[key] = original[key]; + } + + overrides.set(value, [{ set: override }, { set: patchedOriginal }]); } return { diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-exclude-def.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-exclude-def.ts index 07871867dc..f8848f0e80 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-exclude-def.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-exclude-def.ts @@ -2,9 +2,12 @@ import { mapValues } from '../../common/core.helpers'; import ngMocksUniverse from '../../common/ng-mocks-universe'; export default (excludeDef: Set): void => { + const builtDeclarations = ngMocksUniverse.builtDeclarations; + const builtProviders = ngMocksUniverse.builtProviders; + const resolutions = ngMocksUniverse.config.get('ngMocksDepsResolution'); for (const def of mapValues(excludeDef)) { - ngMocksUniverse.builtDeclarations.set(def, null); - ngMocksUniverse.builtProviders.set(def, null); - ngMocksUniverse.config.get('ngMocksDepsResolution').set(def, 'exclude'); + builtDeclarations.set(def, null); + builtProviders.set(def, null); + resolutions.set(def, 'exclude'); } }; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-keep-def.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-keep-def.ts index 7ca8aa687c..e002d5f8e7 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-keep-def.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-keep-def.ts @@ -1,10 +1,22 @@ import { mapValues } from '../../common/core.helpers'; +import { funcExtractDeps } from '../../common/func.extract-deps'; import ngMocksUniverse from '../../common/ng-mocks-universe'; -export default (keepDef: Set): void => { +export default (keepDef: Set, configDef: Map): Set => { + const mockDef = new Set(); + const builtDeclarations = ngMocksUniverse.builtDeclarations; + const builtProviders = ngMocksUniverse.builtProviders; + const resolutions = ngMocksUniverse.config.get('ngMocksDepsResolution'); for (const def of mapValues(keepDef)) { - ngMocksUniverse.builtDeclarations.set(def, def); - ngMocksUniverse.builtProviders.set(def, def); - ngMocksUniverse.config.get('ngMocksDepsResolution').set(def, 'keep'); + builtDeclarations.set(def, def); + builtProviders.set(def, def); + resolutions.set(def, 'keep'); + + const config = configDef.get(def); + if (config.shallow) { + funcExtractDeps(def, mockDef); + } } + + return mockDef; }; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-mock-declarations.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-mock-declarations.ts index ae21754fe4..3b4e7fd4b4 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-mock-declarations.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-mock-declarations.ts @@ -5,8 +5,9 @@ import tryMockDeclaration from './try-mock-declaration'; import tryMockProvider from './try-mock-provider'; export default (mockDef: Set, defValue: Map): void => { + const resolutions: Map = ngMocksUniverse.config.get('ngMocksDepsResolution'); for (const def of mapValues(mockDef)) { - ngMocksUniverse.config.get('ngMocksDepsResolution').set(def, 'mock'); + resolutions.set(def, 'mock'); tryMockDeclaration(def, defValue); tryMockProvider(def, defValue); diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts index f2ad7d0e2d..69eeda4d81 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts @@ -1,11 +1,12 @@ import { flatten, mapValues } from '../../common/core.helpers'; +import coreReflectProvidedIn from '../../common/core.reflect.provided-in'; +import { AnyDeclaration } from '../../common/core.types'; +import funcGetName from '../../common/func.get-name'; import funcGetProvider from '../../common/func.get-provider'; import { isNgDef } from '../../common/func.is-ng-def'; +import { isStandalone } from '../../common/func.is-standalone'; import ngMocksUniverse from '../../common/ng-mocks-universe'; import markProviders from '../../mock-module/mark-providers'; -import funcGetName from '../../common/func.get-name'; -import { AnyDeclaration } from '../../common/core.types'; -import coreReflectProvidedIn from '../../common/core.reflect.provided-in'; import initModule from './init-module'; import { BuilderData, NgMeta } from './types'; @@ -27,7 +28,7 @@ const handleDef = ({ imports, declarations, providers }: NgMeta, def: any, defPr } if (isNgDef(def, 'c') || isNgDef(def, 'd') || isNgDef(def, 'p')) { - declarations.push(ngMocksUniverse.getBuildDeclaration(def)); + (isStandalone(def) ? imports : declarations).push(ngMocksUniverse.getBuildDeclaration(def)); touched = true; } diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-replace-def.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-replace-def.ts index ab1cb4b5c5..f12f35ba4c 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-replace-def.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-replace-def.ts @@ -2,8 +2,10 @@ import { mapValues } from '../../common/core.helpers'; import ngMocksUniverse from '../../common/ng-mocks-universe'; export default (replaceDef: Set, defValue: Map): void => { + const builtDeclarations = ngMocksUniverse.builtDeclarations; + const resolutions = ngMocksUniverse.config.get('ngMocksDepsResolution'); for (const def of mapValues(replaceDef)) { - ngMocksUniverse.builtDeclarations.set(def, defValue.get(def)); - ngMocksUniverse.config.get('ngMocksDepsResolution').set(def, 'replace'); + builtDeclarations.set(def, defValue.get(def)); + resolutions.set(def, 'replace'); } }; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts index aeb15a4345..fac2d1a4ce 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts @@ -1,4 +1,4 @@ -import { mapEntries } from '../../common/core.helpers'; +import { mapEntries, mapValues } from '../../common/core.helpers'; import ngMocksUniverse from '../../common/ng-mocks-universe'; import initExcludeDef from './init-exclude-def'; @@ -27,13 +27,25 @@ export default ({ ngMocksUniverse.config.set('ngMocksDepsSkip', new Set()); // flags to understand how to mock nested declarations. ngMocksUniverse.config.set('ngMocksDepsResolution', new Map()); + + const standaloneMocks = initKeepDef(keepDef, configDef); + for (const def of mapValues(standaloneMocks)) { + if (configDef.has(def)) { + continue; + } + mockDef.add(def); + configDef.set(def, { + dependency: true, + }); + } + for (const [k, v] of mapEntries(configDef)) { ngMocksUniverse.config.set(k, { ...ngMocksUniverse.getConfigMock().get(k), ...v, }); } - initKeepDef(keepDef); + initReplaceDef(replaceDef, defValue); initExcludeDef(excludeDef); initMockDeclarations(mockDef, defValue); diff --git a/libs/ng-mocks/src/lib/mock-builder/types.ts b/libs/ng-mocks/src/lib/mock-builder/types.ts index 0925a9a228..ace029363f 100644 --- a/libs/ng-mocks/src/lib/mock-builder/types.ts +++ b/libs/ng-mocks/src/lib/mock-builder/types.ts @@ -25,6 +25,11 @@ export interface IMockBuilderConfigAll { * @see https://ng-mocks.sudo.eu/api/MockBuilder#export-flag */ export?: boolean; + + /** + * @see https://ng-mocks.sudo.eu/api/MockBuilder#shallow-flag + */ + shallow?: boolean; } /** diff --git a/libs/ng-mocks/src/lib/mock-component/mock-component.ts b/libs/ng-mocks/src/lib/mock-component/mock-component.ts index 8e4dae776b..49ba8d3f02 100644 --- a/libs/ng-mocks/src/lib/mock-component/mock-component.ts +++ b/libs/ng-mocks/src/lib/mock-component/mock-component.ts @@ -199,7 +199,12 @@ coreDefineProperty(ComponentMockBase, 'parameters', [ const decorateClass = (component: Type, mock: Type): void => { const meta = coreReflectDirectiveResolve(component); const template = generateTemplate(meta.queries); - const mockParams = { exportAs: meta.exportAs, selector: meta.selector, template }; + const mockParams = { + exportAs: meta.exportAs, + selector: meta.selector, + standalone: meta.standalone, + template, + }; Component(decorateDeclaration(component, mock, meta, mockParams))(mock); }; diff --git a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts index 2a74936277..6e00967a11 100644 --- a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts +++ b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts @@ -1,3 +1,5 @@ +import { getInjection } from '../../common/core.helpers'; +import { Type } from '../../common/core.types'; import { getSourceOfMock } from '../../common/func.get-source-of-mock'; import { isNgDef } from '../../common/func.is-ng-def'; import mockHelperCrawl from '../crawl/mock-helper.crawl'; @@ -6,8 +8,6 @@ import funcGetFromNode from '../func.get-from-node'; import funcGetLastFixture from '../func.get-last-fixture'; import funcParseFindArgs from '../func.parse-find-args'; import funcParseFindArgsName from '../func.parse-find-args-name'; -import { getInjection } from '../../common/core.helpers'; -import { Type } from '../../common/core.types'; import funcIsValidFindInstanceSelector from './func.is-valid-find-instance-selector'; diff --git a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts index 05495c3e8a..7818b9fc00 100644 --- a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts +++ b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts @@ -1,3 +1,5 @@ +import { getInjection } from '../../common/core.helpers'; +import { Type } from '../../common/core.types'; import { getSourceOfMock } from '../../common/func.get-source-of-mock'; import { isNgDef } from '../../common/func.is-ng-def'; import mockHelperCrawl from '../crawl/mock-helper.crawl'; @@ -5,8 +7,6 @@ import mockHelperFindAll from '../find/mock-helper.find-all'; import funcGetFromNode from '../func.get-from-node'; import funcGetLastFixture from '../func.get-last-fixture'; import funcParseFindArgs from '../func.parse-find-args'; -import { getInjection } from '../../common/core.helpers'; -import { Type } from '../../common/core.types'; import funcIsValidFindInstanceSelector from './func.is-valid-find-instance-selector'; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts index 1afcada24b..762413c9ba 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts @@ -1,13 +1,16 @@ import { DebugElement } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { Type } from '../common/core.types'; +import funcGetName from '../common/func.get-name'; import { getSourceOfMock } from '../common/func.get-source-of-mock'; import { MockedDebugElement } from '../mock-render/types'; +import nestedCheckParent from './crawl/nested-check-parent'; import mockHelperFind from './find/mock-helper.find'; import funcGetFromNode from './func.get-from-node'; import funcGetLastFixture from './func.get-last-fixture'; -import nestedCheckParent from './crawl/nested-check-parent'; +import funcParseFindArgsName from './func.parse-find-args-name'; const defaultNotFoundValue = {}; // simulating Symbol @@ -24,6 +27,14 @@ const parseArgs = ( }); export default (...args: any[]) => { + if (args.length === 1) { + try { + return TestBed.inject ? TestBed.inject(args[0]) : /* istanbul ignore next */ TestBed.get(args[0]); + } catch { + throw new Error(`Cannot find an instance via ngMocks.get(${funcParseFindArgsName(args[0])})`); + } + } + const { el, sel, notFoundValue } = parseArgs(args); const root: DebugElement | undefined = mockHelperFind(funcGetLastFixture(), el, undefined); const source = getSourceOfMock(sel); @@ -50,5 +61,5 @@ export default (...args: any[]) => { if (notFoundValue !== defaultNotFoundValue) { return notFoundValue; } - throw new Error(`Cannot find ${sel.name} instance via ngMocks.get`); + throw new Error(`Cannot find ${funcGetName(sel)} instance via ngMocks.get`); }; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts index 7c2fc01705..1913672bf3 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts @@ -665,6 +665,18 @@ export const ngMocks: { */ get(elSelector: DebugNodeSelector, provider: AnyDeclaration, notFoundValue: D): D | T; + /** + * ngMocks.get tries to get an instance of provider or token for TestBed. + * + * @see https://ng-mocks.sudo.eu/api/ngMocks/get + * + * ```ts + * const myComponent = ngMocks.get(MyComponent); + * const myDirective = ngMocks.get(MyDirective); + * ``` + */ + get(provider: AnyDeclaration): T; + /** * ngMocks.findInstance searches for an instance of declaration, provider or token, * and returns the first one. diff --git a/libs/ng-mocks/src/lib/mock-module/mock-module.ts b/libs/ng-mocks/src/lib/mock-module/mock-module.ts index 154ecdd9b7..d54e165760 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-module.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-module.ts @@ -3,7 +3,7 @@ import { NgModule, Provider } from '@angular/core'; import coreConfig from '../common/core.config'; import { extendClass } from '../common/core.helpers'; import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; -import { Type } from '../common/core.types'; +import { AnyType, Type } from '../common/core.types'; import decorateMock from '../common/decorate.mock'; import funcGetName from '../common/func.get-name'; import funcImportExists from '../common/func.import-exists'; @@ -25,7 +25,7 @@ const flagReplace = (resolution?: string): boolean => const flagNever = (ngModule?: any): boolean => coreConfig.neverMockModule.indexOf(funcGetName(ngModule)) !== -1 && !ngMocksUniverse.flags.has('skipMock'); -const preProcessFlags = (ngModule: Type): { isRootModule: boolean; toggleSkipMockFlag: boolean } => { +const preProcessFlags = (ngModule: AnyType): { isRootModule: boolean; toggleSkipMockFlag: boolean } => { let toggleSkipMockFlag = false; let isRootModule = true; @@ -163,9 +163,9 @@ const getMockProviders = (ngModuleProviders: Provider[] | undefined): Provider[] const generateReturn = ( module: any, - ngModule: Type, + ngModule: AnyType, ngModuleProviders: Provider[] | undefined, - mockModule: Type, + mockModule: AnyType, mockModuleProviders: Provider[] | undefined, ): any => mockModule === ngModule && mockModuleProviders === ngModuleProviders diff --git a/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts b/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts index 2d237a05e8..1249746f36 100644 --- a/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts +++ b/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts @@ -1,5 +1,5 @@ -import { isNgDef } from '../common/func.is-ng-def'; import coreReflectPipeResolve from '../common/core.reflect.pipe-resolve'; +import { isNgDef } from '../common/func.is-ng-def'; const generateTemplateAttrWrap = (prop: string, type: 'i' | 'o') => (type === 'i' ? `[${prop}]` : `(${prop})`); diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts index 1e8d8e4e2b..60c04eaab6 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts @@ -2,7 +2,9 @@ import { DebugElement, Directive, InjectionToken } from '@angular/core'; import { getTestBed, TestBed } from '@angular/core/testing'; import coreDefineProperty from '../common/core.define-property'; +import { getInjection } from '../common/core.helpers'; import { AnyDeclaration, AnyType, Type } from '../common/core.types'; +import funcGetName from '../common/func.get-name'; import funcImportExists from '../common/func.import-exists'; import { isNgDef } from '../common/func.is-ng-def'; import ngMocksStack from '../common/ng-mocks-stack'; @@ -10,8 +12,6 @@ import ngMocksUniverse from '../common/ng-mocks-universe'; import { ngMocks } from '../mock-helper/mock-helper'; import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor'; import { MockService } from '../mock-service/mock-service'; -import funcGetName from '../common/func.get-name'; -import { getInjection } from '../common/core.helpers'; import funcCreateWrapper from './func.create-wrapper'; import funcInstallPropReader from './func.install-prop-reader'; diff --git a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts index bf30495774..9476dfe1db 100644 --- a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts +++ b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts @@ -1,4 +1,4 @@ -import { Component, Directive, Provider, ViewChild } from '@angular/core'; +import { Component, Directive, NgModule, Provider, ViewChild } from '@angular/core'; import { AnyType } from '../common/core.types'; import decorateInputs from '../common/decorate.inputs'; @@ -7,6 +7,7 @@ import decorateOutputs from '../common/decorate.outputs'; import decorateQueries from '../common/decorate.queries'; import { ngMocksMockConfig } from '../common/mock'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import mockNgDef from '../mock-module/mock-ng-def'; import helperMockService from '../mock-service/helper.mock-service'; import cloneProviders from './clone-providers'; @@ -30,31 +31,42 @@ const buildConfig = ( }; }; -export default ( +export default ( source: AnyType, mock: AnyType, - meta: { - hostBindings?: Array<[string, any]>; - hostListeners?: Array<[string, any, any]>; - inputs?: string[]; - outputs?: string[]; - providers?: Provider[]; - queries?: Record; - viewProviders?: Provider[]; - }, - params: T, + meta: Component & + Directive & + NgModule & { + hostBindings?: Array<[string, any]>; + hostListeners?: Array<[string, any, any]>; + imports?: any[]; + }, + params: T & { standalone?: boolean }, ) => { - const data = cloneProviders(source, mock, meta.providers || []); - const providers = [toExistingProvider(source, mock), ...data.providers]; + const options: T & { imports?: any[] } = { ...params }; + + const { setControlValueAccessor, providers } = cloneProviders(source, mock, meta.providers || []); + providers.push(toExistingProvider(source, mock)); + options.providers = providers; + const { providers: viewProviders } = cloneProviders(source, mock, meta.viewProviders || []); - const options: T = { ...params, providers: providers, viewProviders: viewProviders }; + if (viewProviders.length > 0) { + options.viewProviders = viewProviders; + } - if (data.setControlValueAccessor === undefined) { - data.setControlValueAccessor = - helperMockService.extractMethodsFromPrototype(source.prototype).indexOf('writeValue') !== -1; + if (params.standalone && meta.imports) { + const { imports } = mockNgDef({ imports: meta.imports })[1]; + if (imports?.length) { + options.imports = imports as never; + } } - const config: ngMocksMockConfig = buildConfig(source, meta, data.setControlValueAccessor); + const config: ngMocksMockConfig = buildConfig( + source, + meta, + setControlValueAccessor ?? + helperMockService.extractMethodsFromPrototype(source.prototype).indexOf('writeValue') !== -1, + ); decorateMock(mock, source, config); // istanbul ignore else diff --git a/libs/ng-mocks/src/lib/mock/get-mock.ts b/libs/ng-mocks/src/lib/mock/get-mock.ts index 62c8bde4c7..02b0215ab9 100644 --- a/libs/ng-mocks/src/lib/mock/get-mock.ts +++ b/libs/ng-mocks/src/lib/mock/get-mock.ts @@ -14,6 +14,11 @@ export default (def: any, type: any, func: string, cacheFlag: string, base: any, return ngMocksUniverse.cacheDeclarations.get(def); } + const hasNgMocksDepsResolution = ngMocksUniverse.config.has('ngMocksDepsResolution'); + if (!hasNgMocksDepsResolution) { + ngMocksUniverse.config.set('ngMocksDepsResolution', new Map()); + } + const mock = extendClass(base); decorator(def, mock); @@ -22,5 +27,9 @@ export default (def: any, type: any, func: string, cacheFlag: string, base: any, ngMocksUniverse.cacheDeclarations.set(def, mock); } + if (!hasNgMocksDepsResolution) { + ngMocksUniverse.config.delete('ngMocksDepsResolution'); + } + return mock as any; }; diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 724cfe82ef..e2f542a7e1 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -12,9 +12,26 @@ interface Declaration { outputs: string[]; propDecorators: Record; queries: Record; + decorators: Array<'Injectable' | 'Pipe' | 'Directive' | 'Component' | 'NgModule'>; [key: string]: any; } +const pushDecorator = (decorators: string[], decorator: string): void => { + const deleteIndex = decorators.indexOf(decorator); + if (deleteIndex !== -1) { + decorators.splice(deleteIndex, 1); + } + if ( + decorator === 'Injectable' || + decorator === 'Pipe' || + decorator === 'Directive' || + decorator === 'Component' || + decorator === 'NgModule' + ) { + decorators.push(decorator); + } +}; + const getAllKeys = (instance: T): Array => { const props: string[] = []; for (const key of Object.keys(instance)) { @@ -24,18 +41,17 @@ const getAllKeys = (instance: T): Array => { return props as never; }; -const createDeclarations = (parent: Partial): Declaration => { - return { - host: parent.host ? { ...parent.host } : {}, - hostBindings: parent.hostBindings ? [...parent.hostBindings] : [], - hostListeners: parent.hostListeners ? [...parent.hostListeners] : [], - attributes: parent.attributes ? [...parent.attributes] : [], - inputs: parent.inputs ? [...parent.inputs] : [], - outputs: parent.outputs ? [...parent.outputs] : [], - propDecorators: parent.propDecorators ? { ...parent.propDecorators } : {}, - queries: parent.queries ? { ...parent.queries } : {}, - }; -}; +const createDeclarations = (parent: Partial): Declaration => ({ + host: parent.host ? { ...parent.host } : {}, + hostBindings: parent.hostBindings ? [...parent.hostBindings] : [], + hostListeners: parent.hostListeners ? [...parent.hostListeners] : [], + attributes: parent.attributes ? [...parent.attributes] : [], + inputs: parent.inputs ? [...parent.inputs] : [], + outputs: parent.outputs ? [...parent.outputs] : [], + propDecorators: parent.propDecorators ? { ...parent.propDecorators } : {}, + queries: parent.queries ? { ...parent.queries } : {}, + decorators: parent.decorators ? [...parent.decorators] : [], +}); const parseParameters = ( def: { @@ -83,8 +99,8 @@ const parseAnnotations = ( if (!ngMetadataName) { continue; } - declaration[ngMetadataName] = { ...annotation, attributes: declaration.attributes }; + pushDecorator(declaration.decorators, ngMetadataName); } } }; @@ -108,8 +124,8 @@ const parseDecorators = ( if (!ngMetadataName) { continue; } - declaration[ngMetadataName] = decorator.args ? { ...decorator.args[0] } : {}; + pushDecorator(declaration.decorators, ngMetadataName); } } }; @@ -424,4 +440,4 @@ const parse = (def: any): any => { return def.__ngMocksDeclarations; }; -export default ((): ((def: any) => any) => parse)(); +export default ((): ((def: any) => Declaration) => parse)(); diff --git a/tests-e2e/src/ngrx/provide-mock-store.spec.ts b/tests-e2e/src/ngrx/provide-mock-store.spec.ts index d1b47b151e..f4220f838d 100644 --- a/tests-e2e/src/ngrx/provide-mock-store.spec.ts +++ b/tests-e2e/src/ngrx/provide-mock-store.spec.ts @@ -1,3 +1,6 @@ +import { CommonModule } from '@angular/common'; +import { Component, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { createAction, createFeatureSelector, @@ -7,12 +10,9 @@ import { Store, StoreModule, } from '@ngrx/store'; -import { Component, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TestBed } from '@angular/core/testing'; +import { provideMockStore } from '@ngrx/store/testing'; import { MockBuilder, MockRenderFactory, ngMocks } from 'ng-mocks'; import { first, tap } from 'rxjs'; -import { provideMockStore } from '@ngrx/store/testing'; const setValue = createAction( 'set-value', diff --git a/tests-e2e/src/ngxs/test.spec.ts b/tests-e2e/src/ngxs/test.spec.ts index 3808da1dcb..103d970d36 100644 --- a/tests-e2e/src/ngxs/test.spec.ts +++ b/tests-e2e/src/ngxs/test.spec.ts @@ -1,7 +1,6 @@ -import { Component, Injectable, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { Component, Injectable, NgModule } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { MockBuilder, MockRenderFactory, ngMocks } from 'ng-mocks'; import { Action, NgxsModule, @@ -9,6 +8,7 @@ import { StateContext, Store, } from '@ngxs/store'; +import { MockBuilder, MockRenderFactory, ngMocks } from 'ng-mocks'; import { first, tap } from 'rxjs'; class SetValue { diff --git a/tests/issue-2687/legacy.spec.ts b/tests/issue-2687/legacy.spec.ts new file mode 100644 index 0000000000..df0d4edff2 --- /dev/null +++ b/tests/issue-2687/legacy.spec.ts @@ -0,0 +1,236 @@ +import { + Component, + Injectable, + NgModule, + Pipe, + PipeTransform, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { MockComponent, ngMocks } from 'ng-mocks'; + +@Injectable() +class StandaloneService {} + +@NgModule({ + providers: [StandaloneService], +}) +class StandaloneModule {} + +@Pipe( + { + name: 'standalone', + standalone: true, + } as never /* TODO: remove after upgrade to a14 */, +) +class StandalonePipe implements PipeTransform { + transform(): string { + return this.constructor.name; + } +} + +@Component( + { + selector: 'standalone', + template: 'service:{{ service.constructor.name }}', + standalone: true, + imports: [StandaloneModule, StandalonePipe], + } as never /* TODO: remove after upgrade to a14 */, +) +class StandaloneComponent { + constructor(public readonly service: StandaloneService) {} +} + +@Component( + { + selector: 'target', + template: + ' pipe:{{ null | standalone }}', + standalone: true, + imports: [StandaloneComponent, StandalonePipe], + } as never /* TODO: remove after upgrade to a14 */, +) +class TargetComponent {} + +@Component({ + selector: 'render-standalone-component', + template: '', +}) +class RenderStandaloneComponentComponent {} + +@Component({ + selector: 'render-standalone-pipe', + template: '{{ null | standalone }}', +}) +class RenderStandalonePipeComponent {} + +@Component({ + selector: 'render-standalone-service', + template: '', +}) +class RenderStandaloneServiceComponent { + constructor(public readonly service: StandaloneService) {} +} + +@Component({ + selector: 'render-target-component', + template: '', +}) +class RenderTargetComponentComponent {} + +describe('issue-2687', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs a14', () => { + // pending('Need Angular > 5'); + expect(true).toBeTruthy(); + }); + + return; + } + + describe('legacy:real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetComponent, StandaloneComponent], + declarations: [ + RenderStandaloneComponentComponent, + RenderStandalonePipeComponent, + RenderStandaloneServiceComponent, + RenderTargetComponentComponent, + ], + }).compileComponents(), + ); + + it('renders StandaloneComponent', () => { + const fixture = TestBed.createComponent( + RenderStandaloneComponentComponent, + ); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + 'service:StandaloneService', + ); + + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandaloneService), + ).not.toThrow(); + }); + + it('renders StandalonePipe', () => { + expect(() => + TestBed.createComponent(RenderStandalonePipeComponent), + ).toThrowError(/The pipe 'standalone' could not be found/); + }); + + it('renders StandaloneService', () => { + const fixture = TestBed.createComponent( + RenderStandaloneServiceComponent, + ); + fixture.detectChanges(); + expect( + fixture.componentInstance.service.constructor.name, + ).toEqual('StandaloneService'); + }); + + it('renders TargetComponent', () => { + const fixture = TestBed.createComponent( + RenderTargetComponentComponent, + ); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + 'service:StandaloneService pipe:StandalonePipe', + ); + + expect(() => + ngMocks.findInstance(TargetComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandalonePipe), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandaloneService), + ).not.toThrow(); + }); + }); + + describe('legacy:mock', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ + MockComponent(TargetComponent), + MockComponent(StandaloneComponent), + ], + declarations: [ + RenderStandaloneComponentComponent, + RenderStandalonePipeComponent, + RenderStandaloneServiceComponent, + RenderTargetComponentComponent, + ], + }).compileComponents(), + ); + + it('renders StandaloneComponent', () => { + const fixture = TestBed.createComponent( + RenderStandaloneComponentComponent, + ); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + '', + ); + + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandaloneService), + ).not.toThrow(); + }); + + it('renders StandalonePipe', () => { + expect(() => + TestBed.createComponent(RenderStandalonePipeComponent), + ).toThrowError(/The pipe 'standalone' could not be found/); + }); + + it('renders StandaloneService', () => { + const fixture = TestBed.createComponent( + RenderStandaloneServiceComponent, + ); + fixture.detectChanges(); + expect( + fixture.componentInstance.service.constructor.name, + ).toEqual('StandaloneService'); + }); + + it('renders TargetComponent', () => { + const fixture = TestBed.createComponent( + RenderTargetComponentComponent, + ); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + '', + ); + + expect(() => + ngMocks.findInstance(TargetComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).toThrowError( + 'Cannot find an instance via ngMocks.findInstance(StandaloneComponent)', + ); + expect(() => ngMocks.findInstance(StandalonePipe)).toThrowError( + 'Cannot find an instance via ngMocks.findInstance(StandalonePipe)', + ); + expect(() => + ngMocks.findInstance(StandaloneService), + ).not.toThrow(); + }); + }); +}); diff --git a/tests/issue-2687/test.spec.ts b/tests/issue-2687/test.spec.ts new file mode 100644 index 0000000000..f47d8ccc14 --- /dev/null +++ b/tests/issue-2687/test.spec.ts @@ -0,0 +1,203 @@ +import { + Component, + Injectable, + NgModule, + Pipe, + PipeTransform, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { + isMockOf, + MockBuilder, + MockComponent, + MockPipe, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Injectable() +class StandaloneService {} + +@NgModule({ + providers: [StandaloneService], +}) +class StandaloneModule {} + +@Pipe( + { + name: 'standalone', + standalone: true, + } as never /* TODO: remove after upgrade to a14 */, +) +class StandalonePipe implements PipeTransform { + transform(): string { + return this.constructor.name; + } +} + +@Component( + { + selector: 'standalone', + template: 'service:{{ service.constructor.name }}', + standalone: true, + imports: [StandaloneModule, StandalonePipe], + } as never /* TODO: remove after upgrade to a14 */, +) +class StandaloneComponent { + constructor(public readonly service: StandaloneService) {} +} + +@Component( + { + selector: 'empty', + template: 'empty', + standalone: true, + imports: [], // this is the thing we assert: an empty imports array + } as never /* TODO: remove after upgrade to a14 */, +) +class EmptyComponent {} + +@Component( + { + selector: 'target', + template: + ' pipe:{{ null | standalone }}', + standalone: true, + imports: [StandaloneComponent, StandalonePipe, EmptyComponent], + } as never /* TODO: remove after upgrade to a14 */, +) +class TargetComponent {} + +describe('issue-2687', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs a14', () => { + // pending('Need Angular > 5'); + expect(true).toBeTruthy(); + }); + + return; + } + + describe('real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetComponent, StandaloneComponent], + }).compileComponents(), + ); + + it('renders StandaloneComponent', () => { + const fixture = TestBed.createComponent(StandaloneComponent); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + 'service:StandaloneService', + ); + }); + + it('renders TargetComponent', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + 'service:StandaloneService pipe:StandalonePipe', + ); + }); + }); + + describe('override', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetComponent, StandaloneComponent], + }).compileComponents(), + ); + + beforeEach(() => { + TestBed.overrideComponent(TargetComponent, { + set: { + imports: [ + MockComponent(StandaloneComponent), + MockPipe(StandalonePipe), + ], + } as never /* TODO: remove after upgrade to a14 */, + }); + }); + + afterAll(() => { + TestBed.overrideComponent(TargetComponent, { + set: { + imports: [StandaloneComponent, StandalonePipe], + } as never /* TODO: remove after upgrade to a14 */, + }); + }); + + it('renders TargetComponent', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + expect(ngMocks.formatHtml(fixture)).toEqual( + ' pipe:', + ); + + const standaloneComponent = ngMocks.findInstance( + fixture, + StandaloneComponent, + ); + expect( + isMockOf(standaloneComponent, StandaloneComponent), + ).toEqual(true); + + const standalonePipe = ngMocks.findInstance( + fixture, + StandalonePipe, + ); + expect(isMockOf(standalonePipe, StandalonePipe)).toEqual(true); + }); + }); + + describe('.mock', () => { + beforeEach(() => MockBuilder(TargetComponent)); + + it('renders TargetComponent', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatHtml(fixture)).toEqual( + ' pipe:', + ); + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandalonePipe), + ).not.toThrow(); + }); + }); + + describe('.keep', () => { + beforeEach(() => + MockBuilder(TargetComponent).keep(StandalonePipe), + ); + + it('renders TargetComponent', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatHtml(fixture)).toEqual( + ' pipe:StandalonePipe', + ); + expect(() => + ngMocks.findInstance(StandaloneComponent), + ).not.toThrow(); + expect(() => + ngMocks.findInstance(StandalonePipe), + ).not.toThrow(); + }); + }); + + describe('StandaloneComponent:exclude', () => { + beforeEach(() => + MockBuilder(TargetComponent).exclude(StandalonePipe), + ); + + it('renders TargetComponent', () => { + expect(() => MockRender(TargetComponent)).toThrowError( + /The pipe 'standalone' could not be found/, + ); + }); + }); +}); diff --git a/tests/ng-mocks-get/test.spec.ts b/tests/ng-mocks-get/test.spec.ts new file mode 100644 index 0000000000..ff2c86ab14 --- /dev/null +++ b/tests/ng-mocks-get/test.spec.ts @@ -0,0 +1,34 @@ +import { Component, Injectable, NgModule } from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Injectable() +class RootService {} + +@Injectable() +class ComponentService {} + +@Component({ + selector: 'target', + template: 'target', + providers: [ComponentService], +}) +class TargetComponent {} + +@NgModule({ + declarations: [TargetComponent], + providers: [RootService], +}) +class TargetModule {} + +describe('ng-mocks-get', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('gets services', () => { + MockRender(TargetComponent); + expect(() => ngMocks.get(RootService)).not.toThrow(); + expect(() => ngMocks.get(ComponentService)).toThrowError( + 'Cannot find an instance via ngMocks.get(ComponentService)', + ); + }); +});