Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testability): Expose function frameworkStabilizers #5485

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/angular2/manual_typings/globals-es6.d.ts
Expand Up @@ -30,6 +30,7 @@ interface BrowserNodeGlobal {
zone: Zone;
getAngularTestability: Function;
getAllAngularTestabilities: Function;
frameworkStabilizers: Array<Function>;
setTimeout: Function;
clearTimeout: Function;
setInterval: Function;
Expand Down
18 changes: 15 additions & 3 deletions modules/angular2/src/core/testability/testability.ts
Expand Up @@ -15,6 +15,13 @@ import {PromiseWrapper, ObservableWrapper} from 'angular2/src/facade/async';
export class Testability {
/** @internal */
_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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment on what this is for might be useful for someone unfamiliar with the goal of our framework stabilizers.

/** @internal */
_callbacks: Function[] = [];
/** @internal */
Expand All @@ -23,8 +30,10 @@ export class Testability {

/** @internal */
_watchAngularEvents(_ngZone: NgZone): void {
ObservableWrapper.subscribe(_ngZone.onTurnStart,
(_) => { this._isAngularEventPending = true; });
ObservableWrapper.subscribe(_ngZone.onTurnStart, (_) => {
this._didWork = true;
this._isAngularEventPending = true;
});

_ngZone.runOutsideAngular(() => {
ObservableWrapper.subscribe(_ngZone.onEventDone, (_) => {
Expand All @@ -38,6 +47,7 @@ export class Testability {

increasePendingRequestCount(): number {
this._pendingCount += 1;
this._didWork = true;
return this._pendingCount;
}

Expand All @@ -55,14 +65,16 @@ export class Testability {
/** @internal */
_runCallbacksIfReady(): void {
if (!this.isStable()) {
this._didWork = true;
return; // Not ready
}

// Schedules the call backs in a new frame so that it is always async.
PromiseWrapper.resolve(null).then((_) => {
while (this._callbacks.length !== 0) {
(this._callbacks.pop())();
(this._callbacks.pop())(this._didWork);
}
this._didWork = false;
});
}

Expand Down
30 changes: 26 additions & 4 deletions modules/angular2/src/platform/browser/testability.dart
Expand Up @@ -90,7 +90,7 @@ class PublicTestability implements _JsObjectProxyable {
'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) =>
findBindings(bindingString, exactMatch, allowNonElementNodes),
'isStable': () => isStable(),
'whenStable': (callback) => whenStable(() => callback.apply([]))
'whenStable': (callback) => whenStable((didWork) => callback.apply([didWork]))
})..['_dart_'] = this;
}
}
Expand All @@ -116,16 +116,38 @@ class BrowserGetTestability implements GetTestability {
}
throw 'Could not find testability for element.';
});
js.context['getAllAngularTestabilities'] = _jsify(() {
var getAllAngularTestabilities = () {
var registry = js.context['ngTestabilityRegistries'];
var result = [];
for (int i = 0; i < registry.length; i++) {
var testabilities =
registry[i].callMethod('getAllAngularTestabilities');
registry[i].callMethod('getAllAngularTestabilities');
if (testabilities != null) result.addAll(testabilities);
}
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));
}
Expand Down Expand Up @@ -163,4 +185,4 @@ class BrowserGetTestability implements GetTestability {
});
return object;
}
}
}
19 changes: 19 additions & 0 deletions modules/angular2/src/platform/browser/testability.ts
Expand Up @@ -48,6 +48,25 @@ export class BrowserGetTestability implements GetTestability {
var testabilities = registry.getAllTestabilities();
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juliemr Can we write a test that uses global.frameworkStabilizers?

}

findTestabilityInTree(registry: TestabilityRegistry, elem: any,
Expand Down
69 changes: 68 additions & 1 deletion modules/angular2/test/core/testability/testability_spec.ts
Expand Up @@ -43,12 +43,13 @@ class MockNgZone extends NgZone {

export function main() {
describe('Testability', () => {
var testability, execute, ngZone;
var testability, execute, execute2, ngZone;

beforeEach(() => {
ngZone = new MockNgZone();
testability = new Testability(ngZone);
execute = new SpyObject().spy('execute');
execute2 = new SpyObject().spy('execute');
});

describe('Pending count logic', () => {
Expand Down Expand Up @@ -109,6 +110,35 @@ export function main() {

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', () => {
Expand Down Expand Up @@ -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();
});
});
});
}));
});
});
}
30 changes: 30 additions & 0 deletions modules/playground/e2e_test/async/async_spec.ts
Expand Up @@ -58,5 +58,35 @@ describe('async', () => {
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);
});