From 659f20f702873c942d6476301730107c71a566c4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 13 Mar 2021 23:50:23 -0500 Subject: [PATCH] feat: utilize ember-window-mock for the underlying provider ...of the services. This is _instead of_ custom implementations that suited the needs of the maintainers of this package. ember-window-mock uses a recursive proxy that allows any api to be stubbed, regardless of its ability to be stubbed on the native object (ie: can't set a read-only property). fix: there is now URL validation on initial location.href values BREAKING CHANGE: invalid hrefs will throw an error --- README.md | 85 ++++++++---- addon-test-support/-private/-helpers.ts | 46 ------- addon-test-support/-private/document.ts | 11 -- addon-test-support/-private/navigator.ts | 22 ---- addon-test-support/-private/window.ts | 27 ---- addon-test-support/index.ts | 89 ++++++++++--- addon-test-support/window-mock-augments.ts | 55 ++++++++ addon/types.ts | 10 ++ addon/utils/proxy-service.ts | 4 + package.json | 8 +- tests/unit/services/browser/navigator-test.ts | 8 +- tests/unit/services/browser/window-test.ts | 121 ++++++++++++++---- yarn.lock | 50 +++++++- 13 files changed, 356 insertions(+), 180 deletions(-) delete mode 100644 addon-test-support/-private/-helpers.ts delete mode 100644 addon-test-support/-private/document.ts delete mode 100644 addon-test-support/-private/navigator.ts delete mode 100644 addon-test-support/-private/window.ts create mode 100644 addon-test-support/window-mock-augments.ts diff --git a/README.md b/README.md index 2b325860..e296cd10 100644 --- a/README.md +++ b/README.md @@ -44,33 +44,60 @@ export default class MyComponent extends Component { ### Testing -for fuller examples, see the tests directory. +_for fuller examples, see the tests directory_ -As with any service, if the default implementation is not suitable for testing, -it may be swapped out during the test. -```js -import Service from '@ember/service'; +There are two types of stubbing you may be interested in when working with browser services + - service overriding -module('Scenario Name', function (hooks) { - test('rare browser API', function (assert) { - let called = false; - - this.owner.register( - 'service:browser/window', - class TestWindow extends Service { - rareBrowserApi() { - called = true; - } - }, - ); + As with any service, if the default implementation is not suitable for testing, + it may be swapped out during the test. - this.owner.lookup('service:browser/window').rareBrowserApi(); + ```js + import Service from '@ember/service'; - assert.ok(called, 'the browser api was called'); - }); -}); -``` + module('Scenario Name', function (hooks) { + test('rare browser API', function (assert) { + let called = false; + + this.owner.register( + 'service:browser/window', + class TestWindow extends Service { + rareBrowserApi() { + called = true; + } + }, + ); + + this.owner.lookup('service:browser/window').rareBrowserApi(); + + assert.ok(called, 'the browser api was called'); + }); + }); + ``` + + - direct assignment + + This approach may be useful for deep-objects are complex interactions that otherwise would be + hard to reproduce via normal UI interaction. + + ```js + module('Scenario Name', function (hooks) { + test('rare browser API', function (assert) { + let service = this.owner.lookup('service:browser/window'); + let called = false; + + service.rareBrowserApi = () => (called = true); + + service.rareBrowserApi(); + + assert.ok(called, 'the browser api was called'); + }); + }); + ``` + + +There is also a shorthand for grouped "modules" in your tests: #### Window @@ -188,6 +215,8 @@ module('Examples: How to use the browser/document service', function (hooks) { [ember-window-mock](https://github.com/kaliber5/ember-window-mock) offers much of the same feature set as ember-browser-services. +_ember-browser-services builds on top of ember-window-mock and the two libraries can be used together_. + The main differences being: - ember-window-mock - smaller API surface @@ -206,8 +235,14 @@ The main differences being: - ember-browser-services - uses services instead of imports - multiple top-level browser APIs, instead of just `window` - - any changes to a service's implementation during a test are discarded after the test finishes - - adding additional behavior to the test version of an object requires something like: + - setting behavior on services can be done by simply assigning, thanks to ember-window-mock + + ```js + let service = this.owner.lookup('service:browser/navigator'); + + service.someApi = someValue; + ``` + - or adding additional behavior to the test version of an object can be done via familiar service extension like: ```js this.owner.register( @@ -228,7 +263,7 @@ The main differences being: Similarities / both addons: - use proxies to fallback to default browser API behavior - provide default stubs for commonly tested behavior (`location`, `localStorage`) - + - all state reset between tests ## Contributing diff --git a/addon-test-support/-private/-helpers.ts b/addon-test-support/-private/-helpers.ts deleted file mode 100644 index 48df9e74..00000000 --- a/addon-test-support/-private/-helpers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Service from '@ember/service'; - -import { proxyService } from 'ember-browser-services/utils/proxy-service'; - -import type { Class } from 'ember-browser-services/types'; - -// this usage of any is correct, because it literally could be *any*thing -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnknownObject = Record; - -export function maybeMake( - maybeImplementation: true | typeof Service | TestClass, - defaultImplementation: DefaultType, -): typeof Service { - if (maybeImplementation === true) { - return defaultImplementation; - } else if (typeof maybeImplementation === 'object') { - return proxyService(maybeImplementation); - } else if (maybeImplementation.prototype instanceof Service) { - return maybeImplementation; - } - - return defaultImplementation; -} - -export function testableVersionOf< - // eslint-disable-next-line @typescript-eslint/ban-types - Real extends object, - // eslint-disable-next-line @typescript-eslint/ban-types - TestInstance extends object, - TestClass extends Class ->(real: Real, Klass: TestClass) { - let overrides = new Klass(); - - let proxied = new Proxy(overrides, { - get(target, propName, receiver) { - if (propName in target) { - return Reflect.get(target, propName, receiver); - } - - return Reflect.get(real, propName); - }, - }); - - return proxied; -} diff --git a/addon-test-support/-private/document.ts b/addon-test-support/-private/document.ts deleted file mode 100644 index 1028a8ce..00000000 --- a/addon-test-support/-private/document.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { proxyService } from 'ember-browser-services/utils/proxy-service'; - -export interface TestDocument { - title: string; -} - -class FakeDocument { - title = ''; -} - -export const FakeDocumentService = proxyService(FakeDocument); diff --git a/addon-test-support/-private/navigator.ts b/addon-test-support/-private/navigator.ts deleted file mode 100644 index 12751d92..00000000 --- a/addon-test-support/-private/navigator.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { proxyService } from 'ember-browser-services/utils/proxy-service'; -import { testableVersionOf } from './-helpers'; - -export type TestNavigator = typeof fakeNavigator; - -export const fakeMediaDevices = testableVersionOf( - navigator.mediaDevices, - class TestMediaDevices { - // overrides deliberately left empty - }, -); - -export const fakeNavigator = testableVersionOf( - Navigator, - class TestClass { - get mediaDevices() { - return fakeMediaDevices; - } - }, -); - -export const FakeNavigatorService = proxyService(fakeNavigator); diff --git a/addon-test-support/-private/window.ts b/addon-test-support/-private/window.ts deleted file mode 100644 index 4145c26d..00000000 --- a/addon-test-support/-private/window.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { proxyService } from 'ember-browser-services/utils/proxy-service'; - -interface TestLocation extends Partial { - href?: string; -} - -export interface TestWindow { - parent?: TestWindow; - top?: TestWindow; - location?: TestLocation; -} - -class FakeLocation { - href = ''; - - replace(href: string) { - this.href = href; - } -} - -class FakeWindow { - location = new FakeLocation(); - top = this; - parent = this; -} - -export const FakeWindowService = proxyService(FakeWindow); diff --git a/addon-test-support/index.ts b/addon-test-support/index.ts index 52e7a598..e963fca6 100644 --- a/addon-test-support/index.ts +++ b/addon-test-support/index.ts @@ -1,30 +1,38 @@ -import { FakeDocumentService, TestDocument } from './-private/document'; +import window from 'ember-window-mock'; +import Service from '@ember/service'; +import { proxyService } from 'ember-browser-services/utils/proxy-service'; +import { setupWindowMock } from 'ember-window-mock/test-support'; + import { FakeLocalStorageService } from './-private/local-storage'; -import { FakeWindowService, TestWindow } from './-private/window'; -import { FakeNavigatorService, TestNavigator } from './-private/navigator'; -import { maybeMake } from './-private/-helpers'; -import type Service from '@ember/service'; import type { TestContext } from 'ember-test-helpers'; +import type { RecursivePartial } from 'ember-browser-services/types'; +import { patchWindow } from './window-mock-augments'; type Fakes = { - window?: boolean | TestWindow | typeof Service; + window?: boolean | typeof Service | RecursivePartial; localStorage?: boolean; - document?: boolean | TestDocument | typeof Service; - navigator?: boolean | TestNavigator; + document?: boolean | typeof Service | RecursivePartial; + navigator?: boolean | RecursivePartial; }; export function setupBrowserFakes(hooks: NestedHooks, options: Fakes): void { + setupWindowMock(hooks); + hooks.beforeEach(function (this: TestContext) { if (options.window) { - this.owner.register('service:browser/window', maybeMake(options.window, FakeWindowService)); + // default, can still be overwritten + // see: https://github.com/kaliber5/ember-window-mock/issues/175 + let patched = patchWindow(window); + let service = maybeMake(options.window, patched); + + this.owner.register('service:browser/window', service); } if (options.document) { - this.owner.register( - 'service:browser/document', - maybeMake(options.document, FakeDocumentService), - ); + let service = maybeMake(options.document, window.document); + + this.owner.register('service:browser/document', service); } if (options.localStorage) { @@ -32,10 +40,57 @@ export function setupBrowserFakes(hooks: NestedHooks, options: Fakes): void { } if (options.navigator) { - this.owner.register( - 'service:browser/navigator', - maybeMake(options.navigator, FakeNavigatorService), - ); + let service = maybeMake(options.navigator, window.navigator); + + this.owner.register('service:browser/navigator', service); } }); } + +// this usage of any is correct, because it literally could be *any*thing +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type UnknownObject = Record; + +export function maybeMake( + maybeImplementation: true | typeof Service | TestClass | RecursivePartial, + target: DefaultType, +): DefaultType { + if (maybeImplementation === true) { + return proxyService(target); + } + + if (maybeImplementation.prototype instanceof Service) { + return target; + } + + if (typeof maybeImplementation === 'object') { + applyStub(target, maybeImplementation); + + return proxyService(target); + } + + return proxyService(target); +} + +// we are already using ember-window-mock, so the proxy internal to that package will +// "just handle" setting stuff on the window +// +// NOTE: +// - Location implementation is incomplete: +// https://github.com/kaliber5/ember-window-mock/blob/2b8fbf581fc65e7f5455cd291497a3fdc2efdaf5/addon-test-support/-private/mock/location.js#L23 +// - does not allow setting "origin" +function applyStub(root: any, partial?: any) { + if (!partial) return root; + + for (let key of Object.keys(partial)) { + let value = partial[key]; + + if (Array.isArray(value)) { + root[key] = value; + } else if (typeof value === 'object') { + applyStub(root[key], value); + } else { + root[key] = value; + } + } +} diff --git a/addon-test-support/window-mock-augments.ts b/addon-test-support/window-mock-augments.ts new file mode 100644 index 00000000..b18b2479 --- /dev/null +++ b/addon-test-support/window-mock-augments.ts @@ -0,0 +1,55 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import locationFactory from 'ember-window-mock/test-support/-private/mock/location'; +import window from 'ember-window-mock'; + +const AUGMENTS: Array = ['origin']; + +function createLocation(target?: Window) { + let initialHref = target?.location?.href ?? window.location.href; + let mockLocation = locationFactory(initialHref); + let values: any = {}; + + mockLocation.isPatchedLocation = true; + + return new Proxy(mockLocation, { + get(target, key, receiver) { + if (AUGMENTS.includes(key)) { + return values[key] ?? Reflect.get(target, key, receiver); + } + + return Reflect.get(target, key, receiver); + }, + + set(target, key, value, receiver) { + if (AUGMENTS.includes(key)) { + return (values[key] = value); + } + + return Reflect.set(target, key, value, receiver); + }, + }); +} + +export function patchWindow(target: any) { + let location = createLocation(target); + + let self: any = new Proxy(target, { + get(target, key, receiver) { + if (key === 'location') return location; + if (key === 'parent') return self; + if (key === 'top') return self; + + return Reflect.get(target, key, receiver); + }, + set(target, key, value, receiver) { + if (key === 'location') { + throw new Error(`location cannot be set on window`); + } + + return Reflect.set(target, key, value, receiver); + }, + }); + + return self; +} diff --git a/addon/types.ts b/addon/types.ts index ad1d2823..1fd070e6 100644 --- a/addon/types.ts +++ b/addon/types.ts @@ -11,3 +11,13 @@ export type NavigatorService = typeof _NavigatorService; export interface Class { new (...args: unknown[]): T; } + +// https://stackoverflow.com/a/51365037/356849 +/* eslint-disable @typescript-eslint/ban-types */ +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : T[P]; +}; diff --git a/addon/utils/proxy-service.ts b/addon/utils/proxy-service.ts index 75f1d9e9..76d5e227 100644 --- a/addon/utils/proxy-service.ts +++ b/addon/utils/proxy-service.ts @@ -76,6 +76,8 @@ export function proxyService( static create(injections: Parameters): ReturnType { let serviceInstance = class ProxiedService extends Service { + // @private + declare __browser_object__: BrowserAPI; /* * We cannot create the base Service, we must use a new one. * If we don't, we are unable to run tests in a legacy qunit environment @@ -88,6 +90,8 @@ export function proxyService( let browserObject = isConstructable(ObjectToProxy) ? new ObjectToProxy() : ObjectToProxy; + serviceInstance.__browser_object__ = browserObject; + return new Proxy(serviceInstance, instanceHandlerFor(browserObject)); } diff --git a/package.json b/package.json index 289fb92d..5a397235 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "dependencies": { "ember-cli-babel": "^7.26.6", "ember-cli-htmlbars": "^5.7.1", - "ember-cli-typescript": "^4.2.1" + "ember-cli-typescript": "^4.2.1", + "ember-window-mock": "^0.7.2" }, "devDependencies": { "@commitlint/cli": "^11.0.0", @@ -132,7 +133,8 @@ "remark-lint": "^8.0.0", "remark-preset-lint-recommended": "^5.0.0", "semantic-release": "^17.4.7", - "typescript": "^4.4.2" + "typescript": "^4.4.2", + "qunit-console-grouper": "^0.3.0" }, "remarkConfig": { "plugins": [ @@ -152,4 +154,4 @@ "node": "14.17.6", "yarn": "1.22.11" } -} +} \ No newline at end of file diff --git a/tests/unit/services/browser/navigator-test.ts b/tests/unit/services/browser/navigator-test.ts index 20c94492..024cf199 100644 --- a/tests/unit/services/browser/navigator-test.ts +++ b/tests/unit/services/browser/navigator-test.ts @@ -27,10 +27,14 @@ module('Service | browser/navigator', function (hooks) { module('for config: true', function (hooks) { setupBrowserFakes(hooks, { navigator: true }); - test('APIs fallback to browser APIs', function (assert) { + test('APIs fallback to browser APIs', async function (assert) { let service = getNavigatorService(this.owner); - assert.equal(service.mediaDevices.getUserMedia, navigator.mediaDevices.getUserMedia); + let serviceMedia = service.mediaDevices.getUserMedia; + let browserMedia = navigator.mediaDevices.getUserMedia; + + assert.equal(serviceMedia.name, browserMedia.name); + assert.equal(serviceMedia.prototype, browserMedia.prototype); }); }); diff --git a/tests/unit/services/browser/window-test.ts b/tests/unit/services/browser/window-test.ts index 1b30847a..f5531adc 100644 --- a/tests/unit/services/browser/window-test.ts +++ b/tests/unit/services/browser/window-test.ts @@ -10,40 +10,113 @@ module('Service | browser/window', function (hooks) { let service = this.owner.lookup('service:browser/window'); assert.equal(service.location, window.location); + assert.equal(service.top.location, window.top?.location); + assert.equal(service.parent.location, window.parent.location); }); }); - module('Examples', function (hooks) { - setupBrowserFakes(hooks, { - window: { - location: { href: '' }, - parent: { location: { href: '' } }, - }, - }); + module('invalid usage', function (hooks) { + setupBrowserFakes(hooks, { window: true }); - // if this test were to fail, the test suite would hang and timeout, because - // we can't run tests and change the href at the same time - test('can reset the href without causing a browser refresh', function (assert) { + test('href cannot be boolean', function (assert) { let service = this.owner.lookup('service:browser/window'); - // verify that the initial config works - assert.equal(service.location.href, ''); - assert.equal(service.parent.location.href, ''); + assert.throws(() => { + service.location.href = true; + }, /TypeError/); + }); + }); + + module('Examples', function () { + module('Stubbing location.href', function (hooks) { + setupBrowserFakes(hooks, { + window: { + location: { href: 'http://init.ial' }, + parent: { location: { href: 'http://init.ial' } }, + }, + }); + + // if this test were to fail, the test suite would hang and timeout, because + // we can't run tests and change the href at the same time + test('can reset the href without causing a browser refresh', function (assert) { + let service = this.owner.lookup('service:browser/window'); + + // verify that the initial config works + assert.equal(service.location.href, 'http://init.ial/', 'window.location.href'); + assert.equal( + service.parent.location.href, + 'http://init.ial/', + 'window.parent.location.href', + ); + + // potential real ways to redirect to the login app + let loginPath = 'https://example.com/login'; + + service.location.href = loginPath; + service.parent.location.href = loginPath; + + // We'll redirect away from the test if the replacement methods don't work / + // are incorrect / have spelling errors + assert.notEqual(service.location.href, window.location.href); + assert.notEqual(service.parent.location.href, window.parent.location.href); + + // verify that setting actually works + assert.equal(service.location.href, loginPath); + assert.equal(service.parent.location.href, loginPath); + }); + }); + + module('Stubbing location.origin', function (hooks) { + setupBrowserFakes(hooks, { + window: { + location: { origin: 'http://init.ial', href: 'http://init.ial' }, + }, + }); + + test('can read from the stubbed origin', function (assert) { + let service = this.owner.lookup('service:browser/window'); + + assert.equal(service.location.href, 'http://init.ial/', 'window.location.href'); + assert.equal(service.location.origin, 'http://init.ial', 'window.location.origin'); + }); + }); + }); + + module('related data is properly related', function () { + module('location', function (hooks) { + setupBrowserFakes(hooks, { + window: { + parent: { location: { href: 'http://init.ial' } }, + }, + }); + + test('it works', function (assert) { + let service = this.owner.lookup('service:browser/window'); + let loginPath = 'https://example.com/login'; + + service.parent.location.href = loginPath; + + assert.equal(service.location.href, loginPath); + assert.equal(service.location.href, service.parent.location.href); + assert.equal(service.location, service.parent.location); + assert.equal(service.location, service.top.location); + }); + }); - // potential real ways to redirect to the login app - let loginPath = 'https://example.com/login'; + module('origin', function (hooks) { + setupBrowserFakes(hooks, { + window: true, + }); - service.location.href = loginPath; - service.parent.location.href = loginPath; + test('it works', function (assert) { + let service = this.owner.lookup('service:browser/window'); - // We'll redirect away from the test if the replacement methods don't work / - // are incorrect / have spelling errors - assert.notEqual(service.location.href, window.location.href); - assert.notEqual(service.parent.location.href, window.parent.location.href); + assert.ok(service.location.origin); + service.parent.location.href = '/login'; - // verify that setting actually works - assert.equal(service.location.href, loginPath); - assert.equal(service.parent.location.href, loginPath); + assert.ok(service.location.origin); + assert.equal(service.location.origin, service.parent.location.origin); + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 2b5d7c8f..23ab8527 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3026,7 +3026,7 @@ babel-plugin-ember-data-packages-polyfill@^0.1.2: dependencies: "@ember-data/rfc395-data" "^0.0.4" -babel-plugin-ember-modules-api-polyfill@^3.5.0: +babel-plugin-ember-modules-api-polyfill@^3.4.0, babel-plugin-ember-modules-api-polyfill@^3.5.0: version "3.5.0" resolved "https://registry.npmjs.org/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-3.5.0.tgz#27b6087fac75661f779f32e60f94b14d0e9f6965" integrity sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA== @@ -3041,6 +3041,13 @@ babel-plugin-filter-imports@^4.0.0: "@babel/types" "^7.7.2" lodash "^4.17.15" +babel-plugin-htmlbars-inline-precompile@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-4.4.5.tgz#ca0fc6ea49fe13b0a91ff995ee381d33d421a4ef" + integrity sha512-7qnZTDm9uUQppOmEWjAyIPTQ54akEdd9PCIfbTJ8HNgUdekeKC+24uwd+M1ZTjUItby1iLy9maQOK3Wv9RjWJA== + dependencies: + babel-plugin-ember-modules-api-polyfill "^3.4.0" + babel-plugin-htmlbars-inline-precompile@^5.0.0: version "5.3.0" resolved "https://registry.npmjs.org/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-5.3.0.tgz#eeaff07c35415264aea4d6bafb5e71167f6ffb2f" @@ -4362,7 +4369,7 @@ cli-table3@^0.5.1: cli-table3@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== dependencies: object-assign "^4.1.0" @@ -5370,6 +5377,27 @@ ember-cli-get-component-path-option@^1.0.0: resolved "https://registry.npmjs.org/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771" integrity sha1-DXtZVVni+QUKvtgE8djv8bCLx3E= +ember-cli-htmlbars@^5.3.1: + version "5.6.5" + resolved "https://registry.npmjs.org/ember-cli-htmlbars/-/ember-cli-htmlbars-5.6.5.tgz#15a55e4e4f47869a5e95a1e49813f77fd22fb76e" + integrity sha512-Wl3AntESMmQoG//yKuu+/7qAOznYAwRgWU8ZOCOPaGdPFaFXD6SPd2SKpRW4BEox5KLBJZFH0e7b9m78IAzcUw== + dependencies: + "@ember/edition-utils" "^1.2.0" + babel-plugin-htmlbars-inline-precompile "^4.4.5" + broccoli-debug "^0.6.5" + broccoli-persistent-filter "^3.1.2" + broccoli-plugin "^4.0.3" + common-tags "^1.8.0" + ember-cli-babel-plugin-helpers "^1.1.1" + fs-tree-diff "^2.0.1" + hash-for-dep "^1.5.1" + heimdalljs-logger "^0.1.10" + json-stable-stringify "^1.0.1" + semver "^7.3.4" + silent-error "^1.1.1" + strip-bom "^4.0.0" + walk-sync "^2.2.0" + ember-cli-htmlbars@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/ember-cli-htmlbars/-/ember-cli-htmlbars-5.7.1.tgz#eb5b88c7d9083bc27665fb5447a9b7503b32ce4f" @@ -5838,6 +5866,15 @@ ember-try@^1.4.0: rsvp "^4.7.0" walk-sync "^1.1.3" +ember-window-mock@^0.7.2: + version "0.7.2" + resolved "https://registry.npmjs.org/ember-window-mock/-/ember-window-mock-0.7.2.tgz#9f43c6c6d3047ac4b8c85aa67c2912a6a2c76ce6" + integrity sha512-+zRvJbiiUkc2O9TocRG5REbx5DxPmBWU/zTBCrpc4Fdu4JVQucrs9JfMfl+HLWldjJEU6x5UEDg+PIi8VREZHg== + dependencies: + broccoli-funnel "^3.0.3" + ember-cli-babel "^7.23.0" + ember-cli-htmlbars "^5.3.1" + emit-function@0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/emit-function/-/emit-function-0.0.2.tgz#e3a50b3d61be1bf8ca88b924bf713157a5bec124" @@ -10152,7 +10189,7 @@ npmlog@^4.0.0, npmlog@^4.1.2: npmlog@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.0.tgz#e6a41b556e9b34cb29ea132294676c07acb30efb" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.0.tgz#e6a41b556e9b34cb29ea132294676c07acb30efb" integrity sha512-ftpIiLjerL2tUg3dCqN8pOSoB90gqZlzv/gaZoxHaKjeLClrfJIEQ1Pdxi6qSzflz916Bljdy8dTWQ4J7hAFSQ== dependencies: are-we-there-yet "^1.1.5" @@ -11044,6 +11081,13 @@ quick-temp@^0.1.2, quick-temp@^0.1.3, quick-temp@^0.1.5, quick-temp@^0.1.8: rimraf "^2.5.4" underscore.string "~3.3.4" +qunit-console-grouper@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/qunit-console-grouper/-/qunit-console-grouper-0.3.0.tgz#6b13de524fdb080af9519ca30d428d1c1d815f4a" + integrity sha512-uHg5kcjyEGX85Rh9mimWoXsqeGmJZxfNLqmVxA5O2xJW+JAlQ0r0JFA07lHqlOQcegs5CAhvioKUXpjicDra6g== + dependencies: + broccoli-funnel "^3.0.3" + qunit-dom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/qunit-dom/-/qunit-dom-2.0.0.tgz#c4d7f7676dbb57f54151b72f8366d862134cd1c0"