Skip to content

Commit

Permalink
Ensure unhandled rejections cause test failure
Browse files Browse the repository at this point in the history
Prior to this change, an unhandled exception in a native promise that was not returned from the `test` callback would not cause test failure.

For example, prior to this change both of the following (using default out of the box settings) results in test failures:

```js
test('native promises cause an unhandled rejection', function(assert) {
  // imagine this was "deep" inside various method calls
  setTimeout(function() {
    throw new Error('whoops!');
  });

  // other things that we are trying to test
  assert.deepEqual(someMethod(), ['one', 'two']);
});

test('RSVP promises cause an unhandled rejection', function(assert) {
  // imagine this was "deep" inside various method calls
  Ember.RSVP.Promise.resolve().then(function() {
    throw new Error('whoops!');
  });

  // other things that we are trying to test
  assert.deepEqual(someMethod(), ['one', 'two']);
});
```

But this test (prior to this PR) would result in a passing test suite:

```js
test('native promises cause an unhandled rejection', function(assert) {
  // imagine this was "deep" inside various method calls
  Promise.resolve().then(function() {
    throw new Error('whoops!');
  });

  // other things that we are trying to test
  assert.deepEqual(someMethod(), ['one', 'two']);
});
```

This is a polyfill of the general behavior being worked on upstream in
QUnit itself, and can be removed once the feature is landed.
  • Loading branch information
rwjblue committed Dec 18, 2017
1 parent ff69425 commit dee9e31
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 1 deletion.
2 changes: 1 addition & 1 deletion addon-test-support/ember-qunit/adapter.js
Expand Up @@ -15,7 +15,7 @@ function unhandledRejectionAssertion(current, error) {
source = 'unknown source';
}

current.pushResult({
current.assert.pushResult({
result: false,
actual: false,
expected: true,
Expand Down
43 changes: 43 additions & 0 deletions addon-test-support/ember-qunit/index.js
Expand Up @@ -222,6 +222,43 @@ export function setupEmberOnerrorValidation() {
});
}

/**
Ensures unhandled native promise rejections are tracked and fail tests
properly. This brings native promise rejections in line with
Ember.RSVP.Promise rejections (which already have this behavior).
Please note: This only works on Chrome (other browsers simply never hit this
code) since the `unhandledrejection` event is only fired on Chrome (>= 49).
*/
export function setupUnhandledRejectionHandler() {
// only needed until https://github.com/qunitjs/qunit/pull/1241 lands and is
// included in a release that ember-qunit can depend upon

// Listen for unhandled rejections, and call QUnit.onError.
window.addEventListener('unhandledrejection', __unhandledRejectionHandler__);
}

export function __unhandledRejectionHandler__(event) {
let { reason } = event;
let resultInfo = {
result: false,
message: reason.message || 'error',
actual: reason,
source: reason.stack,
};

let currentTest = QUnit.config.current;
if (currentTest) {
currentTest.assert.pushResult(resultInfo);
} else {
let test = assert => {
assert.pushResult(resultInfo);
};
test.validTest = true;
QUnit.test('global failure', test);
}
}

/**
@method start
@param {Object} [options] Options to be used for enabling/disabling behaviors
Expand All @@ -237,6 +274,8 @@ export function setupEmberOnerrorValidation() {
back to `false` after each test will.
@param {Boolean} [options.setupEmberOnerrorValidation] If `false` validation
of `Ember.onerror` will be disabled.
@param {Boolean} [options.setupUnhandledRejectionHandler=true] Adds an `unhandledRejection` event listener
that will cause your test suite to fail for any unhandled native promise rejections.
*/
export function start(options = {}) {
if (options.loadTests !== false) {
Expand All @@ -259,6 +298,10 @@ export function start(options = {}) {
setupEmberOnerrorValidation();
}

if (options.setupUnhandledRejectionHandler !== false) {
setupUnhandledRejectionHandler();
}

if (options.startTests !== false) {
startTests();
}
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/unhandled-rejection-test.js
@@ -0,0 +1,94 @@
import { __unhandledRejectionHandler__ } from 'ember-qunit';
import { Promise as RSVPPromise } from 'rsvp';
import { module, test } from 'qunit';

const HAS_NATIVE_PROMISE = typeof Promise !== 'undefined';
const HAS_UNHANDLED_REJECTION_HANDLER = 'onunhandledrejection' in window;

module('unhandle promise rejections', function(hooks) {
hooks.beforeEach(function(assert) {
let originalPushResult = assert.pushResult;
assert.pushResult = function(resultInfo) {
// Inverts the result so we can test failing assertions
resultInfo.result = !resultInfo.result;
resultInfo.message = `Failed: ${resultInfo.message}`;
originalPushResult(resultInfo);
};
});

test('RSVP promises cause an unhandled rejection', function(assert) {
let done = assert.async();

// ensure we do not exit this test until the assertion has happened
setTimeout(done, 10);

new RSVPPromise(resolve => {
setTimeout(resolve);
}).then(function() {
throw new Error('whoops!');
});
});

if (HAS_NATIVE_PROMISE && HAS_UNHANDLED_REJECTION_HANDLER) {
test('native promises cause an unhandled rejection', function(assert) {
let done = assert.async();

// ensure we do not exit this test until the assertion has happened
setTimeout(done, 10);

new self.Promise(resolve => {
setTimeout(resolve);
}).then(function() {
throw new Error('whoops!');
});
});
}
});

if (HAS_NATIVE_PROMISE && HAS_UNHANDLED_REJECTION_HANDLER) {
module('unhandled native promise rejection outside of test context', function(hooks) {
var originalPushResult;

hooks.beforeEach(function(assert) {
// Duck-punch pushResult so we can check test name and assert args.
originalPushResult = assert.pushResult;

assert.pushResult = function(resultInfo) {
// Restore pushResult for this assert object, to allow following assertions.
this.pushResult = originalPushResult;

this.strictEqual(this.test.testName, 'global failure', 'Test is appropriately named');

this.deepEqual(
resultInfo,
{
message: 'Error message',
source: 'filePath.js:1',
result: false,
actual: {
message: 'Error message',
fileName: 'filePath.js',
lineNumber: 1,
stack: 'filePath.js:1',
},
},
'Expected assert.pushResult to be called with correct args'
);
};
});

hooks.afterEach(function() {
QUnit.config.current.pushResult = originalPushResult;
});

// Actual test (outside QUnit.test context)
__unhandledRejectionHandler__({
reason: {
message: 'Error message',
fileName: 'filePath.js',
lineNumber: 1,
stack: 'filePath.js:1',
},
});
});
}

0 comments on commit dee9e31

Please sign in to comment.