Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
feat(zonespec): add a spec for asynchronous tests
Browse files Browse the repository at this point in the history
This spec is constructed with a done callback and a fail callback,
and waits until all asynchronous tasks are completed before
exiting.

Closes #275
  • Loading branch information
juliemr authored and mhevery committed Mar 21, 2016
1 parent 4d108ce commit aeeb05c
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 1 deletion.
7 changes: 6 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ gulp.task('build/wtf.min.js', function(cb) {
return generateBrowserScript('./lib/zone-spec/wtf.ts', 'wtf.min.js', true, cb);
});

gulp.task('build/async-test.js', function(cb) {
return generateBrowserScript('./lib/zone-spec/async-test.ts', 'async-test.js', false, cb);
});

gulp.task('build', [
'build/zone.js',
'build/zone.js.d.ts',
Expand All @@ -108,7 +112,8 @@ gulp.task('build', [
'build/long-stack-trace-zone.js',
'build/long-stack-trace-zone.min.js',
'build/wtf.js',
'build/wtf.min.js'
'build/wtf.min.js',
'build/async-test.js'
]);


Expand Down
96 changes: 96 additions & 0 deletions lib/zone-spec/async-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
(function() {
class AsyncTestZoneSpec implements ZoneSpec {
_finishCallback: Function;
_failCallback: Function;
_pendingMicroTasks: boolean = false;
_pendingMacroTasks: boolean = false;
_alreadyErrored: boolean = false;
runZone = Zone.current;

constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
this._finishCallback = finishCallback;
this._failCallback = failCallback;
this.name = 'asyncTestZone for ' + namePrefix;
}

_finishCallbackIfDone() {
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
// We do this because we would like to catch unhandled rejected promises.
// To do this quickly when there are native promises, we must run using an unwrapped
// promise implementation.
var symbol = (<any>Zone).__symbol__;
var NativePromise: typeof Promise = <any>window[symbol('Promise')];
if (NativePromise) {
NativePromise.resolve(true)[symbol('then')](() => {
if (!this._alreadyErrored) {
this.runZone.run(this._finishCallback);
}
});
} else {
// For implementations which do not have nativePromise, use setTimeout(0). This is slower,
// but it also works because Zones will handle errors when rejected promises have no
// listeners after one macrotask.
this.runZone.run(() => {
setTimeout(() => {
if (!this._alreadyErrored) {
this._finishCallback();
}
}, 0);
});
}
}
}

// ZoneSpec implementation below.

name: string;

// Note - we need to use onInvoke at the moment to call finish when a test is
// fully synchronous. TODO(juliemr): remove this when the logic for
// onHasTask changes and it calls whenever the task queues are dirty.
onInvoke(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
delegate: Function, applyThis: any, applyArgs: any[], source: string): any {
try {
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
} finally {
this._finishCallbackIfDone();
}
}

onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
error: any): boolean {
// Let the parent try to handle the error.
var result = parentZoneDelegate.handleError(targetZone, error);
if (result) {
this._failCallback(error.message ? error.message : 'unknown error');
this._alreadyErrored = true;
}
return false;
}

onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): Task {
if (task.type == 'macroTask' && task.source == 'setInterval') {
this._failCallback('Cannot use setInterval from within an async zone test.');
return;
}

return delegate.scheduleTask(targetZone, task);
}

onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
delegate.hasTask(target, hasTaskState);

if (hasTaskState.change == 'microTask') {
this._pendingMicroTasks = hasTaskState.microTask;
this._finishCallbackIfDone();
} else if (hasTaskState.change == 'macroTask') {
this._pendingMacroTasks = hasTaskState.macroTask;
this._finishCallbackIfDone();
}
}
}

// Export the class so that new instances can be created with proper
// constructor params.
Zone['AsyncTestZoneSpec'] = AsyncTestZoneSpec;
})();
232 changes: 232 additions & 0 deletions test/async-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import '../lib/zone-spec/async-test';

describe('AsyncTestZoneSpec', function() {
var log;
var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec'];

function finishCallback() {
log.push('finish');
}

function failCallback() {
log.push('fail');
}

beforeEach(() => {
log = [];
});

it('should call finish after zone is run', (done) => {
var finished = false;
var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, failCallback, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
finished = true;
});
});

it('should call finish after a setTimeout is done', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
setTimeout(() => {
finished = true;
}, 10);
});
});

it('should call finish after microtasks are done', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
Promise.resolve().then(() => {
finished = true;
});
});
});

it('should call finish after both micro and macrotasks are done', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
var deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 10)
}).then(() => {
finished = true;
});
});
});

it('should call finish after both macro and microtasks are done', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
Promise.resolve().then(() => {
setTimeout(() => {
finished = true;
}, 10);
});
});
});

describe('event tasks', () => {
var button;
beforeEach(function() {
button = document.createElement('button');
document.body.appendChild(button);
});
afterEach(function() {
document.body.removeChild(button);
});

it('should call finish after an event task is done', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
button.addEventListener('click', () => {
finished = true;
});

var clickEvent = document.createEvent('Event');
clickEvent.initEvent('click', true, true);

button.dispatchEvent(clickEvent);
});
});

it('should call finish after an event task is done asynchronously', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
expect(finished).toBe(true);
done();
}, () => {
done.fail('async zone called failCallback unexpectedly');
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
button.addEventListener('click', () => {
setTimeout(() => {
finished = true;
}, 10);
});

var clickEvent = document.createEvent('Event');
clickEvent.initEvent('click', true, true);

button.dispatchEvent(clickEvent);
});
});
});

it('should fail if setInterval is used', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
done.fail('expected failCallback to be called');
}, (err) => {
expect(err).toEqual('Cannot use setInterval from within an async zone test.');
done();
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
setInterval(() => {
}, 100);
});
});

it('should fail if an error is thrown asynchronously', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
done.fail('expected failCallback to be called');
}, (err) => {
expect(err).toEqual('my error');
done();
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
setTimeout(() => {
throw new Error('my error');
}, 10);
});
});

it('should fail if a promise rejection is unhandled', (done) => {
var finished = false;

var testZoneSpec = new AsyncTestZoneSpec(() => {
done.fail('expected failCallback to be called');
}, (err) => {
expect(err).toEqual('Uncaught (in promise): my reason');
done();
}, 'name');

var atz = Zone.current.fork(testZoneSpec);

atz.run(function() {
Promise.reject('my reason');
});

});
});

export var __something__;
1 change: 1 addition & 0 deletions test/browser_entry_point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import './test-env-setup';

// List all tests here:
import './long-stack-trace-zone.spec';
import './async-test.spec';
import './microtasks.spec';
import './zone.spec';
import './integration/brick.spec';
Expand Down

0 comments on commit aeeb05c

Please sign in to comment.