Skip to content

Commit

Permalink
feat(ng-dev): AngularContext now uses MockErrorHandler.
Browse files Browse the repository at this point in the history
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')`.
  • Loading branch information
ersimont committed Nov 12, 2021
1 parent d83353e commit 71f0c44
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 19 deletions.
1 change: 0 additions & 1 deletion projects/micro-dash/src/lib/array/pull-all.ts
Expand Up @@ -15,4 +15,3 @@ export function pullAll<T>(array: T[], values: T[]): T[] {
}
return array;
}
// TODO: return type could be narrower?
59 changes: 58 additions & 1 deletion projects/ng-dev/src/lib/angular-context/angular-context.spec.ts
Expand Up @@ -6,17 +6,25 @@ import {
Component,
ComponentFactoryResolver,
DoCheck,
ErrorHandler,
Injectable,
InjectionToken,
Injector,
} from '@angular/core';
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 {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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: '<button (click)="throwError()"></button>' })
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()', () => {
Expand Down Expand Up @@ -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);
});
});
});
20 changes: 17 additions & 3 deletions projects/ng-dev/src/lib/angular-context/angular-context.ts
Expand Up @@ -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(
Expand All @@ -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;
}
Expand Down Expand Up @@ -112,7 +122,10 @@ export class AngularContext {
);
AngularContext.#current = this;
TestBed.configureTestingModule(
extendMetadata(moduleMetadata, { imports: [HttpClientTestingModule] }),
extendMetadata(moduleMetadata, {
imports: [HttpClientTestingModule],
providers: [MockErrorHandler.overrideProvider()],
}),
);
}

Expand Down Expand Up @@ -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();
}

/**
Expand Down
Expand Up @@ -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()', () => {
Expand Down
4 changes: 2 additions & 2 deletions projects/ng-dev/src/lib/mock-error-handler.spec.ts
Expand Up @@ -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),
Expand Down
6 changes: 3 additions & 3 deletions projects/ng-dev/src/lib/mock-error-handler.ts
Expand Up @@ -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 };
}

Expand Down

0 comments on commit 71f0c44

Please sign in to comment.