Skip to content

Commit

Permalink
Merge pull request #575 from simonihmig/mock-other-window-refs
Browse files Browse the repository at this point in the history
Add createMockedWindow helper for mocking window.parent or other explicit window instances
  • Loading branch information
simonihmig authored Jul 23, 2024
2 parents fe41797 + b47e5d1 commit 43c8c43
Show file tree
Hide file tree
Showing 8 changed files with 759 additions and 619 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
7 changes: 5 additions & 2 deletions ember-window-mock/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reset, mockProxyHandler } from './window.js';
import { reset, createWindowProxyHandler } from './window.js';
import { _setCurrentHandler } from '../../index.js';

//
Expand All @@ -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();
Expand Down
173 changes: 95 additions & 78 deletions ember-window-mock/src/test-support/-private/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]?.();
}
2 changes: 1 addition & 1 deletion ember-window-mock/src/test-support/index.js
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 18 additions & 0 deletions test-app/tests/unit/create-mocked-window-test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading

0 comments on commit 43c8c43

Please sign in to comment.