diff --git a/API.md b/API.md index eca456454..604a8dc34 100644 --- a/API.md +++ b/API.md @@ -58,6 +58,11 @@ * [getDeprecationsDuringCallback][54] * [Parameters][55] * [Examples][56] +* [getWarnings][57] + * [Examples][58] +* [getWarningsDuringCallback][59] + * [Parameters][60] + * [Examples][61] ## DOM Interaction Helpers @@ -80,7 +85,7 @@ to continue to emulate how actual browsers handle unfocusing a given element. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to unfocus (optional, default `document.activeElement`) +* `target` **([string][62] | [Element][63])** the element or selector to unfocus (optional, default `document.activeElement`) #### Examples @@ -90,7 +95,7 @@ Emulating blurring an input using `blur` blur('input'); ``` -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### click @@ -117,12 +122,12 @@ For focusable (e.g. form control) elements the following events are triggered The exact listing of events that are triggered may change over time as needed to continue to emulate how actual browsers handle clicking a given element. -Use the `options` hash to change the parameters of the [MouseEvents][60]. +Use the `options` hash to change the parameters of the [MouseEvents][65]. You can use this to specifiy modifier keys as well. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to click on +* `target` **([string][62] | [Element][63])** the element or selector to click on * `_options` **MouseEventInit** the options to be merged into the mouse events. (optional, default `{}`) #### Examples @@ -139,7 +144,7 @@ Emulating clicking a button and pressing the `shift` key simultaneously using `c click('button', { shiftKey: true }); ``` -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### doubleClick @@ -174,11 +179,11 @@ For focusable (e.g. form control) elements the following events are triggered The exact listing of events that are triggered may change over time as needed to continue to emulate how actual browsers handle clicking a given element. -Use the `options` hash to change the parameters of the [MouseEvents][60]. +Use the `options` hash to change the parameters of the [MouseEvents][65]. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to double-click on +* `target` **([string][62] | [Element][63])** the element or selector to double-click on * `_options` **MouseEventInit** the options to be merged into the mouse events (optional, default `{}`) #### Examples @@ -195,7 +200,7 @@ Emulating double clicking a button and pressing the `shift` key simultaneously u doubleClick('button', { shiftKey: true }); ``` -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### fillIn @@ -205,8 +210,8 @@ events on the specified target. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to enter text into -* `text` **[string][57]** the text to fill into the target element +* `target` **([string][62] | [Element][63])** the element or selector to enter text into +* `text` **[string][62]** the text to fill into the target element #### Examples @@ -216,7 +221,7 @@ Emulating filling an input with text using `fillIn` fillIn('input', 'hello world'); ``` -Returns **[Promise][59]\** resolves when the application is settled +Returns **[Promise][64]\** resolves when the application is settled ### focus @@ -235,7 +240,7 @@ to continue to emulate how actual browsers handle focusing a given element. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to focus +* `target` **([string][62] | [Element][63])** the element or selector to focus #### Examples @@ -245,7 +250,7 @@ Emulating focusing an input using `focus` focus('input'); ``` -Returns **[Promise][59]\** resolves when the application is settled +Returns **[Promise][64]\** resolves when the application is settled ### scrollTo @@ -253,9 +258,9 @@ Scrolls DOM element or selector to the given coordinates. #### Parameters -* `target` **([string][57] | [HTMLElement][61])** the element or selector to trigger scroll on -* `x` **[Number][62]** x-coordinate -* `y` **[Number][62]** y-coordinate +* `target` **([string][62] | [HTMLElement][66])** the element or selector to trigger scroll on +* `x` **[Number][67]** x-coordinate +* `y` **[Number][67]** y-coordinate #### Examples @@ -266,7 +271,7 @@ scrollTo('#my-long-div', 0, 0); // scroll to top scrollTo('#my-long-div', 0, 100); // scroll down ``` -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### select @@ -277,9 +282,9 @@ multiple attribute is set true on the HTMLSelectElement) then trigger #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector for the select element -* `options` **([string][57] | [Array][63]<[string][57]>)** the value/values of the items to select -* `keepPreviouslySelected` **[boolean][64]** a flag keep any existing selections (optional, default `false`) +* `target` **([string][62] | [Element][63])** the element or selector for the select element +* `options` **([string][62] | [Array][68]<[string][62]>)** the value/values of the items to select +* `keepPreviouslySelected` **[boolean][69]** a flag keep any existing selections (optional, default `false`) #### Examples @@ -293,7 +298,7 @@ select('select', ['apple', 'orange']); select('select', ['apple', 'orange'], true); ``` -Returns **[Promise][59]\** resolves when the application is settled +Returns **[Promise][64]\** resolves when the application is settled ### tap @@ -328,8 +333,8 @@ Use the `options` hash to change the parameters of the tap events. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to tap on -* `options` **[Object][65]** the options to be merged into the touch events (optional, default `{}`) +* `target` **([string][62] | [Element][63])** the element or selector to tap on +* `options` **[Object][70]** the options to be merged into the touch events (optional, default `{}`) #### Examples @@ -339,7 +344,7 @@ Emulating tapping a button using `tap` tap('button'); ``` -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### triggerEvent @@ -347,9 +352,9 @@ Triggers an event on the specified target. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to trigger the event on -* `eventType` **[string][57]** the type of event to trigger -* `options` **[Object][65]** additional properties to be set on the event +* `target` **([string][62] | [Element][63])** the element or selector to trigger the event on +* `eventType` **[string][62]** the type of event to trigger +* `options` **[Object][70]** additional properties to be set on the event #### Examples @@ -357,7 +362,7 @@ Using `triggerEvent` to upload a file When using `triggerEvent` to upload a file the `eventType` must be `change` and you must pass the `options` param as an object with a key `files` containing an array of -[Blob][66]. +[Blob][71]. ```javascript triggerEvent( @@ -369,8 +374,8 @@ triggerEvent( Using `triggerEvent` to upload a dropped file -When using `triggerEvent` to handle a dropped (via drag-and-drop) file, the `eventType` must be `drop`. Assuming your `drop` event handler uses the [DataTransfer API][67], -you must pass the `options` param as an object with a key of `dataTransfer`. The `options.dataTransfer` object should have a `files` key, containing an array of [File][68]. +When using `triggerEvent` to handle a dropped (via drag-and-drop) file, the `eventType` must be `drop`. Assuming your `drop` event handler uses the [DataTransfer API][72], +you must pass the `options` param as an object with a key of `dataTransfer`. The `options.dataTransfer` object should have a `files` key, containing an array of [File][73]. ```javascript triggerEvent( @@ -384,26 +389,26 @@ triggerEvent( ) ``` -Returns **[Promise][59]\** resolves when the application is settled +Returns **[Promise][64]\** resolves when the application is settled ### triggerKeyEvent Triggers a keyboard event of given type in the target element. -It also requires the developer to provide either a string with the [`key`][69] -or the numeric [`keyCode`][70] of the pressed key. +It also requires the developer to provide either a string with the [`key`][74] +or the numeric [`keyCode`][75] of the pressed key. Optionally the user can also provide a POJO with extra modifiers for the event. #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to trigger the event on +* `target` **([string][62] | [Element][63])** the element or selector to trigger the event on * `eventType` **(`"keydown"` | `"keyup"` | `"keypress"`)** the type of event to trigger -* `key` **([number][62] | [string][57])** the `keyCode`(number) or `key`(string) of the event being triggered -* `modifiers` **[Object][65]?** the state of various modifier keys (optional, default `DEFAULT_MODIFIERS`) +* `key` **([number][67] | [string][62])** the `keyCode`(number) or `key`(string) of the event being triggered +* `modifiers` **[Object][70]?** the state of various modifier keys (optional, default `DEFAULT_MODIFIERS`) - * `modifiers.ctrlKey` **[boolean][64]** if true the generated event will indicate the control key was pressed during the key event (optional, default `false`) - * `modifiers.altKey` **[boolean][64]** if true the generated event will indicate the alt key was pressed during the key event (optional, default `false`) - * `modifiers.shiftKey` **[boolean][64]** if true the generated event will indicate the shift key was pressed during the key event (optional, default `false`) - * `modifiers.metaKey` **[boolean][64]** if true the generated event will indicate the meta key was pressed during the key event (optional, default `false`) + * `modifiers.ctrlKey` **[boolean][69]** if true the generated event will indicate the control key was pressed during the key event (optional, default `false`) + * `modifiers.altKey` **[boolean][69]** if true the generated event will indicate the alt key was pressed during the key event (optional, default `false`) + * `modifiers.shiftKey` **[boolean][69]** if true the generated event will indicate the shift key was pressed during the key event (optional, default `false`) + * `modifiers.metaKey` **[boolean][69]** if true the generated event will indicate the meta key was pressed during the key event (optional, default `false`) #### Examples @@ -413,7 +418,7 @@ Emulating pressing the `ENTER` key on a button using `triggerKeyEvent` triggerKeyEvent('button', 'keydown', 'Enter'); ``` -Returns **[Promise][59]\** resolves when the application is settled unless awaitSettled is false +Returns **[Promise][64]\** resolves when the application is settled unless awaitSettled is false ### typeIn @@ -429,9 +434,9 @@ per character of the passed text (this may vary on some browsers). #### Parameters -* `target` **([string][57] | [Element][58])** the element or selector to enter text into -* `text` **[string][57]** the test to fill the element with -* `options` **[Object][65]** {delay: x} (default 50) number of milliseconds to wait per keypress (optional, default `{}`) +* `target` **([string][62] | [Element][63])** the element or selector to enter text into +* `text` **[string][62]** the test to fill the element with +* `options` **[Object][70]** {delay: x} (default 50) number of milliseconds to wait per keypress (optional, default `{}`) #### Examples @@ -441,7 +446,7 @@ Emulating typing in an input using `typeIn` typeIn('input', 'hello world'); ``` -Returns **[Promise][59]\** resolves when the application is settled +Returns **[Promise][64]\** resolves when the application is settled ## DOM Query Helpers @@ -454,9 +459,9 @@ Find the first element matched by the given selector. Equivalent to calling #### Parameters -* `selector` **[string][57]** the selector to search for +* `selector` **[string][62]** the selector to search for -Returns **[Element][58]** matched element or null +Returns **[Element][63]** matched element or null ### findAll @@ -466,15 +471,15 @@ of a `NodeList`. #### Parameters -* `selector` **[string][57]** the selector to search for +* `selector` **[string][62]** the selector to search for -Returns **[Array][63]** array of matched elements +Returns **[Array][68]** array of matched elements ### getRootElement Get the root element of the application under test (usually `#ember-testing`) -Returns **[Element][58]** the root element +Returns **[Element][63]** the root element ## Routing Helpers @@ -486,18 +491,18 @@ Navigate the application to the provided URL. #### Parameters -* `url` **[string][57]** The URL to visit (e.g. `/posts`) -* `options` **[object][65]** app boot options +* `url` **[string][62]** The URL to visit (e.g. `/posts`) +* `options` **[object][70]** app boot options -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### currentRouteName -Returns **[string][57]** the currently active route name +Returns **[string][62]** the currently active route name ### currentURL -Returns **[string][57]** the applications current url +Returns **[string][62]** the applications current url ## Rendering Helpers @@ -511,7 +516,7 @@ Renders the provided template and appends it to the DOM. * `template` **CompiledTemplate** the template to render -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### clearRender @@ -519,7 +524,7 @@ Clears any templates previously rendered. This is commonly used for confirming behavior that is triggered by teardown (e.g. `willDestroyElement`). -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ## Wait Helpers @@ -533,11 +538,11 @@ interim DOM states (e.g. loading states, pending promises, etc). #### Parameters -* `selector` **[string][57]** the selector to wait for -* `options` **[Object][65]?** the options to be used (optional, default `{}`) +* `selector` **[string][62]** the selector to wait for +* `options` **[Object][70]?** the options to be used (optional, default `{}`) - * `options.timeout` **[number][62]** the time to wait (in ms) for a match (optional, default `1000`) - * `options.count` **[number][62]** the number of elements that should match the provided selector (null means one or more) (optional, default `null`) + * `options.timeout` **[number][67]** the time to wait (in ms) for a match (optional, default `1000`) + * `options.count` **[number][67]** the number of elements that should match the provided selector (null means one or more) (optional, default `null`) #### Examples @@ -547,7 +552,7 @@ Waiting until a selector is rendered: await waitFor('.my-selector', { timeout: 2000 }) ``` -Returns **[Promise][59]<([Element][58] | [Array][63]<[Element][58]>)>** resolves when the element(s) appear on the page +Returns **[Promise][64]<([Element][63] | [Array][68]<[Element][63]>)>** resolves when the element(s) appear on the page ### waitUntil @@ -558,11 +563,11 @@ while *not* settled (e.g. "loading" or "pending" states). #### Parameters -* `callback` **[Function][71]** the callback to use for testing when waiting should stop -* `options` **[Object][65]?** options used to override defaults (optional, default `{}`) +* `callback` **[Function][76]** the callback to use for testing when waiting should stop +* `options` **[Object][70]?** options used to override defaults (optional, default `{}`) - * `options.timeout` **[number][62]** the maximum amount of time to wait (optional, default `1000`) - * `options.timeoutMessage` **[string][57]** the message to use in the reject on timeout (optional, default `'waitUntil timed out'`) + * `options.timeout` **[number][67]** the maximum amount of time to wait (optional, default `1000`) + * `options.timeoutMessage` **[string][62]** the message to use in the reject on timeout (optional, default `'waitUntil timed out'`) #### Examples @@ -574,14 +579,14 @@ return find('.my-selector').textContent.includes('something') }, { timeout: 2000 }) ``` -Returns **[Promise][59]** resolves with the callback value when it returns a truthy value +Returns **[Promise][64]** resolves with the callback value when it returns a truthy value ### settled Returns a promise that resolves when in a settled state (see `isSettled` for a definition of "settled state"). -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### isSettled @@ -591,7 +596,7 @@ Settled generally means that there are no pending timers, no pending waiters, no pending AJAX requests, and no current run loop. However, new settledness metrics may be added and used as they become available. -Returns **[boolean][64]** `true` if settled, `false` otherwise +Returns **[boolean][69]** `true` if settled, `false` otherwise ### getSettledState @@ -626,7 +631,7 @@ if there are pending transitions, this will be `true`, otherwise `false`. getDebugInfo method. -Returns **[Object][65]** object with properties for each of the metrics used to determine settledness +Returns **[Object][70]** object with properties for each of the metrics used to determine settledness ## Pause Helpers @@ -669,7 +674,7 @@ module('awesome-sauce', function(hooks) { }); ``` -Returns **[Promise][59]\** resolves *only* when `resumeTest()` is invoked +Returns **[Promise][64]\** resolves *only* when `resumeTest()` is invoked ### resumeTest @@ -745,18 +750,18 @@ Responsible for: #### Parameters -* `context` **[Object][65]** the context to setup -* `options` **[Object][65]?** options used to override defaults (optional, default `{}`) +* `context` **[Object][70]** the context to setup +* `options` **[Object][70]?** options used to override defaults (optional, default `{}`) * `options.resolver` **Resolver?** a resolver to use for customizing normal resolution -Returns **[Promise][59]<[Object][65]>** resolves with the context that was setup +Returns **[Promise][64]<[Object][70]>** resolves with the context that was setup ### getContext Retrive the "global testing context" as stored by `setContext`. -Returns **[Object][65]** the previously stored testing context +Returns **[Object][70]** the previously stored testing context ### setContext @@ -766,7 +771,7 @@ Generally setup automatically by `setupContext`. #### Parameters -* `context` **[Object][65]** the context to use +* `context` **[Object][70]** the context to use ### unsetContext @@ -786,12 +791,12 @@ Responsible for: #### Parameters -* `context` **[Object][65]** the context to setup -* `options` **[Object][65]?** options used to override defaults +* `context` **[Object][70]** the context to setup +* `options` **[Object][70]?** options used to override defaults - * `options.waitForSettled` **[boolean][64]** should the teardown wait for `settled()`ness (optional, default `true`) + * `options.waitForSettled` **[boolean][69]** should the teardown wait for `settled()`ness (optional, default `true`) -Returns **[Promise][59]\** resolves when settled +Returns **[Promise][64]\** resolves when settled ### setupRenderingContext @@ -814,9 +819,9 @@ element). #### Parameters -* `context` **[Object][65]** the context to setup for rendering +* `context` **[Object][70]** the context to setup for rendering -Returns **[Promise][59]<[Object][65]>** resolves with the context that was setup +Returns **[Promise][64]<[Object][70]>** resolves with the context that was setup ### getApplication @@ -847,9 +852,9 @@ Sets up the basic framework used by application tests. #### Parameters -* `context` **[Object][65]** the context to setup +* `context` **[Object][70]** the context to setup -Returns **[Promise][59]<[Object][65]>** resolves with the context that was setup +Returns **[Promise][64]<[Object][70]>** resolves with the context that was setup ### validateErrorHandler @@ -864,7 +869,7 @@ everything is on fire... #### Parameters -* `callback` **[Function][71]** the callback to validate (optional, default `Ember.onerror`) +* `callback` **[Function][76]** the callback to validate (optional, default `Ember.onerror`) #### Examples @@ -879,7 +884,7 @@ test('Ember.onerror is functioning properly', function(assert) { }); ``` -Returns **[Object][65]** object with `isValid` and `message` +Returns **[Object][70]** object with `isValid` and `message` ### setupOnerror @@ -889,7 +894,7 @@ without an `onError` argument. #### Parameters -* `onError` **[Function][71]** the onError function to be set on Ember.onerror +* `onError` **[Function][76]** the onError function to be set on Ember.onerror #### Examples @@ -951,7 +956,7 @@ const deprecations = getDeprecations() // => returns deprecations which have occ }); ``` -Returns **[Array][63]\** An array of deprecation messages +Returns **[Array][68]\** An array of deprecation messages ## getDeprecationsDuringCallback @@ -959,7 +964,7 @@ Returns deprecations which have occured so far for a the current test context ### Parameters -* `callback` **[Function][71]** ASd +* `callback` **CallableFunction?** The callback that when executed will have its DeprecationFailure recorded ### Examples @@ -987,7 +992,67 @@ module('awesome-sauce', function(hooks) { }); ``` -Returns **([Array][63]\ | [Promise][59]<[Array][63]\>)** An array of deprecation messages +Returns **([Array][68]\ | [Promise][64]<[Array][68]\>)** An array of deprecation messages + +## getWarnings + +Returns warnings which have occured so far for a the current test context + +### Examples + +Usage via ember-qunit + +```javascript +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 + }); +}); +``` + +Returns **[Array][68]\** An array of warnings + +## getWarningsDuringCallback + +Returns warnings which have occured so far for a the current test context + +### Parameters + +* `callback` **CallableFunction?** The callback that when executed will have its warnings recorded + +### Examples + +Usage via ember-qunit + +```javascript +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 + }); +}); +``` + +Returns **([Array][68]\ | [Promise][64]<[Array][68]\>)** An array of warnings information [1]: #dom-interaction-helpers @@ -1101,32 +1166,42 @@ Returns **([Array][63]\ | [Promise][59]<[Array][63]\ { return deprecations.slice(previousLength); // only return deprecations created as a result of the callback }); diff --git a/addon-test-support/@ember/test-helpers/-internal/is-promise.ts b/addon-test-support/@ember/test-helpers/-internal/is-promise.ts new file mode 100644 index 000000000..0614245fb --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-internal/is-promise.ts @@ -0,0 +1,15 @@ +/** + * + * detect if a value appears to be a promise + * + * @private + * @param {any} [maybePromise] the value being considered to be a promise + * @return {boolean} true if the value appears to be a promise, or false otherwise + */ +export default function (maybePromise: any): boolean { + return ( + maybePromise !== null && + (typeof maybePromise === 'object' || typeof maybePromise === 'function') && + typeof maybePromise.then === 'function' + ); +} diff --git a/addon-test-support/@ember/test-helpers/-internal/warnings.ts b/addon-test-support/@ember/test-helpers/-internal/warnings.ts new file mode 100644 index 000000000..2a9c70c59 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-internal/warnings.ts @@ -0,0 +1,107 @@ +import { BaseContext } from '../setup-context'; +import { registerWarnHandler } from '@ember/debug'; +import isPromise from './is-promise'; + +export interface WarningOptions { + id?: string; +} + +export interface Warning { + message: string; + options?: WarningOptions; +} + +// the WARNINGS data structure which is used to weakly associated warnings with +// the test context their occured within +const WARNINGS = new WeakMap>(); + +/** + * + * Provides the list of warnings associated with a given base context; + * + * @private + * @param {BaseContext} [context] the test context + * @return {Array} the warnings associated with the corresponding BaseContext; + */ +export function getWarningsForContext(context: BaseContext): Array { + if (!context) { + throw new TypeError( + `[@ember/test-helpers] could not get warnings for an invalid test context: '${context}'` + ); + } + + let warnings = WARNINGS.get(context); + + if (!Array.isArray(warnings)) { + warnings = []; + WARNINGS.set(context, warnings); + } + + return warnings; +} + +/** + * + * Provides the list of warnings associated with a given test context which + * occured only while a the provided callback is executed. This callback can be + * synchonous, or it can be an async function. + * + * @private + * @param {BaseContext} [context] the test context + * @param {CallableFunction} [callback] The callback that when executed will have its warnings recorded + * @return {Array} The warnings associated with the corresponding baseContext which occured while the CallbackFunction was executed + */ +export function getWarningsDuringCallbackForContext( + context: BaseContext, + callback: CallableFunction +): Array | Promise> { + if (!context) { + throw new TypeError( + `[@ember/test-helpers] could not get warnings for an invalid test context: '${context}'` + ); + } + + const warnings = getWarningsForContext(context); + const previousLength = warnings.length; + + const result = callback(); + + if (isPromise(result)) { + return Promise.resolve(result).then(() => { + return warnings.slice(previousLength); // only return warnings created as a result of the callback + }); + } else { + return warnings.slice(previousLength); // only return warnings created as a result of the callback + } +} + +// This provides (when the environment supports) queryParam support for warnings: +// * squelch warnings by name via: `/tests/index.html?disabledWarnings=this-property-fallback,some-other-thing` +// * enable a debuggger when a warning by a specific name is encountered via: `/tests/index.html?debugWarnings=some-other-thing` when the +if (typeof URLSearchParams !== 'undefined') { + const queryParams = new URLSearchParams(document.location.search.substring(1)); + const disabledWarnings = queryParams.get('disabledWarnings'); + const debugWarnings = queryParams.get('debugWarnings'); + + // When using `/tests/index.html?disabledWarnings=this-property-fallback,some-other-thing` + // those warnings will be squelched + if (disabledWarnings) { + registerWarnHandler((message, options, next) => { + if (!disabledWarnings.includes(options.id)) { + next.apply(null, [message, options]); + } + }); + } + + // When using `/tests/index.html?debugWarnings=some-other-thing` when the + // `some-other-thing` warning is triggered, this `debugger` will be hit` + if (debugWarnings) { + registerWarnHandler((message, options, next) => { + if (debugWarnings.includes(options.id)) { + debugger; // eslint-disable-line no-debugger + } + + next.apply(null, [message, options]); + }); + } +} diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index 54632fbf0..5a1fe55b4 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -9,6 +9,8 @@ export { resumeTest, getDeprecations, getDeprecationsDuringCallback, + getWarnings, + getWarningsDuringCallback, } from './setup-context'; export { default as teardownContext } from './teardown-context'; export { default as setupRenderingContext, render, clearRender } from './setup-rendering-context'; diff --git a/addon-test-support/@ember/test-helpers/setup-context.ts b/addon-test-support/@ember/test-helpers/setup-context.ts index b8f8d6eb7..60e8b730a 100644 --- a/addon-test-support/@ember/test-helpers/setup-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-context.ts @@ -7,7 +7,7 @@ import buildOwner, { Owner } from './build-owner'; import { _setupAJAXHooks, _teardownAJAXHooks } from './settled'; import { _prepareOnerror } from './setup-onerror'; import Ember from 'ember'; -import { assert, registerDeprecationHandler } from '@ember/debug'; +import { assert, registerDeprecationHandler, registerWarnHandler } from '@ember/debug'; import global from './global'; import { getResolver } from './resolver'; import { getApplication } from './application'; @@ -19,7 +19,16 @@ import { 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) { @@ -30,6 +39,20 @@ registerDeprecationHandler((message, options, next) => { 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]: any; } @@ -198,7 +221,7 @@ export function getDeprecations(): Array { * Returns deprecations which have occured so far for a the current test context * * @public - * @param {Function} callback ASd + * @param {CallableFunction} [callback] The callback that when executed will have its DeprecationFailure recorded * @returns {Array | Promise>} An array of deprecation messages * @example Usage via ember-qunit * @@ -236,6 +259,79 @@ export function getDeprecationsDuringCallback( return getDeprecationsDuringCallbackForContext(context, callback); } +/** + * Returns warnings which have occured so far for a the current test context + * + * @public + * @returns {Array} An array of warnings + * @example Usage via ember-qunit + * + * 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 { + 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); +} + +/** + * Returns warnings which have occured so far for a the current test context + * + * @public + * @param {CallableFunction} [callback] The callback that when executed will have its warnings recorded + * @returns {Array | Promise>} An array of warnings information + * @example Usage via ember-qunit + * + * 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: CallableFunction +): Array | Promise> { + 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); +} + /** Used by test framework addons to setup the provided context for testing. diff --git a/tests/unit/-internal/is-promise-test.js b/tests/unit/-internal/is-promise-test.js new file mode 100644 index 000000000..b053cef05 --- /dev/null +++ b/tests/unit/-internal/is-promise-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +import isPromise from '@ember/test-helpers/-internal/is-promise'; + +module('isPromise', function () { + test('detects promise-like like values', function (assert) { + assert.ok(isPromise(new Promise(() => {}))); + assert.ok(isPromise(Promise.resolve())); + assert.ok(isPromise({ then() {} })); + const functionObject = () => {}; + functionObject.then = () => {}; + assert.ok(isPromise(functionObject)); + }); + + test('it if a value is not a promise-like value', function (assert) { + assert.notOk(isPromise()); + assert.notOk(isPromise(undefined)); + assert.notOk(isPromise(null)); + assert.notOk(isPromise(1)); + assert.notOk(isPromise(NaN)); + assert.notOk(isPromise({})); + assert.notOk(isPromise(() => {})); + }); +}); diff --git a/tests/unit/setup-context-test.js b/tests/unit/setup-context-test.js index 797b9bb6e..876b38cf8 100644 --- a/tests/unit/setup-context-test.js +++ b/tests/unit/setup-context-test.js @@ -8,11 +8,14 @@ import { resumeTest, getDeprecations, getDeprecationsDuringCallback, + getWarnings, + getWarningsDuringCallback, setApplication, setResolver, getTestMetadata, } from '@ember/test-helpers'; import { getDeprecationsForContext } from '@ember/test-helpers/-internal/deprecations'; +import { getWarningsForContext } from '@ember/test-helpers/-internal/warnings'; import hasEmberVersion from '@ember/test-helpers/has-ember-version'; import { setResolverRegistry, @@ -24,7 +27,7 @@ import { assign } from '@ember/polyfills'; import App from '../../app'; import config from '../../config/environment'; import Ember from 'ember'; -import { deprecate } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; module('setupContext', function (hooks) { if (!hasEmberVersion(2, 4)) { @@ -178,6 +181,106 @@ module('setupContext', function (hooks) { assert.equal(bar.get('someProp'), 'derp', 'property updated'); }); + module('warnings', function () { + test('getWarningForContext', function (assert) { + assert.throws( + () => getWarningsForContext(), + /could not get warnings for an invalid test context: 'undefined'/ + ); + assert.throws( + () => getWarningsForContext(undefined), + /could not get warnings for an invalid test context: 'undefined'/ + ); + assert.throws( + () => getWarningsForContext(null), + /could not get warnings for an invalid test context: 'null'/ + ); + assert.throws( + () => getWarningsForContext(false), + /could not get warnings for an invalid test context: 'false'/ + ); + assert.throws( + () => getWarningsForContext(0), + /could not get warnings for an invalid test context: '0'/ + ); + + const contextA = {}; + const contextB = {}; + const contextC = {}; + + assert.deepEqual(getWarningsForContext(contextA), []); + assert.deepEqual(getWarningsForContext(contextB), []); + assert.deepEqual(getWarningsForContext(contextC), []); + + assert.equal(getWarningsForContext(contextA), getWarningsForContext(contextA)); + assert.equal(getWarningsForContext(contextB), getWarningsForContext(contextB)); + assert.equal(getWarningsForContext(contextC), getWarningsForContext(contextC)); + }); + + test('getApplication leak test 0', function (assert) { + assert.deepEqual(getWarnings(), []); + }); + + test('getWarnings', function (assert) { + assert.deepEqual(getWarnings(), []); + + warn('warning.0', true, { id: 'ember-test-helpers.warn.test' }); + warn('warning.1', false, { id: 'ember-test-helpers.warn.test' }); + warn('warning.2', false, { id: 'ember-test-helpers.warn.test' }); + + assert.deepEqual(getWarnings(), [ + { + message: 'warning.1', + options: { id: 'ember-test-helpers.warn.test' }, + }, + { + message: 'warning.2', + options: { id: 'ember-test-helpers.warn.test' }, + }, + ]); + }); + + test('getApplication leak test 0', function (assert) { + assert.deepEqual(getWarnings(), []); + }); + + test('getWarningForCallback async', async function (assert) { + warn('warning.0', false, { id: 'ember-test-helpers.warn.test' }); + + assert.deepEqual( + await getWarningsDuringCallback(async () => { + await Promise.resolve(); + assert.deepEqual(getWarnings(), [ + { + message: 'warning.0', + options: { id: 'ember-test-helpers.warn.test' }, + }, + ]); + }), + [] + ); + + assert.deepEqual(getWarnings(), [ + { + message: 'warning.0', + options: { id: 'ember-test-helpers.warn.test' }, + }, + ]); + + assert.deepEqual( + await getWarningsDuringCallback(async () => { + await Promise.resolve(); + warn('warning.1', false, { id: 'ember-test-helpers.warn.test' }); + assert.deepEqual(getWarnings(), [ + { message: 'warning.0', options: { id: 'ember-test-helpers.warn.test' } }, + { message: 'warning.1', options: { id: 'ember-test-helpers.warn.test' } }, + ]); + }), + [{ message: 'warning.1', options: { id: 'ember-test-helpers.warn.test' } }] + ); + }); + }); + module('deprecations', function () { test('getDeprecationsForContext', function (assert) { assert.throws(