diff --git a/addon-test-support/@ember/test-helpers/settled.js b/addon-test-support/@ember/test-helpers/settled.js index c76ef1b75..b61ee959c 100644 --- a/addon-test-support/@ember/test-helpers/settled.js +++ b/addon-test-support/@ember/test-helpers/settled.js @@ -79,6 +79,13 @@ function decrementAjaxPendingRequests(event, xhr) { @private */ export function _teardownAJAXHooks() { + // jQuery will not invoke `ajaxComplete` if + // 1. `transport.send` throws synchronously and + // 2. it has an `error` option which also throws synchronously + + // We can no longer handle any remaining requests + requests = []; + if (!jQuery) { return; } diff --git a/package.json b/package.json index 68612d815..8a71a9afd 100644 --- a/package.json +++ b/package.json @@ -90,4 +90,4 @@ "**/sane": "^2.5.2", "ember-cli/testem": "~2.6.0" } -} +} \ No newline at end of file diff --git a/tests/helpers/ajax.js b/tests/helpers/ajax.js index fd2801712..4602da5fa 100644 --- a/tests/helpers/ajax.js +++ b/tests/helpers/ajax.js @@ -2,13 +2,16 @@ import { Promise } from 'rsvp'; import hasjQuery from '../helpers/has-jquery'; import $ from 'jquery'; // FYI - not present in all scenarios import require from 'require'; +import { join } from '@ember/runloop'; export default function ajax(url) { if (hasjQuery()) { return new Promise((resolve, reject) => { $.ajax(url, { success: resolve, - error: reject, + error(reason) { + join(null, reject, reason); + }, cache: false, }); }); diff --git a/tests/helpers/qunit-module-for.js b/tests/helpers/qunit-module-for.js index 3a70575f1..db78f2e1b 100644 --- a/tests/helpers/qunit-module-for.js +++ b/tests/helpers/qunit-module-for.js @@ -18,6 +18,7 @@ export default function qunitModuleFor(testModule) { }, afterEach(assert) { return testModule.teardown(assert).finally(() => { + Ember.Test.adapter = null; if (Ember.testing) { throw new Error('should not have Ember.testing === true after tests have finished'); } diff --git a/tests/test-helper.js b/tests/test-helper.js index 105917a1d..fddf83230 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -2,6 +2,7 @@ import QUnit from 'qunit'; import { registerDeprecationHandler } from '@ember/debug'; import AbstractTestLoader from 'ember-cli-test-loader/test-support/index'; import Ember from 'ember'; +import { isSettled, getSettledState } from '@ember/test-helpers'; if (QUnit.config.seed) { QUnit.config.reorder = false; @@ -9,6 +10,7 @@ if (QUnit.config.seed) { let moduleLoadFailures = []; let cleanupFailures = []; +let asyncLeakageFailures = []; QUnit.done(function() { if (moduleLoadFailures.length) { @@ -18,6 +20,10 @@ QUnit.done(function() { if (cleanupFailures.length) { throw new Error('\n' + cleanupFailures.join('\n')); } + + if (asyncLeakageFailures.length) { + throw new Error('\n' + asyncLeakageFailures.join('\n')); + } }); class TestLoader extends AbstractTestLoader { @@ -71,6 +77,16 @@ QUnit.testDone(function({ module, name }) { console.error(message); testElementContainer.innerHTML = expected; } + + if (!isSettled()) { + let message = `Expected to be settled after ${module}: ${name}, but was \`${JSON.stringify( + getSettledState() + )}\``; + asyncLeakageFailures.push(message); + + // eslint-disable-next-line + console.error(message); + } }); QUnit.assert.noDeprecations = function(callback) { diff --git a/tests/unit/teardown-context-test.js b/tests/unit/teardown-context-test.js index f4d36eedd..dcd906e08 100644 --- a/tests/unit/teardown-context-test.js +++ b/tests/unit/teardown-context-test.js @@ -1,9 +1,13 @@ import { module, test } from 'qunit'; import Service from '@ember/service'; +import { isSettled, getSettledState } from '@ember/test-helpers'; import { getContext, setupContext, teardownContext } from 'ember-test-helpers'; import { setResolverRegistry } from '../helpers/resolver'; import hasEmberVersion from 'ember-test-helpers/has-ember-version'; import Ember from 'ember'; +import hasjQuery from '../helpers/has-jquery'; +import ajax from '../helpers/ajax'; +import Pretender from 'pretender'; module('teardownContext', function(hooks) { if (!hasEmberVersion(2, 4)) { @@ -12,6 +16,7 @@ module('teardownContext', function(hooks) { let context; hooks.beforeEach(function() { + this.pretender = new Pretender(); setResolverRegistry({ 'service:foo': Service.extend({ isFoo: true }), }); @@ -19,6 +24,10 @@ module('teardownContext', function(hooks) { return setupContext(context); }); + hooks.afterEach(function() { + this.pretender.shutdown(); + }); + test('it destroys any instances created', async function(assert) { let instance = context.owner.lookup('service:foo'); assert.notOk(instance.isDestroyed, 'precond - not destroyed'); @@ -45,4 +54,21 @@ module('teardownContext', function(hooks) { assert.strictEqual(getContext(), undefined, 'context is unset'); }); + + if (hasjQuery()) { + test('out of balance xhr semaphores are cleaned up on teardown', async function(assert) { + this.pretender.unhandledRequest = function(/* verb, path, request */) { + throw new Error(`Synchronous error from Pretender.prototype.unhandledRequest`); + }; + + ajax('/some/totally/invalid/url'); + + await teardownContext(context); + + assert.ok( + isSettled(), + `out of balance xhr semaphores are cleaned up on teardown: ${getSettledState()}` + ); + }); + } });