1 | /* eslint-disable no-console */
|
2 | import * as QUnit from 'qunit';
|
3 | import { _cancelTimers as cancelTimers } from '@ember/runloop';
|
4 | import { waitUntil, isSettled, getSettledState } from '@ember/test-helpers';
|
5 | import { getDebugInfo } from '@ember/test-helpers';
|
6 |
|
7 | /**
|
8 | * Detects if a specific test isn't isolated. A test is considered
|
9 | * not isolated if it:
|
10 | *
|
11 | * - has pending timers
|
12 | * - is in a runloop
|
13 | * - has pending AJAX requests
|
14 | * - has pending test waiters
|
15 | *
|
16 | * @function detectIfTestNotIsolated
|
17 | * @param {Object} testInfo
|
18 | * @param {string} testInfo.module The name of the test module
|
19 | * @param {string} testInfo.name The test name
|
20 | */
|
21 | export function detectIfTestNotIsolated(test, message = '') {
|
22 | if (!isSettled()) {
|
23 | let { debugInfo } = getSettledState();
|
24 |
|
25 | console.group(`${test.module.name}: ${test.testName}`);
|
26 | debugInfo.toConsole();
|
27 | console.groupEnd();
|
28 |
|
29 | test.expected++;
|
30 | test.assert.pushResult({
|
31 | result: false,
|
32 | message: `${message} \nMore information has been printed to the console. Please use that information to help in debugging.\n\n`,
|
33 | });
|
34 | }
|
35 | }
|
36 |
|
37 | /**
|
38 | * Installs a hook to detect if a specific test isn't isolated.
|
39 | * This hook is installed by patching into the `test.finish` method,
|
40 | * which allows us to be very precise as to when the detection occurs.
|
41 | *
|
42 | * @function installTestNotIsolatedHook
|
43 | * @param {number} delay the delay delay to use when checking for isolation validation
|
44 | */
|
45 | export function installTestNotIsolatedHook(delay = 50) {
|
46 | if (!getDebugInfo()) {
|
47 | return;
|
48 | }
|
49 |
|
50 | let test = QUnit.config.current;
|
51 | let finish = test.finish;
|
52 | let pushFailure = test.pushFailure;
|
53 |
|
54 | test.pushFailure = function (message) {
|
55 | if (message.indexOf('Test took longer than') === 0) {
|
56 | detectIfTestNotIsolated(this, message);
|
57 | } else {
|
58 | return pushFailure.apply(this, arguments);
|
59 | }
|
60 | };
|
61 |
|
62 | // We're hooking into `test.finish`, which utilizes internal ordering of
|
63 | // when a test's hooks are invoked. We do this mainly because we need
|
64 | // greater precision as to when to detect and subsequently report if the
|
65 | // test is isolated.
|
66 | //
|
67 | // We looked at using:
|
68 | // - `afterEach`
|
69 | // - the ordering of when the `afterEach` is called is not easy to guarantee
|
70 | // (ancestor `afterEach`es have to be accounted for too)
|
71 | // - `QUnit.on('testEnd')`
|
72 | // - is executed too late; the test is already considered done so
|
73 | // we're unable to push a new assert to fail the current test
|
74 | // - 'QUnit.done'
|
75 | // - it detaches the failure from the actual test that failed, making it
|
76 | // more confusing to the end user.
|
77 | test.finish = function () {
|
78 | let doFinish = () => finish.apply(this, arguments);
|
79 |
|
80 | if (isSettled()) {
|
81 | return doFinish();
|
82 | } else {
|
83 | return waitUntil(isSettled, { timeout: delay })
|
84 | .catch(() => {
|
85 | // we consider that when waitUntil times out, you're in a state of
|
86 | // test isolation violation. The nature of the error is irrelevant
|
87 | // in this case, and we want to allow the error to fall through
|
88 | // to the finally, where cleanup occurs.
|
89 | })
|
90 | .finally(() => {
|
91 | detectIfTestNotIsolated(
|
92 | this,
|
93 | 'Test is not isolated (async execution is extending beyond the duration of the test).'
|
94 | );
|
95 |
|
96 | // canceling timers here isn't perfect, but is as good as we can do
|
97 | // to attempt to prevent future tests from failing due to this test's
|
98 | // leakage
|
99 | cancelTimers();
|
100 |
|
101 | return doFinish();
|
102 | });
|
103 | }
|
104 | };
|
105 | }
|