angular-cn/packages/core/test/testability/testability_spec.ts
Michael Giambalvo 16c03c0f38 fix(core): In Testability.whenStable update callback, pass more complete (#25010)
data about tasks.

When building a list of pending tasks for callers of whenStable(),
Testability will copy data about the task into a new object, in order to
avoid leaking references to tasks.

This change copies more properties from Tasks into the list of pending
tasks, as well as a reference to Task.data to give callers more
information about the tasks that are pending.

Specifically, this also copies runCount and task ID, which are needed in
order for callers to know when a given task is repeating.

PR Close #25010
2018-08-06 13:49:19 -07:00

383 lines
13 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {EventEmitter} from '@angular/core';
import {Injectable} from '@angular/core/src/di';
import {PendingMacrotask, Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {async, fakeAsync, flush, tick} from '@angular/core/testing';
import {SpyObject, beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {scheduleMicroTask} from '../../src/util';
// Schedules a microtasks (using a resolved promise .then())
function microTask(fn: Function): void {
scheduleMicroTask(() => {
// We do double dispatch so that we can wait for scheduleMicrotask in the Testability when
// NgZone becomes stable.
scheduleMicroTask(fn);
});
}
@Injectable()
class MockNgZone extends NgZone {
/** @internal */
onUnstable: EventEmitter<any>;
/** @internal */
onStable: EventEmitter<any>;
constructor() {
super({enableLongStackTrace: false});
this.onUnstable = new EventEmitter(false);
this.onStable = new EventEmitter(false);
}
unstable(): void { this.onUnstable.emit(null); }
stable(): void { this.onStable.emit(null); }
}
{
describe('Testability', () => {
let testability: Testability;
let execute: any;
let execute2: any;
let updateCallback: any;
let ngZone: MockNgZone;
beforeEach(async(() => {
ngZone = new MockNgZone();
testability = new Testability(ngZone);
execute = new SpyObject().spy('execute');
execute2 = new SpyObject().spy('execute');
updateCallback = new SpyObject().spy('execute');
}));
describe('Pending count logic', () => {
it('should start with a pending count of 0',
() => { expect(testability.getPendingRequestCount()).toEqual(0); });
it('should fire whenstable callbacks if pending count is 0', async(() => {
testability.whenStable(execute);
microTask(() => { expect(execute).toHaveBeenCalled(); });
}));
it('should not fire whenstable callbacks synchronously if pending count is 0', () => {
testability.whenStable(execute);
expect(execute).not.toHaveBeenCalled();
});
it('should not call whenstable callbacks when there are pending counts', async(() => {
testability.increasePendingRequestCount();
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => { expect(execute).not.toHaveBeenCalled(); });
});
}));
it('should fire whenstable callbacks when pending drops to 0', async(() => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => { expect(execute).toHaveBeenCalled(); });
});
}));
it('should not fire whenstable callbacks synchronously when pending drops to 0', async(() => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
testability.decreasePendingRequestCount();
expect(execute).not.toHaveBeenCalled();
}));
it('should fire whenstable callbacks with didWork if pending count is 0', async(() => {
microTask(() => {
testability.whenStable(execute);
microTask(() => { expect(execute).toHaveBeenCalledWith(false); });
});
}));
it('should fire whenstable callbacks with didWork when pending drops to 0', async(() => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => { expect(execute2).toHaveBeenCalledWith(false); });
});
}));
});
describe('NgZone callback logic', () => {
describe('whenStable with timeout', () => {
it('should list pending tasks when the timeout is hit', fakeAsync(() => {
const id = ngZone.run(() => setTimeout(() => {}, 1000));
testability.whenStable(execute, 200);
expect(execute).not.toHaveBeenCalled();
tick(200);
expect(execute).toHaveBeenCalled();
const tasks = execute.calls.mostRecent().args[1] as PendingMacrotask[];
expect(tasks.length).toEqual(1);
expect(tasks[0].data.delay).toEqual(1000);
expect(tasks[0].source).toEqual('setTimeout');
expect(tasks[0].data.isPeriodic).toEqual(false);
clearTimeout(id);
}));
it('should fire if Angular is already stable', async(() => {
testability.whenStable(execute, 200);
microTask(() => { expect(execute).toHaveBeenCalled(); });
}));
it('should fire when macroTasks are cancelled', fakeAsync(() => {
const id = ngZone.run(() => setTimeout(() => {}, 1000));
testability.whenStable(execute, 500);
tick(200);
ngZone.run(() => clearTimeout(id));
// fakeAsync doesn't trigger NgZones whenStable
ngZone.stable();
tick(1);
expect(execute).toHaveBeenCalled();
}));
it('calls the done callback when angular is stable', fakeAsync(() => {
let timeout1Done = false;
ngZone.run(() => setTimeout(() => timeout1Done = true, 500));
testability.whenStable(execute, 1000);
tick(600);
ngZone.stable();
tick();
expect(timeout1Done).toEqual(true);
expect(execute).toHaveBeenCalled();
// Should cancel the done timeout.
tick(500);
ngZone.stable();
tick();
expect(execute.calls.count()).toEqual(1);
}));
it('calls update when macro tasks change', fakeAsync(() => {
let timeout1Done = false;
let timeout2Done = false;
ngZone.run(() => setTimeout(() => timeout1Done = true, 500));
tick();
testability.whenStable(execute, 1000, updateCallback);
tick(100);
ngZone.run(() => setTimeout(() => timeout2Done = true, 300));
expect(updateCallback.calls.count()).toEqual(1);
tick(600);
expect(timeout1Done).toEqual(true);
expect(timeout2Done).toEqual(true);
expect(updateCallback.calls.count()).toEqual(3);
expect(execute).toHaveBeenCalled();
const update1 = updateCallback.calls.all()[0].args[0] as PendingMacrotask[];
expect(update1[0].data.delay).toEqual(500);
const update2 = updateCallback.calls.all()[1].args[0] as PendingMacrotask[];
expect(update2[0].data.delay).toEqual(500);
expect(update2[1].data.delay).toEqual(300);
}));
it('cancels the done callback if the update callback returns true', fakeAsync(() => {
let timeoutDone = false;
ngZone.unstable();
execute2.and.returnValue(true);
testability.whenStable(execute, 1000, execute2);
tick(100);
ngZone.run(() => setTimeout(() => timeoutDone = true, 500));
ngZone.stable();
expect(execute2).toHaveBeenCalled();
tick(500);
ngZone.stable();
tick();
expect(execute).not.toHaveBeenCalled();
}));
});
it('should fire whenstable callback if event is already finished', fakeAsync(() => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
tick();
expect(execute).toHaveBeenCalled();
}));
it('should not fire whenstable callbacks synchronously if event is already finished', () => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
expect(execute).not.toHaveBeenCalled();
});
it('should fire whenstable callback when event finishes', fakeAsync(() => {
ngZone.unstable();
testability.whenStable(execute);
tick();
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
tick();
expect(execute).toHaveBeenCalled();
}));
it('should not fire whenstable callbacks synchronously when event finishes', () => {
ngZone.unstable();
testability.whenStable(execute);
ngZone.stable();
expect(execute).not.toHaveBeenCalled();
});
it('should not fire whenstable callback when event did not finish', fakeAsync(() => {
ngZone.unstable();
testability.increasePendingRequestCount();
testability.whenStable(execute);
tick();
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
tick();
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
tick();
expect(execute).toHaveBeenCalled();
}));
it('should not fire whenstable callback when there are pending counts', fakeAsync(() => {
ngZone.unstable();
testability.increasePendingRequestCount();
testability.increasePendingRequestCount();
testability.whenStable(execute);
tick();
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
tick();
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
tick();
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
tick();
expect(execute).toHaveBeenCalled();
}));
it('should fire whenstable callback with didWork if event is already finished',
fakeAsync(() => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
tick();
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
tick();
expect(execute2).toHaveBeenCalledWith(false);
}));
it('should fire whenstable callback with didwork when event finishes', fakeAsync(() => {
ngZone.unstable();
testability.whenStable(execute);
tick();
ngZone.stable();
tick();
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
tick();
expect(execute2).toHaveBeenCalledWith(false);
}));
});
});
describe('TestabilityRegistry', () => {
let testability1: Testability;
let testability2: Testability;
let registry: TestabilityRegistry;
let ngZone: MockNgZone;
beforeEach(async(() => {
ngZone = new MockNgZone();
testability1 = new Testability(ngZone);
testability2 = new Testability(ngZone);
registry = new TestabilityRegistry();
}));
describe('unregister testability', () => {
it('should remove the testability when unregistering an existing testability', () => {
registry.registerApplication('testability1', testability1);
registry.registerApplication('testability2', testability2);
registry.unregisterApplication('testability2');
expect(registry.getAllTestabilities().length).toEqual(1);
expect(registry.getTestability('testability1')).toEqual(testability1);
});
it('should remain the same when unregistering a non-existing testability', () => {
expect(registry.getAllTestabilities().length).toEqual(0);
registry.registerApplication('testability1', testability1);
registry.registerApplication('testability2', testability2);
registry.unregisterApplication('testability3');
expect(registry.getAllTestabilities().length).toEqual(2);
expect(registry.getTestability('testability1')).toEqual(testability1);
expect(registry.getTestability('testability2')).toEqual(testability2);
});
it('should remove all the testability when unregistering all testabilities', () => {
registry.registerApplication('testability1', testability1);
registry.registerApplication('testability2', testability2);
registry.unregisterAllApplications();
expect(registry.getAllTestabilities().length).toEqual(0);
});
});
});
}