feat(testability): Expose function frameworkStabilizers

Closes #5485
This commit is contained in:
Hank Duan 2016-01-05 12:56:24 -08:00
parent 95248f46a1
commit 69ae3634c7
6 changed files with 159 additions and 8 deletions

View File

@ -30,6 +30,7 @@ interface BrowserNodeGlobal {
zone: Zone; zone: Zone;
getAngularTestability: Function; getAngularTestability: Function;
getAllAngularTestabilities: Function; getAllAngularTestabilities: Function;
frameworkStabilizers: Array<Function>;
setTimeout: Function; setTimeout: Function;
clearTimeout: Function; clearTimeout: Function;
setInterval: Function; setInterval: Function;

View File

@ -15,6 +15,13 @@ import {PromiseWrapper, ObservableWrapper} from 'angular2/src/facade/async';
export class Testability { export class Testability {
/** @internal */ /** @internal */
_pendingCount: number = 0; _pendingCount: number = 0;
/**
* Whether any work was done since the last 'whenStable' callback. This is
* useful to detect if this could have potentially destabilized another
* component while it is stabilizing.
* @internal
*/
_didWork: boolean = false;
/** @internal */ /** @internal */
_callbacks: Function[] = []; _callbacks: Function[] = [];
/** @internal */ /** @internal */
@ -23,8 +30,10 @@ export class Testability {
/** @internal */ /** @internal */
_watchAngularEvents(_ngZone: NgZone): void { _watchAngularEvents(_ngZone: NgZone): void {
ObservableWrapper.subscribe(_ngZone.onTurnStart, ObservableWrapper.subscribe(_ngZone.onTurnStart, (_) => {
(_) => { this._isAngularEventPending = true; }); this._didWork = true;
this._isAngularEventPending = true;
});
_ngZone.runOutsideAngular(() => { _ngZone.runOutsideAngular(() => {
ObservableWrapper.subscribe(_ngZone.onEventDone, (_) => { ObservableWrapper.subscribe(_ngZone.onEventDone, (_) => {
@ -38,6 +47,7 @@ export class Testability {
increasePendingRequestCount(): number { increasePendingRequestCount(): number {
this._pendingCount += 1; this._pendingCount += 1;
this._didWork = true;
return this._pendingCount; return this._pendingCount;
} }
@ -55,14 +65,16 @@ export class Testability {
/** @internal */ /** @internal */
_runCallbacksIfReady(): void { _runCallbacksIfReady(): void {
if (!this.isStable()) { if (!this.isStable()) {
this._didWork = true;
return; // Not ready return; // Not ready
} }
// Schedules the call backs in a new frame so that it is always async. // Schedules the call backs in a new frame so that it is always async.
PromiseWrapper.resolve(null).then((_) => { PromiseWrapper.resolve(null).then((_) => {
while (this._callbacks.length !== 0) { while (this._callbacks.length !== 0) {
(this._callbacks.pop())(); (this._callbacks.pop())(this._didWork);
} }
this._didWork = false;
}); });
} }

View File

@ -90,7 +90,7 @@ class PublicTestability implements _JsObjectProxyable {
'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) => 'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) =>
findBindings(bindingString, exactMatch, allowNonElementNodes), findBindings(bindingString, exactMatch, allowNonElementNodes),
'isStable': () => isStable(), 'isStable': () => isStable(),
'whenStable': (callback) => whenStable(() => callback.apply([])) 'whenStable': (callback) => whenStable((didWork) => callback.apply([didWork]))
})..['_dart_'] = this; })..['_dart_'] = this;
} }
} }
@ -116,16 +116,38 @@ class BrowserGetTestability implements GetTestability {
} }
throw 'Could not find testability for element.'; throw 'Could not find testability for element.';
}); });
js.context['getAllAngularTestabilities'] = _jsify(() { var getAllAngularTestabilities = () {
var registry = js.context['ngTestabilityRegistries']; var registry = js.context['ngTestabilityRegistries'];
var result = []; var result = [];
for (int i = 0; i < registry.length; i++) { for (int i = 0; i < registry.length; i++) {
var testabilities = var testabilities =
registry[i].callMethod('getAllAngularTestabilities'); registry[i].callMethod('getAllAngularTestabilities');
if (testabilities != null) result.addAll(testabilities); if (testabilities != null) result.addAll(testabilities);
} }
return _jsify(result); return _jsify(result);
};
js.context['getAllAngularTestabilities'] =
_jsify(getAllAngularTestabilities);
var whenAllStable = _jsify((callback) {
var testabilities = getAllAngularTestabilities();
var count = testabilities.length;
var didWork = false;
var decrement = _jsify((bool didWork_) {
didWork = didWork || didWork_;
count--;
if (count == 0) {
callback.apply([didWork]);
}
});
testabilities.forEach((testability) {
testability.callMethod('whenStable', [decrement]);
});
}); });
if (js.context['frameworkStabilizers'] == null) {
js.context['frameworkStabilizers'] = new js.JsArray();
}
js.context['frameworkStabilizers'].add(whenAllStable);
} }
jsRegistry.add(this._createRegistry(registry)); jsRegistry.add(this._createRegistry(registry));
} }
@ -163,4 +185,4 @@ class BrowserGetTestability implements GetTestability {
}); });
return object; return object;
} }
} }

