From 71f0c445f3a2061f1eafac52809ec05ec5a9e33c Mon Sep 17 00:00:00 2001 From: ersimont <8042088+ersimont@users.noreply.github.com> Date: Thu, 11 Nov 2021 20:48:06 -0500 Subject: [PATCH] feat(ng-dev): `AngularContext` now uses `MockErrorHandler`. BREAKING CHANGE: Tests that cause a call to `ErrorHandler.handleError` will now fail. Expect the errors with something like `ctx.inject(MockErrorHandler).expectOne('error message')`. --- projects/micro-dash/src/lib/array/pull-all.ts | 1 - .../angular-context/angular-context.spec.ts | 59 ++++++++++++++++++- .../lib/angular-context/angular-context.ts | 20 ++++++- .../component-context.spec.ts | 9 --- .../ng-dev/src/lib/mock-error-handler.spec.ts | 4 +- projects/ng-dev/src/lib/mock-error-handler.ts | 6 +- 6 files changed, 80 insertions(+), 19 deletions(-) diff --git a/projects/micro-dash/src/lib/array/pull-all.ts b/projects/micro-dash/src/lib/array/pull-all.ts index 4f3befe1..82d759a8 100644 --- a/projects/micro-dash/src/lib/array/pull-all.ts +++ b/projects/micro-dash/src/lib/array/pull-all.ts @@ -15,4 +15,3 @@ export function pullAll(array: T[], values: T[]): T[] { } return array; } -// TODO: return type could be narrower? diff --git a/projects/ng-dev/src/lib/angular-context/angular-context.spec.ts b/projects/ng-dev/src/lib/angular-context/angular-context.spec.ts index 8e98a203..e902997a 100644 --- a/projects/ng-dev/src/lib/angular-context/angular-context.spec.ts +++ b/projects/ng-dev/src/lib/angular-context/angular-context.spec.ts @@ -6,6 +6,7 @@ import { Component, ComponentFactoryResolver, DoCheck, + ErrorHandler, Injectable, InjectionToken, Injector, @@ -13,10 +14,17 @@ import { import { flush, TestBed, tick } from '@angular/core/testing'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarHarness } from '@angular/material/snack-bar/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + ANIMATION_MODULE_TYPE, + BrowserAnimationsModule, + NoopAnimationsModule, +} from '@angular/platform-browser/animations'; import { sleep } from '@s-libs/js-core'; import { noop, Observable } from 'rxjs'; +import { ComponentContext } from '../component-context'; +import { MockErrorHandler } from '../mock-error-handler'; import { AngularContext } from './angular-context'; +import { FakeAsyncHarnessEnvironment } from './fake-async-harness-environment'; describe('AngularContext', () => { class SnackBarContext extends AngularContext { @@ -81,6 +89,13 @@ describe('AngularContext', () => { }); }); + it('sets up MockErrorHandler', () => { + const ctx = new AngularContext(); + ctx.run(() => { + expect(ctx.inject(ErrorHandler)).toEqual(jasmine.any(MockErrorHandler)); + }); + }); + it('gives a nice error message if trying to use 2 at the same time', () => { new AngularContext().run(async () => { expect(() => { @@ -293,6 +308,25 @@ describe('AngularContext', () => { 'Expected no open requests, found 1: GET an unexpected URL', ); }); + + it('errs if there are unexpected errors', () => { + @Component({ template: '' }) + class ThrowingComponent { + throwError(): never { + throw new Error(); + } + } + + const ctx = new ComponentContext(ThrowingComponent); + expect(() => { + ctx.run(async () => { + // TODO: make something like `ctx.getTestElement()`? + const loader = FakeAsyncHarnessEnvironment.documentRootLoader(ctx); + const button = await loader.locatorFor('button')(); + await button.click(); + }); + }).toThrowError('Expected no error(s), found 1'); + }); }); describe('.cleanUp()', () => { @@ -358,3 +392,26 @@ describe('AngularContext class-level doc example', () => { }); }); }); + +describe('extendMetadata', () => { + it('allows animations to be unconditionally disabled', () => { + @Component({ template: '' }) + class BlankComponent {} + const ctx = new ComponentContext(BlankComponent, { + imports: [BrowserAnimationsModule], + }); + ctx.run(() => { + expect(ctx.inject(ANIMATION_MODULE_TYPE)).toBe('NoopAnimations'); + }); + }); + + it('allows the user to override providers', () => { + const errorHandler = { handleError: noop }; + const ctx = new AngularContext({ + providers: [{ provide: ErrorHandler, useValue: errorHandler }], + }); + ctx.run(() => { + expect(ctx.inject(ErrorHandler)).toBe(errorHandler); + }); + }); +}); diff --git a/projects/ng-dev/src/lib/angular-context/angular-context.ts b/projects/ng-dev/src/lib/angular-context/angular-context.ts index b5383dc1..767239d2 100644 --- a/projects/ng-dev/src/lib/angular-context/angular-context.ts +++ b/projects/ng-dev/src/lib/angular-context/angular-context.ts @@ -20,6 +20,7 @@ import { } from '@angular/core/testing'; import { assert, convertTime } from '@s-libs/js-core'; import { clone, forOwn } from '@s-libs/micro-dash'; +import { MockErrorHandler } from '../mock-error-handler'; import { FakeAsyncHarnessEnvironment } from './fake-async-harness-environment'; export function extendMetadata( @@ -28,7 +29,16 @@ export function extendMetadata( ): TestModuleMetadata { const result: any = clone(metadata); forOwn(toAdd, (val, key) => { - result[key] = Array.isArray(result[key]) ? result[key].concat(val) : val; + const existing = result[key]; + if (!existing) { + result[key] = val; + } else if (key === 'imports') { + // to allow ComponentContext to unconditionally disable animations, added imports override previous imports + result[key] = [result[key], val]; + } else { + // but for most things we want to let what comes in from subclasses and users win + result[key] = [val, result[key]]; + } }); return result; } @@ -112,7 +122,10 @@ export class AngularContext { ); AngularContext.#current = this; TestBed.configureTestingModule( - extendMetadata(moduleMetadata, { imports: [HttpClientTestingModule] }), + extendMetadata(moduleMetadata, { + imports: [HttpClientTestingModule], + providers: [MockErrorHandler.overrideProvider()], + }), ); } @@ -193,10 +206,11 @@ export class AngularContext { } /** - * Runs post-test verifications. This base implementation runs [HttpTestingController#verify]{@linkcode https://angular.io/api/common/http/testing/HttpTestingController#verify}. Unlike {@link #cleanUp}, it is OK for this method to throw an error to indicate a violation. + * Runs post-test verifications. This base implementation runs [HttpTestingController.verify]{@linkcode https://angular.io/api/common/http/testing/HttpTestingController#verify} and {@linkcode MockErrorHandler.verify}. Unlike {@linkcode #cleanUp}, it is OK for this method to throw an error to indicate a violation. */ protected verifyPostTestConditions(): void { this.inject(HttpTestingController).verify(); + this.inject(MockErrorHandler).verify(); } /** diff --git a/projects/ng-dev/src/lib/component-context/component-context.spec.ts b/projects/ng-dev/src/lib/component-context/component-context.spec.ts index 13995f66..9bf77e0c 100644 --- a/projects/ng-dev/src/lib/component-context/component-context.spec.ts +++ b/projects/ng-dev/src/lib/component-context/component-context.spec.ts @@ -65,15 +65,6 @@ describe('ComponentContext', () => { expect(ctx.inject(ANIMATION_MODULE_TYPE)).toBe('NoopAnimations'); }); }); - - it('allows default module metadata to be overridden', () => { - const ctx = new ComponentContext(TestComponent, { - imports: [BrowserAnimationsModule], - }); - ctx.run(() => { - expect(ctx.inject(ANIMATION_MODULE_TYPE)).toBe('NoopAnimations'); - }); - }); }); describe('.assignInputs()', () => { diff --git a/projects/ng-dev/src/lib/mock-error-handler.spec.ts b/projects/ng-dev/src/lib/mock-error-handler.spec.ts index 421df4db..aa094196 100644 --- a/projects/ng-dev/src/lib/mock-error-handler.spec.ts +++ b/projects/ng-dev/src/lib/mock-error-handler.spec.ts @@ -10,10 +10,10 @@ describe('MockErrorHandler', () => { consoleSpy = spyOn(console, 'error'); }); - describe('createProvider()', () => { + describe('overrideProvider()', () => { it('makes MockErrorHandler the ErrorHandler', () => { TestBed.configureTestingModule({ - providers: [MockErrorHandler.createProvider()], + providers: [MockErrorHandler.overrideProvider()], }); expect(TestBed.inject(ErrorHandler)).toBe( TestBed.inject(MockErrorHandler), diff --git a/projects/ng-dev/src/lib/mock-error-handler.ts b/projects/ng-dev/src/lib/mock-error-handler.ts index 7f8e747f..cde0efb5 100644 --- a/projects/ng-dev/src/lib/mock-error-handler.ts +++ b/projects/ng-dev/src/lib/mock-error-handler.ts @@ -24,15 +24,15 @@ type Match = string | RegExp | ((error: ErrorType) => boolean); @Injectable({ providedIn: 'root' }) export class MockErrorHandler extends ErrorHandler { /** - * Convenience method to put in a `provide` array, to override Angular's default error handler. Note that this is provided automatically by {@linkcode AngularContext}. + * Convenience method to put in a `provide` array, to override Angular's default error handler. You do not need to use this if you are using {@linkcode AngularContext}, which automatically provides it. * * ```ts * TestBed.configureTestingModule({ - * providers: [MockErrorHandler.createProvider()], + * providers: [MockErrorHandler.overrideProvider()], * }); * ``` */ - static createProvider(): Provider { + static overrideProvider(): Provider { return { provide: ErrorHandler, useExisting: MockErrorHandler }; }