diff --git a/README.md b/README.md index 7287075f4f..ba0c0050fa 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ ngMocks.defaultMock(AuthService, () => ({ An example of a spec for a profile edit component. -```ts title="src/form.component.spec.ts" +```ts title="src/profile.component.spec.ts" // Let's imagine that there is a ProfileComponent // and it has 3 text fields: email, firstName, // lastName, and a user can edit them. @@ -77,24 +77,45 @@ describe('profile', () => { // https://ng-mocks.sudo.eu/api/ngMocks/faster ngMocks.faster(); - // Now we would like to configure TestBed that - // ProfileComponent would stay as it is, and - // all its dependencies would be mocks. - // Even more, if a dependency is missing, - // we would like to get a failing test. - // Also, we would like to rely on reactive forms, - // therefore we would like to avoid its mocking. + // Let's declare TestBed in beforeAll + // instead of beforeEach. + // The code mocks everything in SharedModule + // and provides a mock AuthService. beforeAll(() => { - // The result of MockBuilder should be returned. - // https://ng-mocks.sudo.eu/api/MockBuilder - return MockBuilder( - ProfileComponent, - ProfileModule, - ).keep(ReactiveFormsModule); + return TestBed.configureTestingModule({ + imports: [ + MockModule(SharedModule), // mock + ReactiveFormsModule, // real + ], + declarations: [ + MockComponent(AvatarComponent), // mock + ProfileComponent, // real + ], + providers: [ + MockProvider(AuthService), // mock + ], + }).compileComponents(); }); - // A test to ensure that ProfileModule imports - // and declares all the dependencies. + // A test to ensure that ProfileComponent + // can be created. + it('should be created', () => { + // MockRender is an advanced version of + // TestBed.createComponent. + // It respects all lifecycle hooks, + // onPush change detection, and creates a + // wrapper component with a template like + // + // https://ng-mocks.sudo.eu/api/MockRender + const fixture = MockRender(ProfileComponent); + + expect( + fixture.point.componentInstance, + ).toEqual(jasmine.any(ProfileComponent)); + }); + + // A test to ensure that the component listens + // on ctrl+s hotkey. it('should be created', () => { // MockRender respects all lifecycle hooks, // onPush change detection, and creates a diff --git a/docs/articles/api/ngMocks/faster.md b/docs/articles/api/ngMocks/faster.md index 32d281a5fb..fdb79f3fac 100644 --- a/docs/articles/api/ngMocks/faster.md +++ b/docs/articles/api/ngMocks/faster.md @@ -7,7 +7,8 @@ There is a `ngMocks.faster` feature that optimizes setup of similar test modules and reduces required time on their execution. Imagine a situation when `beforeEach` creates the same setup used by dozens of `it`. -This is the case where `ngMocks.faster` might be useful, simply call it before `beforeAll` and +This is the case where `ngMocks.faster` might be useful, simply use `beforeAll` instead of `beforeEach`, +call `ngMocks.faster` before `beforeAll` and **the Angular tests will run faster**. ```ts diff --git a/docs/articles/index.md b/docs/articles/index.md index d92f1f7156..2f8ef759bc 100644 --- a/docs/articles/index.md +++ b/docs/articles/index.md @@ -64,7 +64,7 @@ ngMocks.defaultMock(AuthService, () => ({ An example of a spec for a profile edit component. -```ts title="src/form.component.spec.ts" +```ts title="src/profile.component.spec.ts" // Let's imagine that there is a ProfileComponent // and it has 3 text fields: email, firstName, // lastName, and a user can edit them. @@ -79,24 +79,45 @@ describe('profile', () => { // https://ng-mocks.sudo.eu/api/ngMocks/faster ngMocks.faster(); - // Now we would like to configure TestBed that - // ProfileComponent would stay as it is, and - // all its dependencies would be mocks. - // Even more, if a dependency is missing, - // we would like to get a failing test. - // Also, we would like to rely on reactive forms, - // therefore we would like to avoid its mocking. + // Let's declare TestBed in beforeAll + // instead of beforeEach. + // The code mocks everything in SharedModule + // and provides a mock AuthService. beforeAll(() => { - // The result of MockBuilder should be returned. - // https://ng-mocks.sudo.eu/api/MockBuilder - return MockBuilder( - ProfileComponent, - ProfileModule, - ).keep(ReactiveFormsModule); + return TestBed.configureTestingModule({ + imports: [ + MockModule(SharedModule), // mock + ReactiveFormsModule, // real + ], + declarations: [ + MockComponent(AvatarComponent), // mock + ProfileComponent, // real + ], + providers: [ + MockProvider(AuthService), // mock + ], + }).compileComponents(); }); - // A test to ensure that ProfileModule imports - // and declares all the dependencies. + // A test to ensure that ProfileComponent + // can be created. + it('should be created', () => { + // MockRender is an advanced version of + // TestBed.createComponent. + // It respects all lifecycle hooks, + // onPush change detection, and creates a + // wrapper component with a template like + // + // https://ng-mocks.sudo.eu/api/MockRender + const fixture = MockRender(ProfileComponent); + + expect(fixture.point.componentInstance).toEqual( + jasmine.any(ProfileComponent), + ); + }); + + // A test to ensure that the component listens + // on ctrl+s hotkey. it('should be created', () => { // MockRender respects all lifecycle hooks, // onPush change detection, and creates a diff --git a/docs/package-lock.json b/docs/package-lock.json index 58ec658a71..304d2f4c38 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2603,6 +2603,15 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4337,6 +4346,12 @@ "schema-utils": "^3.0.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "filesize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", @@ -6443,6 +6458,12 @@ "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -10021,7 +10042,11 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/e2e/a-min/package-lock.json b/e2e/a-min/package-lock.json index 8be48b935c..29782c435c 100644 --- a/e2e/a-min/package-lock.json +++ b/e2e/a-min/package-lock.json @@ -504,6 +504,14 @@ "source-map": "0.7.3", "tslib": "2.3.0", "typescript": "4.3.3" + }, + "dependencies": { + "typescript": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.3.tgz", + "integrity": "sha512-rUvLW0WtF7PF2b9yenwWUi9Da9euvDRhmH7BLyBG4DCFfOJ850LGNknmRpp8Z8kXNUPObdZQEfKOiHtXuQHHKA==", + "dev": true + } } }, "@angular-devkit/build-webpack": { @@ -6364,6 +6372,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -8815,6 +8833,13 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -12988,6 +13013,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", "dev": true, + "optional": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -12998,13 +13024,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "dev": true, + "optional": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "optional": true, "requires": { "has-flag": "^4.0.0" } @@ -14074,6 +14102,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -18246,6 +18281,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.2.tgz", "integrity": "sha512-6QhDaAiVHIQr5Ab3XUWZyDmrIPCHMiqJVljMF91YKyqwKkL5QHnYMkrMBy96v9Z7ev1hGhSEw1HQZc2p/s5Z8Q==", "dev": true, + "optional": true, "requires": { "jest-worker": "^26.6.2", "p-limit": "^3.1.0", @@ -18260,6 +18296,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "optional": true, "requires": { "yocto-queue": "^0.1.0" } @@ -18269,6 +18306,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, + "optional": true, "requires": { "@types/json-schema": "^7.0.6", "ajv": "^6.12.5", @@ -18279,7 +18317,8 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "dev": true, + "optional": true } } }, @@ -18853,6 +18892,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.38.1.tgz", "integrity": "sha512-OqRmYD1OJbHZph6RUMD93GcCZy4Z4wC0ele4FXyYF0J6AxO1vOSuIlU1hkS/lDlR9CDYBz64MZRmdbdnFFoT2g==", "dev": true, + "optional": true, "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.47", @@ -18884,6 +18924,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, + "optional": true, "requires": { "@types/json-schema": "^7.0.6", "ajv": "^6.12.5", @@ -18894,13 +18935,15 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "dev": true, + "optional": true }, "webpack-sources": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz", "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==", "dev": true, + "optional": true, "requires": { "source-list-map": "^2.0.1", "source-map": "^0.6.1" @@ -19101,7 +19144,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/e2e/a12/package-lock.json b/e2e/a12/package-lock.json index 55a984934f..f21639ce8c 100644 --- a/e2e/a12/package-lock.json +++ b/e2e/a12/package-lock.json @@ -449,6 +449,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true + }, + "typescript": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.3.tgz", + "integrity": "sha512-rUvLW0WtF7PF2b9yenwWUi9Da9euvDRhmH7BLyBG4DCFfOJ850LGNknmRpp8Z8kXNUPObdZQEfKOiHtXuQHHKA==", + "dev": true } } }, @@ -5717,6 +5723,16 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -8120,6 +8136,13 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -11934,6 +11957,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -17370,7 +17400,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/examples/readme/test.spec.ts b/examples/readme/builder.spec.ts similarity index 99% rename from examples/readme/test.spec.ts rename to examples/readme/builder.spec.ts index a7d4103439..f9debe59f8 100644 --- a/examples/readme/test.spec.ts +++ b/examples/readme/builder.spec.ts @@ -92,7 +92,7 @@ ngMocks.defaultMock(AuthService, () => ({ // lastName, and a user can edit them. // In the following test suite, we would like to // cover behavior of the component. -describe('profile', () => { +describe('profile:builder', () => { // First of all, we want to avoid creation of // the same TestBed for every test, because it // is not going to be changed from test to test. diff --git a/examples/readme/classic.spec.ts b/examples/readme/classic.spec.ts new file mode 100644 index 0000000000..b220507b0d --- /dev/null +++ b/examples/readme/classic.spec.ts @@ -0,0 +1,212 @@ +// tslint:disable object-literal-sort-keys + +import { + Component, + HostListener, + Injectable, + Input, + NgModule, + OnInit, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + MockInstance, + MockModule, + MockProvider, + MockRender, + ngMocks, +} from 'ng-mocks'; +import { Subject } from 'rxjs'; + +// remove with A5 +export const EMPTY = new Subject(); +EMPTY.complete(); + +interface User { + email: string; + firstName: string; + lastName: string; +} + +@Injectable() +class AuthService { + public readonly currentUser?: User; + public readonly isLoggedIn$ = new Subject(); +} + +@Injectable() +class StorageService { + public user?: User; + + public save(user: User): void { + this.user = user; + } +} + +@Component({ + selector: 'profile', + template: `
+ + + +
`, +}) +class ProfileComponent implements OnInit { + public readonly form = new FormGroup({ + email: new FormControl(null, Validators.required), + firstName: new FormControl(null, Validators.required), + lastName: new FormControl(null, Validators.required), + }); + + @Input() public readonly profile: User | null = null; + + public constructor(private readonly storage: StorageService) {} + + public ngOnInit(): void { + if (this.profile) { + this.form.setValue(this.profile); + } + } + + @HostListener('keyup.control.s') + public save(): void { + this.storage.save(this.form.value); + } +} + +@NgModule({ + declarations: [], + providers: [AuthService, StorageService], +}) +class SharedModule {} + +ngMocks.defaultMock(AuthService, () => ({ + isLoggedIn$: EMPTY, +})); + +// Let's imagine that there is a ProfileComponent +// and it has 3 text fields: email, firstName, +// lastName, and a user can edit them. +// In the following test suite, we would like to +// cover behavior of the component. +describe('profile:classic', () => { + // First of all, we would like to reuse the same + // TestBed in every test. + // ngMocks.faster suppresses reset of TestBed + // after each test and allows to use TestBed, + // MockBuilder and MockRender in beforeAll. + // https://ng-mocks.sudo.eu/api/ngMocks/faster + ngMocks.faster(); + + // Let's declare TestBed in beforeAll instead of beforeEach. + // The code mocks everything in SharedModule and provides a mock AuthService. + beforeAll(async () => { + return TestBed.configureTestingModule({ + imports: [ + MockModule(SharedModule), // mock + ReactiveFormsModule, // real + ], + declarations: [ + ProfileComponent, // real + ], + providers: [ + MockProvider(AuthService), // mock + ], + }).compileComponents(); + }); + + // A test to ensure that ProfileComponent can be created. + it('should be created', () => { + // MockRender is an advanced version of TestBed.createComponent. + // It respects all lifecycle hooks, + // onPush change detection, and creates a + // wrapper component with a template like + // + // https://ng-mocks.sudo.eu/api/MockRender + const fixture = MockRender(ProfileComponent); + + expect(fixture.point.componentInstance).toEqual( + jasmine.any(ProfileComponent), + ); + }); + + // A test to ensure that the component listens + // on ctrl+s hotkey. + it('should be created', () => { + // MockRender respects all lifecycle hooks, + // onPush change detection, and creates a + // wrapper component with a template like + // + // https://ng-mocks.sudo.eu/api/MockRender + const fixture = MockRender(ProfileComponent); + + expect(fixture.point.componentInstance).toEqual( + jasmine.any(ProfileComponent), + ); + }); + + // A test to ensure that the component listens + // on ctrl+s hotkey. + it('saves on ctrl+s hot key', () => { + // A fake profile. + const profile = { + email: 'test2@email.com', + firstName: 'testFirst2', + lastName: 'testLast2', + }; + + // A spy to track save calls. + // MockInstance helps to configure mock + // providers, declarations and modules + // before their initialization and usage. + // https://ng-mocks.sudo.eu/api/MockInstance + const spySave = MockInstance( + StorageService, + 'save', + jasmine.createSpy('StorageService.save'), + ); + + // Renders + // . + // https://ng-mocks.sudo.eu/api/MockRender + const { point } = MockRender( + ProfileComponent, + { profile }, // bindings + ); + + // Let's change the value of the form control + // for email addresses with a random value. + // ngMocks.change finds a related control + // value accessor and updates it properly. + // https://ng-mocks.sudo.eu/api/ngMocks/change + ngMocks.change( + '[name=email]', // css selector + 'test3@em.ail', // an email address + ); + + // Let's ensure that nothing has been called. + expect(spySave).not.toHaveBeenCalled(); + + // Let's assume that there is a host listener + // for a keyboard combination of ctrl+s, + // and we want to trigger it. + // ngMocks.trigger helps to emit events via + // simple interface. + // https://ng-mocks.sudo.eu/api/ngMocks/trigger + ngMocks.trigger(point, 'keyup.control.s'); + + // The spy should be called with the user + // and the random email address. + expect(spySave).toHaveBeenCalledWith({ + email: 'test3@em.ail', + firstName: profile.firstName, + lastName: profile.lastName, + }); + }); +}); 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 3a7af48c0d..c9711ca7f5 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts @@ -2,12 +2,14 @@ import { InjectionToken, NgModule } from '@angular/core'; import { getTestBed, MetadataOverride, TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; import coreConfig from '../common/core.config'; +import coreDefineProperty from '../common/core.define-property'; import { flatten, mapEntries } from '../common/core.helpers'; import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; import { AnyType, Type } from '../common/core.types'; import { isNgDef } from '../common/func.is-ng-def'; import ngMocksUniverse from '../common/ng-mocks-universe'; import { ngMocks } from '../mock-helper/mock-helper'; +import mockHelperFasterInstall from '../mock-helper/mock-helper.faster-install'; import funcExtractTokens from './func.extract-tokens'; import { MockBuilderPerformance } from './mock-builder.performance'; @@ -135,18 +137,17 @@ const applyNgMocksOverrides = (testBed: TestBedStatic & { ngMocksOverrides?: any const initTestBed = () => { if (!(TestBed as any).ngMocksSelectors) { - (TestBed as any).ngMocksSelectors = new Map(); + coreDefineProperty(TestBed, 'ngMocksSelectors', new Map()); } // istanbul ignore else if (!(TestBed as any).ngMocksOverrides) { - (TestBed as any).ngMocksOverrides = new Set(); + coreDefineProperty(TestBed, 'ngMocksOverrides', new Set()); } }; const configureTestingModule = (original: TestBedStatic['configureTestingModule']): TestBedStatic['configureTestingModule'] => (moduleDef: TestModuleMetadata) => { - ngMocksUniverse.global.set('bullet:customized', true); initTestBed(); const { mocks, overrides } = funcExtractTokens(moduleDef.providers); @@ -174,23 +175,30 @@ const configureTestingModule = const resetTestingModule = (original: TestBedStatic['resetTestingModule']): TestBedStatic['resetTestingModule'] => () => { - if (ngMocksUniverse.global.has('bullet')) { - if (ngMocksUniverse.global.has('bullet:customized')) { - ngMocksUniverse.global.set('bullet:reset', true); - } - - return TestBed; - } ngMocksUniverse.global.delete('builder:config'); ngMocksUniverse.global.delete('builder:module'); - ngMocksUniverse.global.delete('bullet:customized'); - ngMocksUniverse.global.delete('bullet:reset'); (TestBed as any).ngMocksSelectors = undefined; applyNgMocksOverrides(TestBed); return original.call(TestBed); }; +let needInstall = true; +const install = () => { + const hooks = mockHelperFasterInstall(); + if (needInstall) { + // istanbul ignore else + if (hooks.before.indexOf(configureTestingModule) === -1) { + hooks.before.push(configureTestingModule); + } + // istanbul ignore else + if (hooks.after.indexOf(resetTestingModule) === -1) { + hooks.after.push(resetTestingModule); + } + needInstall = false; + } +}; + /** * @see https://ng-mocks.sudo.eu/api/MockBuilder */ @@ -204,11 +212,7 @@ export function MockBuilder( | undefined, itsModuleToMock?: AnyType | Array> | null | undefined, ): IMockBuilder { - if (!(TestBed as any).ngMocks) { - TestBed.configureTestingModule = configureTestingModule(TestBed.configureTestingModule); - TestBed.resetTestingModule = resetTestingModule(TestBed.resetTestingModule); - (TestBed as any).ngMocks = true; - } + install(); const instance = new MockBuilderPerformance(); diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster-install.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster-install.ts new file mode 100644 index 0000000000..df496842e3 --- /dev/null +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster-install.ts @@ -0,0 +1,56 @@ +import { TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; + +import ngMocksUniverse from '../common/ng-mocks-universe'; + +const hooks: { + after: Array<(original: TestBedStatic['resetTestingModule']) => TestBedStatic['resetTestingModule']>; + before: Array<(original: TestBedStatic['configureTestingModule']) => TestBedStatic['configureTestingModule']>; +} = ngMocksUniverse.global.get('faster-hooks') || { + after: [], + before: [], +}; +ngMocksUniverse.global.set('reporter-stack', hooks); + +const configureTestingModule = + (original: TestBedStatic['configureTestingModule']): TestBedStatic['configureTestingModule'] => + (moduleDef: TestModuleMetadata) => { + ngMocksUniverse.global.set('bullet:customized', true); + + let final = original; + for (const callback of hooks.before) { + final = callback(final); + } + + return final.call(TestBed, moduleDef); + }; + +const resetTestingModule = + (original: TestBedStatic['resetTestingModule']): TestBedStatic['resetTestingModule'] => + () => { + if (ngMocksUniverse.global.has('bullet')) { + if (ngMocksUniverse.global.has('bullet:customized')) { + ngMocksUniverse.global.set('bullet:reset', true); + } + + return TestBed; + } + ngMocksUniverse.global.delete('bullet:customized'); + ngMocksUniverse.global.delete('bullet:reset'); + let final = original; + for (const callback of hooks.after) { + final = callback(final); + } + + return final.call(TestBed); + }; + +let needInstall = true; +export default () => { + if (needInstall) { + TestBed.configureTestingModule = configureTestingModule(TestBed.configureTestingModule); + TestBed.resetTestingModule = resetTestingModule(TestBed.resetTestingModule); + needInstall = false; + } + + return hooks; +}; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts index 4749ed7ea9..7e0c583896 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts @@ -3,6 +3,7 @@ import { ComponentFixture, getTestBed, TestBed } from '@angular/core/testing'; import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import mockHelperFasterInstall from './mock-helper.faster-install'; import mockHelperFlushTestBed from './mock-helper.flush-test-bed'; const resetFixtures = (stack: NgMocksStack) => { @@ -28,6 +29,8 @@ export default () => { needInstall = false; } + mockHelperFasterInstall(); + beforeAll(() => { if (ngMocksUniverse.global.has('bullet:customized')) { TestBed.resetTestingModule(); diff --git a/tests/issue-721/before-all.spec.ts b/tests/issue-721/before-all.spec.ts new file mode 100644 index 0000000000..8591c5f282 --- /dev/null +++ b/tests/issue-721/before-all.spec.ts @@ -0,0 +1,37 @@ +import { Component, Inject, InjectionToken } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockProvider, MockRender, ngMocks } from 'ng-mocks'; + +const TOKEN = new InjectionToken('TOKEN'); + +@Component({ + selector: 'target', + template: '{{ value }}', +}) +class TargetComponent { + public constructor(@Inject(TOKEN) public readonly value: number) {} +} + +describe('issue-721:before-all', () => { + ngMocks.faster(); + + let value = 0; + beforeAll(() => + TestBed.configureTestingModule({ + declarations: [TargetComponent], + providers: [MockProvider(TOKEN, (value += 1))], + }).compileComponents(), + ); + + it('works right value=1', () => { + expect(ngMocks.formatText(MockRender(TargetComponent))).toEqual( + '1', + ); + }); + + it('works right value=1', () => { + expect(ngMocks.formatText(MockRender(TargetComponent))).toEqual( + '1', + ); + }); +}); diff --git a/tests/issue-721/before-each.spec.ts b/tests/issue-721/before-each.spec.ts new file mode 100644 index 0000000000..d8d0bca008 --- /dev/null +++ b/tests/issue-721/before-each.spec.ts @@ -0,0 +1,38 @@ +import { Component, Inject, InjectionToken } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockProvider, MockRender, ngMocks } from 'ng-mocks'; + +const TOKEN = new InjectionToken('TOKEN'); + +@Component({ + selector: 'target', + template: '{{ value }}', +}) +class TargetComponent { + public constructor(@Inject(TOKEN) public readonly value: number) {} +} + +describe('issue-721:before-each', () => { + ngMocks.faster(); + + let value = 0; + beforeEach(() => + TestBed.configureTestingModule({ + declarations: [TargetComponent], + providers: [MockProvider(TOKEN, (value += 1))], + }).compileComponents(), + ); + + it('works right value=1', () => { + expect(ngMocks.formatText(MockRender(TargetComponent))).toEqual( + '1', + ); + }); + + // Should it be a better reset? Or an error? + it('works right value=2', () => { + expect(ngMocks.formatText(MockRender(TargetComponent))).toEqual( + '2', + ); + }); +});