Skip to content

Commit

Permalink
emberjs/warning-primitives
Browse files Browse the repository at this point in the history
Expose deprecation primitives

* `?disableWarnings` queryParam
* `?debugWarnings` queryParam
* `import { getWarnings, getWarningsDuringCallback } from ‘@ember/test-helpers’;`

This is a second step in allowing us to deprecate https://github.com/workmanw/ember-qunit-assert-helpers in-favor of these helpers simply being provided by default.

---

Context: This is motivated by 4.0 changes that are incompatible with the original add-on, and the time trade-off of resurrecting that add-on vs adopting its great ideas into mainline. Although users will need to upgrade their `@ember/test-helpers` + `ember-qunit` they will now get the original functionality, + async + bugfixes by default.

 I was lead down this path getting embroider working properly on `ember@canary`

---

 enables resolving emberjs/ember-string#259 amongst other related issues (basically anyone consuming ember-qunit-assert-helpers)
  • Loading branch information
stefanpenner committed Aug 3, 2021
1 parent ae7b1d4 commit 395bb36
Show file tree
Hide file tree
Showing 8 changed files with 531 additions and 113 deletions.
285 changes: 180 additions & 105 deletions API.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseContext } from '../setup-context';
import { registerDeprecationHandler } from '@ember/debug';
import isPromise from './is-promise';

export interface DeprecationOptions {
id: string;
Expand Down Expand Up @@ -68,10 +69,7 @@ export function getDeprecationsDuringCallbackForContext(

const result = callback();

if (
(result !== null && typeof result === 'object') ||
(typeof result === 'function' && typeof result.then === 'function')
) {
if (isPromise(result)) {
return Promise.resolve(result).then(() => {
return deprecations.slice(previousLength); // only return deprecations created as a result of the callback
});
Expand Down
14 changes: 14 additions & 0 deletions addon-test-support/@ember/test-helpers/-internal/is-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
*
* 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')
);
}
107 changes: 107 additions & 0 deletions addon-test-support/@ember/test-helpers/-internal/warnings.ts
Original file line number Diff line number Diff line change
@@ -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<BaseContext, Array<Warning>>();

/**
*
* Provides the list of warnings associated with a given base context;
*
* @private
* @param {BaseContext} [context] the test context
* @return {Array<Warning>} the warnings associated with the corresponding BaseContext;
*/
export function getWarningsForContext(context: BaseContext): Array<Warning> {
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<Warning>} The warnings associated with the corresponding baseContext which occured while the CallbackFunction was executed
*/
export function getWarningsDuringCallbackForContext(
context: BaseContext,
callback: CallableFunction
): Array<Warning> | Promise<Array<Warning>> {
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]);
});
}
}
2 changes: 2 additions & 0 deletions addon-test-support/@ember/test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
102 changes: 99 additions & 3 deletions addon-test-support/@ember/test-helpers/setup-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -198,7 +221,7 @@ export function getDeprecations(): Array<DeprecationFailure> {
* 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<DeprecationFailure> | Promise<Array<DeprecationFailure>>} An array of deprecation messages
* @example <caption>Usage via ember-qunit</caption>
*
Expand Down Expand Up @@ -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<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);
}

/**
* 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 DeprecationFailure 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: CallableFunction
): 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);
}

/**
Used by test framework addons to setup the provided context for testing.
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/-internal/is-promise-test.js
Original file line number Diff line number Diff line change
@@ -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.noOk(isPromise());
assert.noOk(isPromise(undefined));
assert.noOk(isPromise(null));
assert.noOk(isPromise(1));
assert.noOk(isPromise(NaN));
assert.noOk(isPromise({}));
assert.noOk(isPromise(() => {}));
});
});
Loading

0 comments on commit 395bb36

Please sign in to comment.