Skip to content

Commit

Permalink
feat(ng-dev): AsyncMethodController no longer needs the ctx argument
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The behavior for `AsyncMethodController` to automatically trigger promise handlers and change detection is now opt-out instead of opt-in. The `ctx` option for its constructor has been removed. If you are using an `AngularContext` and do _not_ want automatic calls to `.tick()` after each `.flush()` and `.error()`, pass a new option the constructor: `autoTick: false`.
  • Loading branch information
ersimont committed Sep 14, 2021
1 parent 0c0acfc commit 3c2f190
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,7 @@ describe('AsyncMethodController', () => {
const ctx = new AngularContext();

// mock the browser API for pasting
const controller = new AsyncMethodController(clipboard, 'readText', {
ctx,
});
const controller = new AsyncMethodController(clipboard, 'readText');
ctx.run(() => {
// BEGIN production code that copies to the clipboard
let pastedText: string;
Expand Down
10 changes: 4 additions & 6 deletions projects/ng-dev/src/lib/spies/async-method-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ type AsyncMethodKeys<T> = {
* const ctx = new AngularContext();
*
* // mock the browser API for pasting
* const controller = new AsyncMethodController(clipboard, 'readText', {
* ctx,
* });
* const controller = new AsyncMethodController(clipboard, 'readText');
* ctx.run(() => {
* // BEGIN production code that copies to the clipboard
* let pastedText: string;
Expand All @@ -53,18 +51,18 @@ export class AsyncMethodController<
#testCalls: TestCall<WrappingObject[FunctionName]>[] = [];

/**
* Optionally provide `ctx` to automatically trigger promise handlers and changed detection after calling `flush()` or `error()`. This is the normal production behavior of asynchronous browser APIs. However, if the function you are stubbing is not patched by zone.js, change detection would not run automatically, in which case you many not want to pass this parameter. See the list of functions that zone.js patches [here](https://github.com/angular/angular/blob/master/packages/zone.js/STANDARD-APIS.md).
* If you are using an `AngularContext`, the default behavior is to automatically call `.tick()` after each `.flush()` and `.error()` to trigger promise handlers and changed detection. This is the normal production behavior of asynchronous browser APIs. However, if zone.js does not patch the function you are stubbing, change detection would not run automatically. In that case you many want to turn off this behavior by passing the option `autoTick: false`. See the list of functions that zone.js patches [here](https://github.com/angular/angular/blob/master/packages/zone.js/STANDARD-APIS.md).
*/
constructor(
obj: WrappingObject,
methodName: FunctionName,
{ ctx = undefined as { tick(): void } | undefined } = {},
{ autoTick = true } = {},
) {
// Note: it wasn't immediately clear how avoid `any` in this constructor, and this will be invisible to users. So I gave up. (For now?)
this.#spy = spyOn(obj, methodName as any) as any;
this.#spy.and.callFake((() => {
const deferred = new Deferred<any>();
this.#testCalls.push(new TestCall(deferred, ctx));
this.#testCalls.push(new TestCall(deferred, autoTick));
return deferred.promise;
}) as any);
}
Expand Down
37 changes: 33 additions & 4 deletions projects/ng-dev/src/lib/spies/test-call.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ describe('TestCall', () => {
expectSingleCallAndReset(spy, 'the clipboard text');
}));

it('triggers change detection if the AsyncMethodController was passed a context', () => {
it('triggers a tick if appropriate', () => {
const ctx = new ComponentContext(TestComponent);
const controller = new AsyncMethodController(
navigator.clipboard,
'readText',
{ ctx },
);

ctx.run(() => {
Expand Down Expand Up @@ -78,12 +77,11 @@ describe('TestCall', () => {
expectSingleCallAndReset(spy, 'some problem');
}));

it('triggers change detection if the AsyncMethodController was passed a context', () => {
it('triggers a tick if appropriate', () => {
const ctx = new ComponentContext(TestComponent);
const controller = new AsyncMethodController(
navigator.clipboard,
'readText',
{ ctx },
);

ctx.run(() => {
Expand All @@ -96,4 +94,35 @@ describe('TestCall', () => {
});
});
});

describe('.maybeTick()', () => {
it('does not call .tick() when autoTick is false', () => {
const ctx = new ComponentContext(TestComponent);
const controller = new AsyncMethodController(
navigator.clipboard,
'readText',
{ autoTick: false },
);

ctx.run(() => {
navigator.clipboard.readText();
ctx.getComponentInstance().name = 'Spy';
controller.expectOne([]).flush('this is the clipboard content');

expect(ctx.fixture.nativeElement.textContent).not.toContain('Spy');
});
});

it('gracefully handles being run outside an AngularContext', () => {
const controller = new AsyncMethodController(
navigator.clipboard,
'readText',
);

navigator.clipboard.readText();
expect(() => {
controller.expectOne([]).flush('this is the clipboard content');
}).not.toThrowError();
});
});
});
13 changes: 10 additions & 3 deletions projects/ng-dev/src/lib/spies/test-call.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Deferred } from '@s-libs/js-core';
import { PromiseType } from 'utility-types';
import { AngularContext } from '../angular-context';

/**
* A mock method call that was made and is ready to be answered. This interface allows access to the underlying <code>jasmine.CallInfo</code>, and allows resolving or rejecting the asynchronous call's result.
Expand All @@ -10,22 +11,28 @@ export class TestCall<F extends jasmine.Func> {

constructor(
private deferred: Deferred<PromiseType<ReturnType<F>>>,
private ctx?: { tick(): void },
private autoTick: boolean,
) {}

/**
* Resolve the call with the given value.
*/
flush(value: PromiseType<ReturnType<F>>): void {
this.deferred.resolve(value);
this.ctx?.tick();
this.maybeTick();
}

/**
* Reject the call with the given reason.
*/
error(reason: any): void {
this.deferred.reject(reason);
this.ctx?.tick();
this.maybeTick();
}

private maybeTick(): void {
if (this.autoTick) {
AngularContext.getCurrent()?.tick();
}
}
}

0 comments on commit 3c2f190

Please sign in to comment.