diff --git a/addon-test-support/ember-qunit/index.js b/addon-test-support/ember-qunit/index.js index 1c463544..120a7b02 100644 --- a/addon-test-support/ember-qunit/index.js +++ b/addon-test-support/ember-qunit/index.js @@ -1,5 +1,7 @@ export { default as moduleFor } from './legacy-2-x/module-for'; -export { default as moduleForComponent } from './legacy-2-x/module-for-component'; +export { + default as moduleForComponent, +} from './legacy-2-x/module-for-component'; export { default as moduleForModel } from './legacy-2-x/module-for-model'; export { default as QUnitAdapter } from './adapter'; export { module, test, skip, only, todo } from 'qunit'; @@ -25,6 +27,10 @@ import { teardownApplicationContext, validateErrorHandler, } from '@ember/test-helpers'; +import { + detectIfTestNotIsolated, + reportIfTestNotIsolated, +} from './test-isolation-validation'; export function setResolver() { deprecate( @@ -164,7 +170,8 @@ export function setupTestContainer() { let params = QUnit.urlParams; let containerVisibility = params.nocontainer ? 'hidden' : 'visible'; - let containerPosition = params.dockcontainer || params.devmode ? 'fixed' : 'relative'; + let containerPosition = + params.dockcontainer || params.devmode ? 'fixed' : 'relative'; if (params.devmode) { testContainer.className = ' full-screen'; @@ -175,7 +182,9 @@ export function setupTestContainer() { let qunitContainer = document.getElementById('qunit'); if (params.dockcontainer) { - qunitContainer.style.marginBottom = window.getComputedStyle(testContainer).height; + qunitContainer.style.marginBottom = window.getComputedStyle( + testContainer + ).height; } } @@ -228,6 +237,11 @@ export function setupEmberOnerrorValidation() { }); } +export function setupTestIsolationValidation() { + QUnit.testDone(detectIfTestNotIsolated); + QUnit.done(reportIfTestNotIsolated); +} + /** @method start @param {Object} [options] Options to be used for enabling/disabling behaviors @@ -243,6 +257,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.setupTestIsolationValidation] If `false` test isolation validation + will be disabled. */ export function start(options = {}) { if (options.loadTests !== false) { @@ -265,6 +281,13 @@ export function start(options = {}) { setupEmberOnerrorValidation(); } + if ( + typeof options.setupTestIsolationValidation !== 'undefined' && + options.setupTestIsolationValidation !== false + ) { + setupTestIsolationValidation(); + } + if (options.startTests !== false) { startTests(); } diff --git a/addon-test-support/ember-qunit/test-isolation-validation.js b/addon-test-support/ember-qunit/test-isolation-validation.js new file mode 100644 index 00000000..de8e52bd --- /dev/null +++ b/addon-test-support/ember-qunit/test-isolation-validation.js @@ -0,0 +1,48 @@ +import { run } from '@ember/runloop'; +import { isSettled } from '@ember/test-helpers'; + +const TESTS_NOT_ISOLATED = []; + +/** + * Detects if a specific test isn't isolated. A test is considered + * not isolated if it: + * + * - has no pending timers + * - is not in a runloop + * - has no pending AJAX requests + * - has no pending test waiters + * + * @function detectIfTestNotIsolated + * @param {Object} testInfo + * @param {string} testInfo.module The name of the test module + * @param {string} testInfo.name The test name + */ +export function detectIfTestNotIsolated({ module, name }) { + if (!isSettled()) { + TESTS_NOT_ISOLATED.push(`${module}: ${name}`); + run.cancelTimers(); + } +} + +/** + * Reports if a test isn't isolated. Please see above for what + * constitutes a test being isolated. + * + * @function reportIfTestNotIsolated + * @throws Error if tests are not isolated + */ +export function reportIfTestNotIsolated() { + if (TESTS_NOT_ISOLATED.length > 0) { + let leakyTests = TESTS_NOT_ISOLATED.slice(); + TESTS_NOT_ISOLATED.length = 0; + + throw new Error(getMessage(leakyTests.length, leakyTests.join('\n'))); + } +} + +export function getMessage(testCount, testsToReport) { + return `TESTS ARE NOT ISOLATED + The following (${testCount}) tests have one or more of pending timers, pending AJAX requests, pending test waiters, or are still in a runloop: \n + ${testsToReport} + `; +} diff --git a/testem.js b/testem.js index 1993f327..03946afc 100644 --- a/testem.js +++ b/testem.js @@ -9,12 +9,15 @@ module.exports = { 'Chrome' ], browser_args: { - Chrome: [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--remote-debugging-port=9222', - '--window-size=1440,900' - ] + Chrome: { + mode: 'ci', + args: [ + '--disable-gpu', + '--headless', + '--no-sandbox', + '--remote-debugging-port=9222', + '--window-size=1440,900' + ] + } } }; diff --git a/tests/unit/setup-test-isolation-validation.js b/tests/unit/setup-test-isolation-validation.js new file mode 100644 index 00000000..bfe4f4bf --- /dev/null +++ b/tests/unit/setup-test-isolation-validation.js @@ -0,0 +1,74 @@ +import Ember from 'ember'; +import { run } from '@ember/runloop'; +import { module, test } from 'qunit'; +import { + detectIfTestNotIsolated, + reportIfTestNotIsolated, + getMessage, +} from 'ember-qunit/test-isolation-validation'; + +module('setupTestIsolationValidation', function(hooks) { + hooks.beforeEach(function() { + this.cancelId = 0; + + this._waiter = () => { + return !this.isWaiterPending; + }; + + // In Ember < 2.8 `registerWaiter` expected to be bound to + // `Ember.Test` 😭 + // + // Once we have dropped support for < 2.8 we should swap this to + // use: + // + // import { registerWaiter } from '@ember/test'; + Ember.Test.registerWaiter(this._waiter); + }); + + hooks.afterEach(function() { + Ember.Test.unregisterWaiter(this._waiter); + + run.cancel(this.cancelId); + }); + + test('reportIfTestNotIsolated does not throw when test is isolated', function(assert) { + assert.expect(1); + + detectIfTestNotIsolated({ module: 'foo', name: 'bar' }); + reportIfTestNotIsolated(); + + assert.ok(true); + }); + + test('reportIfTestNotIsolated throws when test has pending timers', function(assert) { + assert.expect(1); + + this.cancelId = run.later(() => {}, 10); + + detectIfTestNotIsolated({ module: 'foo', name: 'bar' }); + + assert.throws( + function() { + reportIfTestNotIsolated(); + }, + Error, + getMessage(1, 'foo: bar') + ); + }); + + test('reportIfTestNotIsolated throws when test has test waiters', function(assert) { + assert.expect(1); + + this.isWaiterPending = true; + + detectIfTestNotIsolated({ module: 'foo', name: 'bar' }); + + assert.throws( + function() { + reportIfTestNotIsolated(); + }, + Error, + getMessage(1, 'foo: bar') + ); + }); +});