-
-
Notifications
You must be signed in to change notification settings - Fork 251
/
setup-context.ts
522 lines (450 loc) · 15.1 KB
/
setup-context.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
import { _backburner, run } from '@ember/runloop';
import { set, setProperties, get, getProperties } from '@ember/object';
import type Resolver from 'ember-resolver';
import { setOwner } from '@ember/application';
import buildOwner, { Owner } from './build-owner';
import { _setupAJAXHooks, _teardownAJAXHooks } from './settled';
import { _prepareOnerror } from './setup-onerror';
import Ember from 'ember';
import {
assert,
registerDeprecationHandler,
registerWarnHandler,
} from '@ember/debug';
import global from './global';
import { getResolver } from './resolver';
import { getApplication } from './application';
import { Promise } from './-utils';
import getTestMetadata from './test-metadata';
import {
registerDestructor,
associateDestroyableChild,
} from '@ember/destroyable';
import {
getDeprecationsForContext,
getDeprecationsDuringCallbackForContext,
DeprecationFailure,
} from './-internal/deprecations';
import {
getWarningsForContext,
getWarningsDuringCallbackForContext,
Warning,
} from './-internal/warnings';
// This handler exists to provide the underlying data to enable the following methods:
// * getDeprecations()
// * getDeprecationsDuringCallback()
// * getDeprecationsDuringCallbackForContext()
registerDeprecationHandler((message, options, next) => {
const context = getContext();
if (context === undefined) {
return;
}
getDeprecationsForContext(context).push({ message, options });
next.apply(null, [message, options]);
});
// This handler exists to provide the underlying data to enable the following methods:
// * getWarnings()
// * getWarningsDuringCallback()
// * getWarningsDuringCallbackForContext()
registerWarnHandler((message, options, next) => {
const context = getContext();
if (context === undefined) {
return;
}
getWarningsForContext(context).push({ message, options });
next.apply(null, [message, options]);
});
export interface BaseContext {
[key: string]: unknown;
}
/**
* The public API for the test context, which test authors can depend on being
* available.
*
* Note: this is *not* user-constructible; it becomes available by calling
* `setupContext()` with a `BaseContext`.
*/
export interface TestContext extends BaseContext {
owner: Owner;
set<T>(key: string, value: T): T;
setProperties<T extends Record<string, unknown>>(hash: T): T;
get(key: string): unknown;
getProperties(...args: string[]): Pick<BaseContext, string>;
pauseTest(): Promise<void>;
resumeTest(): void;
}
// eslint-disable-next-line require-jsdoc
export function isTestContext(context: BaseContext): context is TestContext {
return (
typeof context['pauseTest'] === 'function' &&
typeof context['resumeTest'] === 'function'
);
}
let __test_context__: BaseContext | undefined;
/**
Stores the provided context as the "global testing context".
Generally setup automatically by `setupContext`.
@public
@param {Object} context the context to use
*/
export function setContext(context: BaseContext): void {
__test_context__ = context;
}
/**
Retrive the "global testing context" as stored by `setContext`.
@public
@returns {Object} the previously stored testing context
*/
export function getContext(): BaseContext | undefined {
return __test_context__;
}
/**
Clear the "global testing context".
Generally invoked from `teardownContext`.
@public
*/
export function unsetContext(): void {
__test_context__ = undefined;
}
/**
* Returns a promise to be used to pauses the current test (due to being
* returned from the test itself). This is useful for debugging while testing
* or for test-driving. It allows you to inspect the state of your application
* at any point.
*
* The test framework wrapper (e.g. `ember-qunit` or `ember-mocha`) should
* ensure that when `pauseTest()` is used, any framework specific test timeouts
* are disabled.
*
* @public
* @returns {Promise<void>} resolves _only_ when `resumeTest()` is invoked
* @example <caption>Usage via ember-qunit</caption>
*
* import { setupRenderingTest } from 'ember-qunit';
* import { render, click, pauseTest } from '@ember/test-helpers';
*
*
* module('awesome-sauce', function(hooks) {
* setupRenderingTest(hooks);
*
* test('does something awesome', async function(assert) {
* await render(hbs`{{awesome-sauce}}`);
*
* // added here to visualize / interact with the DOM prior
* // to the interaction below
* await pauseTest();
*
* click('.some-selector');
*
* assert.equal(this.element.textContent, 'this sauce is awesome!');
* });
* });
*/
export function pauseTest(): Promise<void> {
let context = getContext();
if (!context || !isTestContext(context)) {
throw new Error(
'Cannot call `pauseTest` without having first called `setupTest` or `setupRenderingTest`.'
);
}
return context.pauseTest();
}
/**
Resumes a test previously paused by `await pauseTest()`.
@public
*/
export function resumeTest(): void {
let context = getContext();
if (!context || !isTestContext(context)) {
throw new Error(
'Cannot call `resumeTest` without having first called `setupTest` or `setupRenderingTest`.'
);
}
context.resumeTest();
}
/**
@private
@param {Object} context the test context being cleaned up
*/
function cleanup(context: BaseContext) {
_teardownAJAXHooks();
// SAFETY: this is intimate API *designed* for us to override.
(Ember as any).testing = false;
unsetContext();
}
/**
* Returns deprecations which have occured so far for a the current test context
*
* @public
* @returns {Array<DeprecationFailure>} An array of deprecation messages
* @example <caption>Usage via ember-qunit</caption>
*
* import { getDeprecations } from '@ember/test-helpers';
*
* module('awesome-sauce', function(hooks) {
* setupRenderingTest(hooks);
*
* test('does something awesome', function(assert) {
const deprecations = getDeprecations() // => returns deprecations which have occured so far in this test
* });
* });
*/
export function getDeprecations(): Array<DeprecationFailure> {
const context = getContext();
if (!context) {
throw new Error(
'[@ember/test-helpers] could not get deprecations if no test context is currently active'
);
}
return getDeprecationsForContext(context);
}
export type { DeprecationFailure };
/**
* Returns deprecations which have occured so far for a the current test context
*
* @public
* @param {Function} [callback] The callback that when executed will have its DeprecationFailure recorded
* @returns {Array<DeprecationFailure> | Promise<Array<DeprecationFailure>>} An array of deprecation messages
* @example <caption>Usage via ember-qunit</caption>
*
* import { getDeprecationsDuringCallback } from '@ember/test-helpers';
*
* module('awesome-sauce', function(hooks) {
* setupRenderingTest(hooks);
*
* test('does something awesome', function(assert) {
* const deprecations = getDeprecationsDuringCallback(() => {
* // code that might emit some deprecations
*
* }); // => returns deprecations which occured while the callback was invoked
* });
*
*
* test('does something awesome', async function(assert) {
* const deprecations = await getDeprecationsDuringCallback(async () => {
* // awaited code that might emit some deprecations
* }); // => returns deprecations which occured while the callback was invoked
* });
* });
*/
export function getDeprecationsDuringCallback(
callback: () => void
): Array<DeprecationFailure> | Promise<Array<DeprecationFailure>> {
const context = getContext();
if (!context) {
throw new Error(
'[@ember/test-helpers] could not get deprecations if no test context is currently active'
);
}
return getDeprecationsDuringCallbackForContext(context, callback);
}
/**
* Returns warnings which have occured so far for a the current test context
*
* @public
* @returns {Array<Warning>} An array of warnings
* @example <caption>Usage via ember-qunit</caption>
*
* import { getWarnings } from '@ember/test-helpers';
*
* module('awesome-sauce', function(hooks) {
* setupRenderingTest(hooks);
*
* test('does something awesome', function(assert) {
const warnings = getWarnings() // => returns warnings which have occured so far in this test
* });
* });
*/
export function getWarnings(): Array<Warning> {
const context = getContext();
if (!context) {
throw new Error(
'[@ember/test-helpers] could not get warnings if no test context is currently active'
);
}
return getWarningsForContext(context);
}
export type { Warning };
/**
* Returns warnings which have occured so far for a the current test context
*
* @public
* @param {Function} [callback] The callback that when executed will have its warnings recorded
* @returns {Array<Warning> | Promise<Array<Warning>>} An array of warnings information
* @example <caption>Usage via ember-qunit</caption>
*
* import { getWarningsDuringCallback } from '@ember/test-helpers';
* import { warn } from '@ember/debug';
*
* module('awesome-sauce', function(hooks) {
* setupRenderingTest(hooks);
*
* test('does something awesome', function(assert) {
* const warnings = getWarningsDuringCallback(() => {
* warn('some warning');
*
* }); // => returns warnings which occured while the callback was invoked
* });
*
* test('does something awesome', async function(assert) {
* warn('some warning');
*
* const warnings = await getWarningsDuringCallback(async () => {
* warn('some other warning');
* }); // => returns warnings which occured while the callback was invoked
* });
* });
*/
export function getWarningsDuringCallback(
callback: () => void
): Array<Warning> | Promise<Array<Warning>> {
const context = getContext();
if (!context) {
throw new Error(
'[@ember/test-helpers] could not get warnings if no test context is currently active'
);
}
return getWarningsDuringCallbackForContext(context, callback);
}
// This WeakMap is used to track whenever a component is rendered in a test so that we can throw
// assertions when someone uses `this.{set,setProperties}` while rendering a component.
export const ComponentRenderMap = new WeakMap<BaseContext, true>();
export const SetUsage = new WeakMap<BaseContext, Array<string>>();
/**
Used by test framework addons to setup the provided context for testing.
Responsible for:
- sets the "global testing context" to the provided context (`setContext`)
- create an owner object and set it on the provided context (e.g. `this.owner`)
- setup `this.set`, `this.setProperties`, `this.get`, and `this.getProperties` to the provided context
- setting up AJAX listeners
- setting up `pauseTest` (also available as `this.pauseTest()`) and `resumeTest` helpers
@public
@param {Object} context the context to setup
@param {Object} [options] options used to override defaults
@param {Resolver} [options.resolver] a resolver to use for customizing normal resolution
@returns {Promise<Object>} resolves with the context that was setup
*/
export default function setupContext(
context: BaseContext,
options: { resolver?: Resolver } = {}
): Promise<TestContext> {
// SAFETY: this is intimate API *designed* for us to override.
(Ember as any).testing = true;
setContext(context);
let testMetadata = getTestMetadata(context);
testMetadata.setupTypes.push('setupContext');
_backburner.DEBUG = true;
registerDestructor(context, cleanup);
_prepareOnerror(context);
return Promise.resolve()
.then(() => {
let application = getApplication();
if (application) {
return application.boot().then(() => {});
}
return;
})
.then(() => {
let { resolver } = options;
// This handles precendence, specifying a specific option of
// resolver always trumps whatever is auto-detected, then we fallback to
// the suite-wide registrations
//
// At some later time this can be extended to support specifying a custom
// engine or application...
if (resolver) {
return buildOwner(null, resolver);
}
return buildOwner(getApplication(), getResolver());
})
.then((owner) => {
associateDestroyableChild(context, owner);
Object.defineProperty(context, 'owner', {
configurable: true,
enumerable: true,
value: owner,
writable: false,
});
setOwner(context, owner);
Object.defineProperty(context, 'set', {
configurable: true,
enumerable: true,
value(key: string, value: unknown): unknown {
let ret = run(function () {
if (ComponentRenderMap.has(context)) {
assert(
'You cannot call `this.set` when passing a component to `render()` (the rendered component does not have access to the test context).'
);
} else {
let setCalls = SetUsage.get(context);
if (setCalls === undefined) {
setCalls = [];
SetUsage.set(context, setCalls);
}
setCalls?.push(key);
}
return set(context, key, value);
});
return ret;
},
writable: false,
});
Object.defineProperty(context, 'setProperties', {
configurable: true,
enumerable: true,
value(hash: { [key: string]: any }): { [key: string]: any } {
let ret = run(function () {
if (ComponentRenderMap.has(context)) {
assert(
'You cannot call `this.setProperties` when passing a component to `render()` (the rendered component does not have access to the test context)'
);
} else {
let setCalls = SetUsage.get(context);
if (SetUsage.get(context) === undefined) {
setCalls = [];
SetUsage.set(context, setCalls);
}
setCalls?.push(...Object.keys(hash));
}
return setProperties(context, hash);
});
return ret;
},
writable: false,
});
Object.defineProperty(context, 'get', {
configurable: true,
enumerable: true,
value(key: string): any {
return get(context, key);
},
writable: false,
});
Object.defineProperty(context, 'getProperties', {
configurable: true,
enumerable: true,
value(...args: string[]): Pick<BaseContext, string> {
return getProperties(context, args);
},
writable: false,
});
let resume: ((value?: unknown) => void) | undefined;
context['resumeTest'] = function resumeTest() {
assert(
'Testing has not been paused. There is nothing to resume.',
!!resume
);
resume();
global.resumeTest = resume = undefined;
};
context['pauseTest'] = function pauseTest() {
console.info('Testing paused. Use `resumeTest()` to continue.'); // eslint-disable-line no-console
return new Promise((resolve) => {
resume = resolve;
global.resumeTest = resumeTest;
});
};
_setupAJAXHooks();
return context as TestContext;
});
}