diff --git a/README.md b/README.md index 3dd93ea5..8e086254 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,59 @@ module('SidebarController', function(hooks) { }); ``` +If you want to reset the state within a test, you can explicitly call `reset`: + +```js +import window from 'ember-window-mock'; +import { setupWindowMock, reset } from 'ember-window-mock/test-support'; + +module('SidebarController', function(hooks) { + setupWindowMock(hooks); + + test('some test', function(assert) { + window.location.href = 'https://example.com'; + assert.strictEqual(window.location.hostname, 'example.com'); + + reset(); + + assert.strictEqual(window.location.hostname, 'localhost'); + }); +}); +``` + +### createMockedWindow() + +When all you need is mocking the global `window` object, the guide above has you covered. But there can be cases where you want to create a new mocked window object from scratch, for example to mock `window.parent` with a different window instance. This you can use the `createMockedWindow()` test helper for: + +```js +import window from 'ember-window-mock/test-support'; +import { createMockedWindow, setupWindowMock } from 'ember-window-mock/test-support'; + +module('SidebarController', function(hooks) { + setupWindowMock(hooks); + + test('app is running in iframe', function(assert) { + window.location.href = 'https://myapp.com'; + window.parent = createMockedWindow(); + window.parent.location.href = 'https://example.com'; + + // ... + }); +}); +``` + +`setupWindowMock()` will _not_ reset the state of any explicitly created mocked windows, but in most cases this is not needed, since as soon as the reference to that mocked window is not used anymore, it will not have any effects and regular garbage collection will dispose the object. However, if you need to reset the state explicitly _within_ a test, you can do so by passing the mocked window object to `reset()`: + +```js +import { createMockedWindow, reset } from 'ember-window-mock/test-support'; + +const mockedWindow = createMockedWindow(); +mockedWindow.localStorage.set('foo', 'bar'); +// do something +reset(mockedWindow); +// now mockedWindow is back to its original state again +``` + ### Test examples #### Mocking `window.location` diff --git a/ember-window-mock/index.d.ts b/ember-window-mock/index.d.ts index 3a06bcc7..37c26555 100644 --- a/ember-window-mock/index.d.ts +++ b/ember-window-mock/index.d.ts @@ -3,6 +3,9 @@ declare module 'ember-window-mock' { } declare module 'ember-window-mock/test-support' { - export function setupWindowMock(hooks: { afterEach: (fn: () => void) => void }): void; - export function reset(): void; + export function setupWindowMock(hooks: { + afterEach: (fn: () => void) => void; + }): void; + export function reset(window?: typeof window): void; + export function createMockedWindow(window?: typeof window): typeof window; } diff --git a/ember-window-mock/src/test-support/-private/setup-window-mock.js b/ember-window-mock/src/test-support/-private/setup-window-mock.js index ced332a7..4950574e 100644 --- a/ember-window-mock/src/test-support/-private/setup-window-mock.js +++ b/ember-window-mock/src/test-support/-private/setup-window-mock.js @@ -1,4 +1,4 @@ -import { reset, mockProxyHandler } from './window.js'; +import { reset, createWindowProxyHandler } from './window.js'; import { _setCurrentHandler } from '../../index.js'; // @@ -7,7 +7,7 @@ import { _setCurrentHandler } from '../../index.js'; // NOTE: the `hooks = self` is for mocha support // export default function setupWindowMock(hooks = self) { - hooks.beforeEach(() => _setCurrentHandler(mockProxyHandler)); + hooks.beforeEach(() => _setCurrentHandler(createWindowProxyHandler())); hooks.afterEach(() => { reset(); _setCurrentHandler(); diff --git a/ember-window-mock/src/test-support/-private/window.js b/ember-window-mock/src/test-support/-private/window.js index 42566c47..3699dbd1 100644 --- a/ember-window-mock/src/test-support/-private/window.js +++ b/ember-window-mock/src/test-support/-private/window.js @@ -2,87 +2,104 @@ import mockFunction from './mock/function.js'; import locationFactory from './mock/location.js'; import proxyFactory from './mock/proxy.js'; import Storage from './mock/storage.js'; +import mockedGlobalWindow from '../../index.js'; -const originalWindow = window; +function noop() {} +const _reset = Symbol('ember-window-mock:reset'); -let location = locationFactory(originalWindow.location.href); -let localStorage = new Storage(); -let sessionStorage = new Storage(); -let holder = {}; +export function createWindowProxyHandler(originalWindow = window) { + let holder; + let location; + let localStorage; + let sessionStorage; -function noop() {} + const reset = () => { + holder = {}; + location = locationFactory(originalWindow.location.href); + localStorage = new Storage(); + sessionStorage = new Storage(); + }; + + reset(); -export const mockProxyHandler = { - get(target, name, receiver) { - switch (name) { - case 'location': - return location; - case 'localStorage': - return localStorage; - case 'sessionStorage': - return sessionStorage; - case 'window': - return receiver; - case 'alert': - case 'confirm': - case 'prompt': - return name in holder ? holder[name] : noop; - case 'onerror': - case 'onunhandledrejection': - // Always return the original error handler - return Reflect.get(target, name); - default: - if (name in holder) { - return holder[name]; - } - if (typeof window[name] === 'function') { - return mockFunction(target[name], target); - } - if (typeof window[name] === 'object' && window[name] !== null) { - let proxy = proxyFactory(window[name]); - holder[name] = proxy; - return proxy; - } - return target[name]; - } - }, - set(target, name, value, receiver) { - switch (name) { - case 'location': - // setting window.location is equivalent to setting window.location.href - receiver.location.href = value; - return true; - case 'onerror': - case 'onunhandledrejection': - // onerror always must live on the real window object to work - return Reflect.set(target, name, value); - default: - holder[name] = value; - return true; - } - }, - has(target, prop) { - return prop in holder || prop in target; - }, - deleteProperty(target, prop) { - delete holder[prop]; - delete target[prop]; - return true; - }, - getOwnPropertyDescriptor(target, property) { - return ( - Reflect.getOwnPropertyDescriptor(holder, property) ?? - Reflect.getOwnPropertyDescriptor(target, property) - ); - }, - defineProperty(target, property, attributes) { - return Reflect.defineProperty(holder, property, attributes); - }, -}; + return { + get(target, name, receiver) { + switch (name) { + case _reset: + return reset; + case 'location': + return location; + case 'localStorage': + return localStorage; + case 'sessionStorage': + return sessionStorage; + case 'window': + return receiver; + case 'top': + case 'parent': + return holder[name] ?? receiver; + case 'alert': + case 'confirm': + case 'prompt': + return name in holder ? holder[name] : noop; + case 'onerror': + case 'onunhandledrejection': + // Always return the original error handler + return Reflect.get(target, name); + default: + if (name in holder) { + return holder[name]; + } + if (typeof window[name] === 'function') { + return mockFunction(target[name], target); + } + if (typeof window[name] === 'object' && window[name] !== null) { + let proxy = proxyFactory(window[name]); + holder[name] = proxy; + return proxy; + } + return target[name]; + } + }, + set(target, name, value, receiver) { + switch (name) { + case 'location': + // setting window.location is equivalent to setting window.location.href + receiver.location.href = value; + return true; + case 'onerror': + case 'onunhandledrejection': + // onerror always must live on the real window object to work + return Reflect.set(target, name, value); + default: + holder[name] = value; + return true; + } + }, + has(target, prop) { + return prop in holder || prop in target; + }, + deleteProperty(target, prop) { + delete holder[prop]; + delete target[prop]; + return true; + }, + getOwnPropertyDescriptor(target, property) { + return ( + Reflect.getOwnPropertyDescriptor(holder, property) ?? + Reflect.getOwnPropertyDescriptor(target, property) + ); + }, + defineProperty(target, property, attributes) { + return Reflect.defineProperty(holder, property, attributes); + }, + }; +} + +export function createMockedWindow(_window = window) { + return new Proxy(_window, createWindowProxyHandler(_window)); +} -export function reset() { - location = locationFactory(originalWindow.location.href); - localStorage = new Storage(); - sessionStorage = new Storage(); - holder = {}; +export function reset(_window = mockedGlobalWindow) { + _window[_reset]?.(); } diff --git a/ember-window-mock/src/test-support/index.js b/ember-window-mock/src/test-support/index.js index a569a5a4..f9da852a 100644 --- a/ember-window-mock/src/test-support/index.js +++ b/ember-window-mock/src/test-support/index.js @@ -1,2 +1,2 @@ -export { reset } from './-private/window.js'; +export { reset, createMockedWindow } from './-private/window.js'; export { default as setupWindowMock } from './-private/setup-window-mock.js'; diff --git a/test-app/tests/unit/create-mocked-window-test.ts b/test-app/tests/unit/create-mocked-window-test.ts new file mode 100644 index 00000000..b28e0777 --- /dev/null +++ b/test-app/tests/unit/create-mocked-window-test.ts @@ -0,0 +1,18 @@ +import { module } from 'qunit'; +import { + setupWindowMock, + createMockedWindow, + reset, +} from 'ember-window-mock/test-support'; +import { runWindowTests } from './run-window-tests'; + +module('create-mocked-window', function (hooks) { + setupWindowMock(hooks); + hooks.afterEach(function () { + reset(mockedWindow); + }); + + const mockedWindow = createMockedWindow(); + + runWindowTests(mockedWindow); +}); diff --git a/test-app/tests/unit/run-window-tests.ts b/test-app/tests/unit/run-window-tests.ts new file mode 100644 index 00000000..f1c588b2 --- /dev/null +++ b/test-app/tests/unit/run-window-tests.ts @@ -0,0 +1,581 @@ +import { createMockedWindow, reset } from 'ember-window-mock/test-support'; +import QUnit, { module, skip, test } from 'qunit'; +import sinon from 'sinon'; +import $ from 'jquery'; + +interface TestGlobals { + window_mock_test_property?: string; + testFn?: () => void; + testKey?: string; +} + +export function runWindowTests(_window: Window & typeof globalThis) { + const window: typeof _window & TestGlobals = _window; + + module('general properties', function () { + test('it proxies properties', function (assert) { + window.window_mock_test_property = 'foo'; + assert.strictEqual(window.window_mock_test_property, 'foo'); + delete window.window_mock_test_property; + }); + + test('it proxies functions', function (assert) { + window.focus(); + assert.ok(true); + }); + + test('it allows adding and deleting properties', function (assert) { + window.testKey = 'test value'; + assert.ok('testKey' in window); + delete window.testKey; + assert.notOk('testKey' in window); + }); + + test('it allows adding and deleting functions', function (assert) { + window.testFn = () => assert.ok(true); + assert.ok('testFn' in window); + window.testFn(); + delete window.testFn; + assert.notOk('testFn' in window); + }); + + // eslint-disable-next-line qunit/require-expect + test('method calls have the correct context', function (assert) { + assert.expect(1); + window.testFn = function () { + assert.strictEqual(this, window); + }; + window.testFn(); + }); + + test('it can call dispatchEvent', function (assert) { + const spy = sinon.spy(); + window.addEventListener('test-event', spy); + window.dispatchEvent(new Event('test-event')); + assert.ok(spy.calledOnce); + }); + + test('it proxies various null fields', function (assert) { + // NOTE: in some conditions these can be set by the navigator + assert.strictEqual(window.frameElement, null); + assert.strictEqual(window.opener, null); + assert.strictEqual(window.onbeforeunload, null); + }); + }); + + module('window.location', function () { + test('it defaults to window.location', function (assert) { + assert.strictEqual(window.location.href, window.location.href); + }); + + test('it mocks window.location.href', function (assert) { + window.location.href = 'http://www.example.com:8080/foo?q=bar#hash'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/foo?q=bar#hash', + ); + assert.strictEqual(window.location.host, 'www.example.com:8080'); + assert.strictEqual(window.location.hostname, 'www.example.com'); + assert.strictEqual(window.location.protocol, 'http:'); + assert.strictEqual(window.location.origin, 'http://www.example.com:8080'); + assert.strictEqual(window.location.port, '8080'); + assert.strictEqual(window.location.pathname, '/foo'); + assert.strictEqual(window.location.search, '?q=bar'); + assert.strictEqual(window.location.hash, '#hash'); + }); + + test('window.location.href supports relative URLs', function (assert) { + window.location.href = 'http://www.example.com:8080/foo?q=bar#hash'; + window.location.href = '/bar'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/bar', + ); + window.location.href = 'baz'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/baz', + ); + window.location.href = '/foo/bar'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/foo/bar', + ); + window.location.href = 'baz'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/foo/baz', + ); + window.location.href = '/foo/bar/'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/foo/bar/', + ); + window.location.href = 'baz'; + assert.strictEqual( + window.location.href, + 'http://www.example.com:8080/foo/bar/baz', + ); + window.location.href = '/'; + assert.strictEqual(window.location.href, 'http://www.example.com:8080/'); + }); + + test('it mocks window.location', function (assert) { + // @ts-expect-error - this actually works + // > Though Window.location is a read-only Location object, you can also assign a string to it. This means that you can work with location as if it were a string in most cases: location = 'http://www.example.com' is a synonym of location.href = 'http://www.example.com'. + // See https://developer.mozilla.org/en-US/docs/Web/API/Window/location + window.location = 'http://www.example.com'; + assert.strictEqual(window.location.href, 'http://www.example.com/'); + }); + + test('it mocks window.location.reload', function (assert) { + window.location.href = 'http://www.example.com'; + window.location.reload(); + assert.strictEqual(window.location.href, 'http://www.example.com/'); + }); + + test('it mocks window.location.replace', function (assert) { + window.location.href = 'http://www.example.com'; + window.location.replace('http://www.emberjs.com'); + assert.strictEqual(window.location.href, 'http://www.emberjs.com/'); + }); + + test('it mocks window.location.assign', function (assert) { + window.location.href = 'http://www.example.com'; + window.location.assign('http://www.emberjs.com'); + assert.strictEqual(window.location.href, 'http://www.emberjs.com/'); + }); + + test('it mocks window.location.toString()', function (assert) { + window.location.href = 'http://www.example.com'; + assert.strictEqual(window.location.toString(), 'http://www.example.com/'); + }); + + test('it mocks pathname', function (assert) { + window.location.href = 'http://www.example.com'; + window.location.pathname = '/foo/'; + assert.strictEqual(window.location.href, 'http://www.example.com/foo/'); + }); + + module('parent', function () { + test('parent matches window by default', function (assert) { + assert.strictEqual(window.parent, window); + assert.strictEqual(window.parent.location.href, window.location.href); + + window.location.href = 'http://www.example.com'; + + assert.strictEqual(window.parent, window); + assert.strictEqual(window.parent.location.href, window.location.href); + }); + + test('parent can be mocked', function (assert) { + window.location.href = 'http://www.example.com'; + window.parent = createMockedWindow(); + window.parent.location.href = 'http://www.example2.com'; + + assert.notStrictEqual(window.parent, window); + assert.strictEqual(window.location.href, 'http://www.example.com/'); + assert.strictEqual( + window.parent.location.href, + 'http://www.example2.com/', + ); + }); + }); + + module('top', function () { + test('top matches window by default', function (assert) { + assert.strictEqual(window.top, window); + assert.strictEqual(window.top!.location.href, window.location.href); + + window.location.href = 'http://www.example.com'; + + assert.strictEqual(window.top, window); + assert.strictEqual(window.top!.location.href, window.location.href); + }); + + // we cannot mock window.top as a non-writable and non-configurable property + }); + }); + + module('blocking dialogs', function () { + test('it replaces alert with noop', function (assert) { + assert.strictEqual(window.alert('foo'), undefined); + }); + + test('it replaces confirm with noop', function (assert) { + assert.strictEqual(window.confirm('foo'), undefined); + }); + + test('it replaces prompt with prompt', function (assert) { + assert.strictEqual(window.prompt('foo'), undefined); + }); + + test('it can stub alert', function (assert) { + const spy = sinon.spy(); + window.alert = spy; + window.alert('foo'); + assert.ok(spy.calledOnce); + assert.ok(spy.calledWith('foo')); + }); + + test('it can stub confirm', function (assert) { + const stub = sinon.stub().returns(true); + window.confirm = stub; + const result = window.confirm('foo'); + assert.ok(stub.calledOnce); + assert.ok(stub.calledWith('foo')); + assert.true(result); + }); + + test('it can stub prompt', function (assert) { + const stub = sinon.stub().returns('bar'); + window.prompt = stub; + const result = window.prompt('foo'); + assert.ok(stub.calledOnce); + assert.ok(stub.calledWith('foo')); + assert.strictEqual(result, 'bar'); + }); + }); + + module('localStorage', function () { + test('it mocks window.localStorage.length', function (assert) { + assert.strictEqual(window.localStorage.length, 0); + + window.localStorage.setItem('a', 'x'); + assert.strictEqual(window.localStorage.length, 1); + + window.localStorage.setItem('b', 'y'); + assert.strictEqual(window.localStorage.length, 2); + + window.localStorage.clear(); + assert.strictEqual(window.localStorage.length, 0); + }); + + test('it mocks window.localStorage.getItem', function (assert) { + assert.strictEqual(window.localStorage.getItem('a'), null); + + window.localStorage.setItem('a', 'x'); + assert.strictEqual(window.localStorage.getItem('a'), 'x'); + + window.localStorage.clear(); + assert.strictEqual(window.localStorage.getItem('a'), null); + }); + + test('it mocks window.localStorage.key', function (assert) { + assert.strictEqual(window.localStorage.key(0), null); + + window.localStorage.setItem('a', 'x'); + assert.strictEqual(window.localStorage.key(0), 'a'); + + window.localStorage.setItem('b', 'y'); + assert.strictEqual(window.localStorage.key(0), 'a'); + assert.strictEqual(window.localStorage.key(1), 'b'); + + window.localStorage.clear(); + assert.strictEqual(window.localStorage.key(0), null); + }); + + test('it mocks window.localStorage.removeItem', function (assert) { + window.localStorage.setItem('a', 'x'); + window.localStorage.setItem('b', 'y'); + assert.strictEqual(window.localStorage.getItem('a'), 'x'); + assert.strictEqual(window.localStorage.getItem('b'), 'y'); + + window.localStorage.removeItem('a'); + assert.strictEqual(window.localStorage.getItem('a'), null); + assert.strictEqual(window.localStorage.getItem('b'), 'y'); + + window.localStorage.removeItem('y'); + assert.strictEqual(window.localStorage.getItem('b'), 'y'); + }); + + test('it mocks window.localStorage.clear', function (assert) { + window.localStorage.setItem('a', 'x'); + window.localStorage.setItem('b', 'y'); + + assert.strictEqual(window.localStorage.length, 2); + + window.localStorage.clear(); + + assert.strictEqual(window.localStorage.length, 0); + assert.strictEqual(window.localStorage.getItem('a'), null); + assert.strictEqual(window.localStorage.getItem('b'), null); + }); + + test('it clears localStorage on reset', function (assert) { + window.localStorage.setItem('c', 'z'); + assert.strictEqual(window.localStorage.getItem('c'), 'z'); + assert.strictEqual(window.localStorage.key(0), 'c'); + assert.strictEqual(window.localStorage.length, 1); + + reset(window); + + assert.strictEqual(window.localStorage.getItem('c'), null); + assert.strictEqual(window.localStorage.key(0), null); + assert.strictEqual(window.localStorage.length, 0); + }); + }); + + module('sessionStorage', function () { + test('it mocks window.sessionStorage.length', function (assert) { + assert.strictEqual(window.sessionStorage.length, 0); + + window.sessionStorage.setItem('a', 'x'); + assert.strictEqual(window.sessionStorage.length, 1); + }); + + test('it mocks window.sessionStorage.getItem', function (assert) { + assert.strictEqual(window.sessionStorage.getItem('a'), null); + + window.sessionStorage.setItem('a', 'x'); + assert.strictEqual(window.sessionStorage.getItem('a'), 'x'); + }); + + test('it mocks window.sessionStorage.key', function (assert) { + assert.strictEqual(window.sessionStorage.key(0), null); + + window.sessionStorage.setItem('a', 'x'); + assert.strictEqual(window.sessionStorage.key(0), 'a'); + }); + + test('it mocks window.sessionStorage.removeItem', function (assert) { + window.sessionStorage.setItem('a', 'x'); + + window.sessionStorage.removeItem('a'); + assert.strictEqual(window.sessionStorage.getItem('a'), null); + }); + + test('it mocks window.sessionStorage.clear', function (assert) { + window.sessionStorage.setItem('a', 'x'); + + window.sessionStorage.clear(); + assert.strictEqual(window.sessionStorage.getItem('a'), null); + }); + + test('it clears sessionStorage on reset', function (assert) { + window.sessionStorage.setItem('c', 'z'); + assert.strictEqual(window.sessionStorage.getItem('c'), 'z'); + assert.strictEqual(window.sessionStorage.key(0), 'c'); + assert.strictEqual(window.sessionStorage.length, 1); + + reset(window); + + assert.strictEqual(window.sessionStorage.getItem('c'), null); + assert.strictEqual(window.sessionStorage.key(0), null); + assert.strictEqual(window.sessionStorage.length, 0); + }); + }); + + module('window.navigator', function () { + module('userAgent', function () { + test('it works', function (assert) { + const ua = navigator.userAgent; // not using window-mock + assert.strictEqual(window.navigator.userAgent, ua); + }); + + test('it can be overridden', function (assert) { + // @ts-expect-error we can override that with the mocked window + window.navigator.userAgent = 'mockUA'; + assert.strictEqual(window.navigator.userAgent, 'mockUA'); + }); + + test('it can be resetted', function (assert) { + const ua = window.navigator.userAgent; + assert.notEqual(ua, 'mockUA'); + + // @ts-expect-error we can override that with the mocked window + window.navigator.userAgent = 'mockUA'; + reset(window); + assert.strictEqual(window.navigator.userAgent, ua); + }); + }); + }); + + module('window.screen', function () { + test('it allows adding and deleting properties', function (assert) { + // @ts-expect-error - ok for testing + window.screen.testKey = 'test value'; + assert.ok('testKey' in window.screen); + // @ts-expect-error - ok for testing + delete window.screen.testKey; + assert.notOk('testKey' in window.screen); + }); + + test('it allows adding and deleting functions', function (assert) { + // @ts-expect-error - ok for testing + window.screen.testFn = () => assert.ok(true); + assert.ok('testFn' in window.screen); + // @ts-expect-error - ok for testing + window.screen.testFn(); + // @ts-expect-error - ok for testing + delete window.screen.testFn; + assert.notOk('testFn' in window.screen); + }); + + module('width', function () { + test('it works', function (assert) { + const w = screen.width; // not using window-mock + assert.strictEqual(window.screen.width, w); + }); + + test('it can be overridden', function (assert) { + // @ts-expect-error we can override that with the mocked window + window.screen.width = 320; + assert.strictEqual(window.screen.width, 320); + }); + + test('it can be resetted', function (assert) { + const w = window.screen.width; + assert.notEqual(w, 320); + + // @ts-expect-error we can override that with the mocked window + window.screen.width = 320; + reset(window); + assert.strictEqual(window.screen.width, w); + }); + }); + }); + + module('window.onerror', function (hooks) { + let origOnerror: typeof window.onerror; + let origUnhandledRejection: typeof window.onunhandledrejection; + let origQunitUncaught: typeof QUnit.onUncaughtException; + + hooks.beforeEach(function () { + origOnerror = window.onerror; + origUnhandledRejection = window.onunhandledrejection; + origQunitUncaught = QUnit.onUncaughtException; + QUnit.onUncaughtException = () => {}; + }); + + hooks.afterEach(function () { + window.onerror = origOnerror; + window.onunhandledrejection = origUnhandledRejection; + QUnit.onUncaughtException = origQunitUncaught; + }); + + // eslint-disable-next-line qunit/require-expect + test('onerror works as expected', function (assert) { + const done = assert.async(); + assert.expect(1); + const spy = sinon.spy(); + window.onerror = spy; + + setTimeout(() => { + throw new Error('error'); + }, 0); + setTimeout(() => { + assert.true(spy.calledOnce, 'was called correctly'); + done(); + }, 10); + }); + + // TODO: flakey + skip('onunhandledrejection works as expected', function (assert) { + const done = assert.async(); + assert.expect(1); + + QUnit.onUncaughtException = () => {}; + const spy = sinon.spy(); + window.onunhandledrejection = spy; + + setTimeout(() => Promise.reject('rejected'), 0); + setTimeout(() => { + assert.true(spy.calledOnce, 'was called correctly'); + done(); + }, 10); + }); + }); + + module('nested proxies', function () { + test('it allows adding and deleting properties', function (assert) { + // @ts-expect-error - ok for testing + window.navigator.testKey = 'test value'; + assert.ok('testKey' in window.navigator); + // @ts-expect-error - ok for testing + delete window.navigator.testKey; + assert.notOk('testKey' in window.navigator); + }); + + test('it proxies functions', function (assert) { + // @ts-expect-error - ok for testing + window.navigator.connection.removeEventListener('foo', () => {}); + assert.ok(true); + }); + + test('it allows adding and deleting functions', function (assert) { + // @ts-expect-error - ok for testing + window.navigator.testFn = () => assert.ok(true); + assert.ok('testFn' in window.navigator); + // @ts-expect-error - ok for testing + window.navigator.testFn(); + // @ts-expect-error - ok for testing + delete window.navigator.testFn; + assert.notOk('testFn' in window.navigator); + }); + + test('method calls have the correct context', function (assert) { + // @ts-expect-error - ok for testing + window.navigator.testFn = function () { + assert.strictEqual(this, window.navigator); + }; + // @ts-expect-error - ok for testing + window.navigator.testFn(); + }); + + test('static methods work', function (assert) { + assert.strictEqual(typeof window.Notification, 'function'); + assert.strictEqual( + typeof window.Notification.requestPermission, + 'function', + ); + }); + + test('it works', function (assert) { + const t = screen.orientation.type; // not using window-mock + assert.strictEqual(window.screen.orientation.type, t); + }); + + test('it can be overridden', function (assert) { + // @ts-expect-error - ok for testing + window.screen.orientation.type = 'custom'; + assert.strictEqual(window.screen.orientation.type, 'custom'); + }); + + test('it can be resetted', function (assert) { + const t = window.screen.orientation.type; + assert.notEqual(t, 'custom'); + + // @ts-expect-error - ok for testing + window.screen.orientation.type = 'custom'; + reset(window); + assert.strictEqual(window.screen.orientation.type, t); + }); + + test('it proxies nested null fields', function (assert) { + assert.strictEqual(window.history.state, null); + }); + }); + + module('window.window', function () { + test('it works', function (assert) { + assert.true( + // eslint-disable-next-line qunit/no-assert-logical-expression + typeof window.window === 'object' && window.window != null, + 'it exists', + ); + assert.strictEqual(window, window.window, 'it is the same'); + }); + }); + + module('jQuery', function () { + test('it can listen to window events', function (assert) { + /* eslint-disable ember/no-jquery */ + const spy = sinon.spy(); + $(window).on('click', spy); + + $(window).trigger('click'); + assert.true(spy.calledOnce, 'event was triggered and listener called'); + }); + }); +} diff --git a/test-app/tests/unit/window-mock-test.ts b/test-app/tests/unit/window-mock-test.ts index f5817b2d..06030ff9 100644 --- a/test-app/tests/unit/window-mock-test.ts +++ b/test-app/tests/unit/window-mock-test.ts @@ -1,542 +1,10 @@ import _window from 'ember-window-mock'; -import { reset, setupWindowMock } from 'ember-window-mock/test-support'; -import QUnit, { module, skip, test } from 'qunit'; -import sinon from 'sinon'; -import $ from 'jquery'; - -interface TestGlobals { - window_mock_test_property?: string; - testFn?: () => void; - testKey?: string; -} -const window: typeof _window & TestGlobals = _window; +import { setupWindowMock } from 'ember-window-mock/test-support'; +import { module } from 'qunit'; +import { runWindowTests } from './run-window-tests'; module('window-mock', function (hooks) { setupWindowMock(hooks); - module('general properties', function () { - test('it proxies properties', function (assert) { - window.window_mock_test_property = 'foo'; - assert.strictEqual(window.window_mock_test_property, 'foo'); - delete window.window_mock_test_property; - }); - - test('it proxies functions', function (assert) { - window.focus(); - assert.ok(true); - }); - - test('it allows adding and deleting properties', function (assert) { - window.testKey = 'test value'; - assert.ok('testKey' in window); - delete window.testKey; - assert.notOk('testKey' in window); - }); - - test('it allows adding and deleting functions', function (assert) { - window.testFn = () => assert.ok(true); - assert.ok('testFn' in window); - window.testFn(); - delete window.testFn; - assert.notOk('testFn' in window); - }); - - // eslint-disable-next-line qunit/require-expect - test('method calls have the correct context', function (assert) { - assert.expect(1); - window.testFn = function () { - assert.strictEqual(this, window); - }; - window.testFn(); - }); - - test('it can call dispatchEvent', function (assert) { - const spy = sinon.spy(); - window.addEventListener('test-event', spy); - window.dispatchEvent(new Event('test-event')); - assert.ok(spy.calledOnce); - }); - - test('it proxies various null fields', function (assert) { - // NOTE: in some conditions these can be set by the navigator - assert.strictEqual(window.frameElement, null); - assert.strictEqual(window.opener, null); - assert.strictEqual(window.onbeforeunload, null); - }); - }); - - module('window.location', function () { - test('it defaults to window.location', function (assert) { - assert.strictEqual(window.location.href, window.location.href); - }); - - test('it mocks window.location.href', function (assert) { - window.location.href = 'http://www.example.com:8080/foo?q=bar#hash'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/foo?q=bar#hash', - ); - assert.strictEqual(window.location.host, 'www.example.com:8080'); - assert.strictEqual(window.location.hostname, 'www.example.com'); - assert.strictEqual(window.location.protocol, 'http:'); - assert.strictEqual(window.location.origin, 'http://www.example.com:8080'); - assert.strictEqual(window.location.port, '8080'); - assert.strictEqual(window.location.pathname, '/foo'); - assert.strictEqual(window.location.search, '?q=bar'); - assert.strictEqual(window.location.hash, '#hash'); - }); - - test('window.location.href supports relative URLs', function (assert) { - window.location.href = 'http://www.example.com:8080/foo?q=bar#hash'; - window.location.href = '/bar'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/bar', - ); - window.location.href = 'baz'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/baz', - ); - window.location.href = '/foo/bar'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/foo/bar', - ); - window.location.href = 'baz'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/foo/baz', - ); - window.location.href = '/foo/bar/'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/foo/bar/', - ); - window.location.href = 'baz'; - assert.strictEqual( - window.location.href, - 'http://www.example.com:8080/foo/bar/baz', - ); - window.location.href = '/'; - assert.strictEqual(window.location.href, 'http://www.example.com:8080/'); - }); - - test('it mocks window.location', function (assert) { - // @ts-expect-error - this actually works - window.location = 'http://www.example.com'; - assert.strictEqual(window.location.href, 'http://www.example.com/'); - }); - - test('it mocks window.location.reload', function (assert) { - window.location.href = 'http://www.example.com'; - window.location.reload(); - assert.strictEqual(window.location.href, 'http://www.example.com/'); - }); - - test('it mocks window.location.replace', function (assert) { - window.location.href = 'http://www.example.com'; - window.location.replace('http://www.emberjs.com'); - assert.strictEqual(window.location.href, 'http://www.emberjs.com/'); - }); - - test('it mocks window.location.assign', function (assert) { - window.location.href = 'http://www.example.com'; - window.location.assign('http://www.emberjs.com'); - assert.strictEqual(window.location.href, 'http://www.emberjs.com/'); - }); - - test('it mocks window.location.toString()', function (assert) { - window.location.href = 'http://www.example.com'; - assert.strictEqual(window.location.toString(), 'http://www.example.com/'); - }); - - test('it mocks pathname', function (assert) { - window.location.href = 'http://www.example.com'; - window.location.pathname = '/foo/'; - assert.strictEqual(window.location.href, 'http://www.example.com/foo/'); - }); - }); - - module('blocking dialogs', function () { - test('it replaces alert with noop', function (assert) { - assert.strictEqual(window.alert('foo'), undefined); - }); - - test('it replaces confirm with noop', function (assert) { - assert.strictEqual(window.confirm('foo'), undefined); - }); - - test('it replaces prompt with prompt', function (assert) { - assert.strictEqual(window.prompt('foo'), undefined); - }); - - test('it can stub alert', function (assert) { - const spy = sinon.spy(); - window.alert = spy; - window.alert('foo'); - assert.ok(spy.calledOnce); - assert.ok(spy.calledWith('foo')); - }); - - test('it can stub confirm', function (assert) { - const stub = sinon.stub().returns(true); - window.confirm = stub; - const result = window.confirm('foo'); - assert.ok(stub.calledOnce); - assert.ok(stub.calledWith('foo')); - assert.true(result); - }); - - test('it can stub prompt', function (assert) { - const stub = sinon.stub().returns('bar'); - window.prompt = stub; - const result = window.prompt('foo'); - assert.ok(stub.calledOnce); - assert.ok(stub.calledWith('foo')); - assert.strictEqual(result, 'bar'); - }); - }); - - module('localStorage', function () { - test('it mocks window.localStorage.length', function (assert) { - assert.strictEqual(window.localStorage.length, 0); - - window.localStorage.setItem('a', 'x'); - assert.strictEqual(window.localStorage.length, 1); - - window.localStorage.setItem('b', 'y'); - assert.strictEqual(window.localStorage.length, 2); - - window.localStorage.clear(); - assert.strictEqual(window.localStorage.length, 0); - }); - - test('it mocks window.localStorage.getItem', function (assert) { - assert.strictEqual(window.localStorage.getItem('a'), null); - - window.localStorage.setItem('a', 'x'); - assert.strictEqual(window.localStorage.getItem('a'), 'x'); - - window.localStorage.clear(); - assert.strictEqual(window.localStorage.getItem('a'), null); - }); - - test('it mocks window.localStorage.key', function (assert) { - assert.strictEqual(window.localStorage.key(0), null); - - window.localStorage.setItem('a', 'x'); - assert.strictEqual(window.localStorage.key(0), 'a'); - - window.localStorage.setItem('b', 'y'); - assert.strictEqual(window.localStorage.key(0), 'a'); - assert.strictEqual(window.localStorage.key(1), 'b'); - - window.localStorage.clear(); - assert.strictEqual(window.localStorage.key(0), null); - }); - - test('it mocks window.localStorage.removeItem', function (assert) { - window.localStorage.setItem('a', 'x'); - window.localStorage.setItem('b', 'y'); - assert.strictEqual(window.localStorage.getItem('a'), 'x'); - assert.strictEqual(window.localStorage.getItem('b'), 'y'); - - window.localStorage.removeItem('a'); - assert.strictEqual(window.localStorage.getItem('a'), null); - assert.strictEqual(window.localStorage.getItem('b'), 'y'); - - window.localStorage.removeItem('y'); - assert.strictEqual(window.localStorage.getItem('b'), 'y'); - }); - - test('it mocks window.localStorage.clear', function (assert) { - window.localStorage.setItem('a', 'x'); - window.localStorage.setItem('b', 'y'); - - assert.strictEqual(window.localStorage.length, 2); - - window.localStorage.clear(); - - assert.strictEqual(window.localStorage.length, 0); - assert.strictEqual(window.localStorage.getItem('a'), null); - assert.strictEqual(window.localStorage.getItem('b'), null); - }); - - test('it clears localStorage on reset', function (assert) { - window.localStorage.setItem('c', 'z'); - assert.strictEqual(window.localStorage.getItem('c'), 'z'); - assert.strictEqual(window.localStorage.key(0), 'c'); - assert.strictEqual(window.localStorage.length, 1); - - reset(); - - assert.strictEqual(window.localStorage.getItem('c'), null); - assert.strictEqual(window.localStorage.key(0), null); - assert.strictEqual(window.localStorage.length, 0); - }); - }); - - module('sessionStorage', function () { - test('it mocks window.sessionStorage.length', function (assert) { - assert.strictEqual(window.sessionStorage.length, 0); - - window.sessionStorage.setItem('a', 'x'); - assert.strictEqual(window.sessionStorage.length, 1); - }); - - test('it mocks window.sessionStorage.getItem', function (assert) { - assert.strictEqual(window.sessionStorage.getItem('a'), null); - - window.sessionStorage.setItem('a', 'x'); - assert.strictEqual(window.sessionStorage.getItem('a'), 'x'); - }); - - test('it mocks window.sessionStorage.key', function (assert) { - assert.strictEqual(window.sessionStorage.key(0), null); - - window.sessionStorage.setItem('a', 'x'); - assert.strictEqual(window.sessionStorage.key(0), 'a'); - }); - - test('it mocks window.sessionStorage.removeItem', function (assert) { - window.sessionStorage.setItem('a', 'x'); - - window.sessionStorage.removeItem('a'); - assert.strictEqual(window.sessionStorage.getItem('a'), null); - }); - - test('it mocks window.sessionStorage.clear', function (assert) { - window.sessionStorage.setItem('a', 'x'); - - window.sessionStorage.clear(); - assert.strictEqual(window.sessionStorage.getItem('a'), null); - }); - - test('it clears sessionStorage on reset', function (assert) { - window.sessionStorage.setItem('c', 'z'); - assert.strictEqual(window.sessionStorage.getItem('c'), 'z'); - assert.strictEqual(window.sessionStorage.key(0), 'c'); - assert.strictEqual(window.sessionStorage.length, 1); - - reset(); - - assert.strictEqual(window.sessionStorage.getItem('c'), null); - assert.strictEqual(window.sessionStorage.key(0), null); - assert.strictEqual(window.sessionStorage.length, 0); - }); - }); - - module('window.navigator', function () { - module('userAgent', function () { - test('it works', function (assert) { - const ua = navigator.userAgent; // not using window-mock - assert.strictEqual(window.navigator.userAgent, ua); - }); - - test('it can be overridden', function (assert) { - // @ts-expect-error we can override that with the mocked window - window.navigator.userAgent = 'mockUA'; - assert.strictEqual(window.navigator.userAgent, 'mockUA'); - }); - - test('it can be resetted', function (assert) { - const ua = window.navigator.userAgent; - assert.notEqual(ua, 'mockUA'); - - // @ts-expect-error we can override that with the mocked window - window.navigator.userAgent = 'mockUA'; - reset(); - assert.strictEqual(window.navigator.userAgent, ua); - }); - }); - }); - - module('window.screen', function () { - test('it allows adding and deleting properties', function (assert) { - // @ts-expect-error - ok for testing - window.screen.testKey = 'test value'; - assert.ok('testKey' in window.screen); - // @ts-expect-error - ok for testing - delete window.screen.testKey; - assert.notOk('testKey' in window.screen); - }); - - test('it allows adding and deleting functions', function (assert) { - // @ts-expect-error - ok for testing - window.screen.testFn = () => assert.ok(true); - assert.ok('testFn' in window.screen); - // @ts-expect-error - ok for testing - window.screen.testFn(); - // @ts-expect-error - ok for testing - delete window.screen.testFn; - assert.notOk('testFn' in window.screen); - }); - - module('width', function () { - test('it works', function (assert) { - const w = screen.width; // not using window-mock - assert.strictEqual(window.screen.width, w); - }); - - test('it can be overridden', function (assert) { - // @ts-expect-error we can override that with the mocked window - window.screen.width = 320; - assert.strictEqual(window.screen.width, 320); - }); - - test('it can be resetted', function (assert) { - const w = window.screen.width; - assert.notEqual(w, 320); - - // @ts-expect-error we can override that with the mocked window - window.screen.width = 320; - reset(); - assert.strictEqual(window.screen.width, w); - }); - }); - }); - - module('window.onerror', function (hooks) { - let origOnerror: typeof window.onerror; - let origUnhandledRejection: typeof window.onunhandledrejection; - let origQunitUncaught: typeof QUnit.onUncaughtException; - - hooks.beforeEach(function () { - origOnerror = window.onerror; - origUnhandledRejection = window.onunhandledrejection; - origQunitUncaught = QUnit.onUncaughtException; - QUnit.onUncaughtException = () => {}; - }); - - hooks.afterEach(function () { - window.onerror = origOnerror; - window.onunhandledrejection = origUnhandledRejection; - QUnit.onUncaughtException = origQunitUncaught; - }); - - // eslint-disable-next-line qunit/require-expect - test('onerror works as expected', function (assert) { - const done = assert.async(); - assert.expect(1); - const spy = sinon.spy(); - window.onerror = spy; - - setTimeout(() => { - throw new Error('error'); - }, 0); - setTimeout(() => { - assert.true(spy.calledOnce, 'was called correctly'); - done(); - }, 10); - }); - - // TODO: flakey - skip('onunhandledrejection works as expected', function (assert) { - const done = assert.async(); - assert.expect(1); - - QUnit.onUncaughtException = () => {}; - const spy = sinon.spy(); - window.onunhandledrejection = spy; - - setTimeout(() => Promise.reject('rejected'), 0); - setTimeout(() => { - assert.true(spy.calledOnce, 'was called correctly'); - done(); - }, 10); - }); - }); - - module('nested proxies', function () { - test('it allows adding and deleting properties', function (assert) { - // @ts-expect-error - ok for testing - window.navigator.testKey = 'test value'; - assert.ok('testKey' in window.navigator); - // @ts-expect-error - ok for testing - delete window.navigator.testKey; - assert.notOk('testKey' in window.navigator); - }); - - test('it proxies functions', function (assert) { - // @ts-expect-error - ok for testing - window.navigator.connection.removeEventListener('foo', () => {}); - assert.ok(true); - }); - - test('it allows adding and deleting functions', function (assert) { - // @ts-expect-error - ok for testing - window.navigator.testFn = () => assert.ok(true); - assert.ok('testFn' in window.navigator); - // @ts-expect-error - ok for testing - window.navigator.testFn(); - // @ts-expect-error - ok for testing - delete window.navigator.testFn; - assert.notOk('testFn' in window.navigator); - }); - - test('method calls have the correct context', function (assert) { - // @ts-expect-error - ok for testing - window.navigator.testFn = function () { - assert.strictEqual(this, window.navigator); - }; - // @ts-expect-error - ok for testing - window.navigator.testFn(); - }); - - test('static methods work', function (assert) { - assert.strictEqual(typeof window.Notification, 'function'); - assert.strictEqual( - typeof window.Notification.requestPermission, - 'function', - ); - }); - - test('it works', function (assert) { - const t = screen.orientation.type; // not using window-mock - assert.strictEqual(window.screen.orientation.type, t); - }); - - test('it can be overridden', function (assert) { - // @ts-expect-error - ok for testing - window.screen.orientation.type = 'custom'; - assert.strictEqual(window.screen.orientation.type, 'custom'); - }); - - test('it can be resetted', function (assert) { - const t = window.screen.orientation.type; - assert.notEqual(t, 'custom'); - - // @ts-expect-error - ok for testing - window.screen.orientation.type = 'custom'; - reset(); - assert.strictEqual(window.screen.orientation.type, t); - }); - - test('it proxies nested null fields', function (assert) { - assert.strictEqual(window.history.state, null); - }); - }); - - module('window.window', function () { - test('it works', function (assert) { - assert.true( - // eslint-disable-next-line qunit/no-assert-logical-expression - typeof window.window === 'object' && window.window != null, - 'it exists', - ); - assert.strictEqual(window, window.window, 'it is the same'); - }); - }); - - module('jQuery', function () { - test('it can listen to window events', function (assert) { - /* eslint-disable ember/no-jquery */ - const spy = sinon.spy(); - $(window).on('click', spy); - - $(window).trigger('click'); - assert.true(spy.calledOnce, 'event was triggered and listener called'); - }); - }); + runWindowTests(_window); });