View File

@ -48,6 +48,25 @@ export class BrowserGetTestability implements GetTestability {
var testabilities = registry.getAllTestabilities(); var testabilities = registry.getAllTestabilities();
return testabilities.map((testability) => { return new PublicTestability(testability); }); return testabilities.map((testability) => { return new PublicTestability(testability); });
}; };
var whenAllStable = (callback) => {
var testabilities = global.getAllAngularTestabilities();
var count = testabilities.length;
var didWork = false;
var decrement = function(didWork_) {
didWork = didWork || didWork_;
count--;
if (count == 0) {
callback(didWork);
}
};
testabilities.forEach(function(testability) { testability.whenStable(decrement); });
};
if (!global.frameworkStabilizers) {
global.frameworkStabilizers = ListWrapper.createGrowableSize(0);
}
global.frameworkStabilizers.push(whenAllStable);
} }
findTestabilityInTree(registry: TestabilityRegistry, elem: any, findTestabilityInTree(registry: TestabilityRegistry, elem: any,

View File

@ -43,12 +43,13 @@ class MockNgZone extends NgZone {
export function main() { export function main() {
describe('Testability', () => { describe('Testability', () => {
var testability, execute, ngZone; var testability, execute, execute2, ngZone;
beforeEach(() => { beforeEach(() => {
ngZone = new MockNgZone(); ngZone = new MockNgZone();
testability = new Testability(ngZone); testability = new Testability(ngZone);
execute = new SpyObject().spy('execute'); execute = new SpyObject().spy('execute');
execute2 = new SpyObject().spy('execute');
}); });
describe('Pending count logic', () => { describe('Pending count logic', () => {
@ -109,6 +110,35 @@ export function main() {
expect(execute).not.toHaveBeenCalled(); expect(execute).not.toHaveBeenCalled();
}); });
it('should fire whenstable callbacks with didWork if pending count is 0',
inject([AsyncTestCompleter], (async) => {
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalledWith(false);
async.done();
});
}));
it('should fire whenstable callbacks with didWork when pending drops to 0',
inject([AsyncTestCompleter], (async) => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
});
}));
}); });
describe('NgZone callback logic', () => { describe('NgZone callback logic', () => {
@ -208,6 +238,43 @@ export function main() {
}); });
}); });
})); }));
it('should fire whenstable callback with didWork if event is already finished',
inject([AsyncTestCompleter], (async) => {
ngZone.start();
ngZone.finish();
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
}));
it('should fire whenstable callback with didwork when event finishes',
inject([AsyncTestCompleter], (async) => {
ngZone.start();
testability.whenStable(execute);
microTask(() => {
ngZone.finish();
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
});
}));
}); });
}); });
} }

View File

@ -58,5 +58,35 @@ describe('async', () => {
expect(timeout.$('.val').getText()).toEqual('10'); expect(timeout.$('.val').getText()).toEqual('10');
}); });
it('should wait via frameworkStabilizer', () => {
var whenAllStable = function() {
return browser.executeAsyncScript('window.frameworkStabilizers[0](arguments[0]);');
};
// This disables protractor's wait mechanism
browser.ignoreSynchronization = true;
var timeout = $('#multiDelayedIncrements');
// At this point, the async action is still pending, so the count should
// still be 0.
expect(timeout.$('.val').getText()).toEqual('0');
timeout.$('.action').click();
whenAllStable().then((didWork) => {
// whenAllStable should only be called when all the async actions
// finished, so the count should be 10 at this point.
expect(timeout.$('.val').getText()).toEqual('10');
expect(didWork).toBeTruthy(); // Work was done.
});
whenAllStable().then((didWork) => {
// whenAllStable should be called immediately since nothing is pending.
expect(didWork).toBeFalsy(); // No work was done.
browser.ignoreSynchronization = false;
});
});
afterEach(verifyNoBrowserErrors); afterEach(verifyNoBrowserErrors);
}); });