Skip to content

Commit

Permalink
Introducing "it.yield", a convenient way to test promise code (#9601)
Browse files Browse the repository at this point in the history
* Introduce yield.

* Address comments.

* fix done state error handling
  • Loading branch information
lannka authored Jun 4, 2017
1 parent dc5763a commit 6f4e6f2
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions test/_init_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from '../src/error';
import {resetExperimentTogglesForTesting} from '../src/experiments';
import * as describes from '../testing/describes';
import {installYieldIt} from '../testing/yield';
import stringify from 'json-stable-stringify';


Expand Down Expand Up @@ -207,6 +208,8 @@ describe.configure = function() {
return new TestConfig(describe);
};

installYieldIt(it);

it.configure = function() {
return new TestConfig(it);
};
Expand Down
115 changes: 115 additions & 0 deletions test/functional/test-yield.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as lolex from 'lolex';
import {macroTask} from '../../testing/yield';

describes.realWin('yield', {}, env => {

let win;
let clock;

beforeEach(() => {
win = env.win;
clock = lolex.install(win, 0, ['Date', 'setTimeout', 'clearTimeout']);
});

it('should work with nested promises', function* () {
let value = false;

const nestPromise = level => {
if (level == 0) {
value = true;
return;
}
return Promise.resolve().then(() => {
return nestPromise(level - 1);
});
};

nestPromise(100);
expect(value).to.be.false;
yield macroTask();
expect(value).to.be.true;
});

it('should work with promise chain', function* () {
let value;

const chainPromise = Promise.resolve();
for (let i = 0; i < 100; i++) {
chainPromise.then(() => {value = false;});
}
chainPromise.then(() => {
value = true;
});
expect(value).to.be.undefined;
yield macroTask();
expect(value).to.be.true;
});

it('should work with promise inside setTimeout', function* () {
let value;
win.setTimeout(() => {
value = false;
Promise.resolve().then(() => {
value = true;
});
}, 100);

expect(value).to.be.undefined;
clock.tick(100);
expect(value).to.be.false;
yield macroTask();
expect(value).to.be.true;
});

it('should work with manually resolved promise inside ' +
'setTimeout', function* () {
let value;
let resolver;
const promise = new Promise(r => {resolver = r;});
promise.then(() => {
value = true;
});
win.setTimeout(() => {
value = false;
resolver();
}, 100);
clock.tick(100);
expect(value).to.be.false;
yield macroTask();
expect(value).to.be.true;
});

it('should block a promise', function* () {
let resolver;
const promise = new Promise(r => {resolver = r;}).then(() => 'yes');
resolver();
const result = yield promise;
expect(result).to.equal('yes');
});

it('should be able to expect throwable', function* () {
const promiseThatRejects = Promise.reject(new Error('OMG'));
try {
yield promiseThatRejects;
throw new Error('UNREACHABLE');
} catch (e) {
expect(e.message).to.contain('OMG');
}
});
});
72 changes: 72 additions & 0 deletions testing/yield.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Install "YieldIt" support to Mocha tests.
* "YieldIt" allows you to wait for a promise to resolve before resuming your
* test, so you can write asynchronous test in a synchronous way.
* Check test-yield.js for how-to.
*/
export function installYieldIt(realIt) {
it = enableYield.bind(null, realIt); // eslint-disable-line no-native-reassign, no-undef
it./*OK*/only = enableYield.bind(null, realIt.only);
it.skip = realIt.skip;
}

/**
* A convenient method so you can flush the event queue by doing
* `yield macroTask()` in your test.
* @returns {Promise}
*/
export function macroTask() {
return new Promise(setTimeout);
}

function enableYield(fn, message, runnable) {
if (!runnable || !runnable.constructor
|| runnable.constructor.name !== 'GeneratorFunction') {
return fn(message, runnable);
}
return fn(message, done => {
const iterator = runnable();
function step(method, result) {
let state;
try {
state = iterator[method](result);
} catch (e) {
// catch any assertion errors and pass to `done`
// otherwise the messages are swallowed
return done(e);
}
if (state.done) {
Promise.resolve(state.value).then(() => done(), done);
return;
}

Promise.resolve(state.value).then(_next, _throw);
}

function _next(value) {
step('next', value);
}

function _throw(error) {
step('throw', error);
}

_next();
});
}

0 comments on commit 6f4e6f2

Please sign in to comment.