Skip to content

Commit

Permalink
Merge pull request #70 from CrowdStrike/simplify-implementation
Browse files Browse the repository at this point in the history
Replace underlying mocking with ember-window-mock
  • Loading branch information
NullVoxPopuli authored Sep 9, 2021
2 parents 6019aaf + 659f20f commit 66fe857
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 180 deletions.
85 changes: 60 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand Down
46 changes: 0 additions & 46 deletions addon-test-support/-private/-helpers.ts

This file was deleted.

11 changes: 0 additions & 11 deletions addon-test-support/-private/document.ts

This file was deleted.

22 changes: 0 additions & 22 deletions addon-test-support/-private/navigator.ts

This file was deleted.

27 changes: 0 additions & 27 deletions addon-test-support/-private/window.ts

This file was deleted.

89 changes: 72 additions & 17 deletions addon-test-support/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,96 @@
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<Window>;
localStorage?: boolean;
document?: boolean | TestDocument | typeof Service;
navigator?: boolean | TestNavigator;
document?: boolean | typeof Service | RecursivePartial<Document>;
navigator?: boolean | RecursivePartial<Navigator>;
};

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) {
this.owner.register('service:browser/local-storage', FakeLocalStorageService);
}

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<string, any>;

export function maybeMake<DefaultType extends UnknownObject, TestClass extends UnknownObject>(
maybeImplementation: true | typeof Service | TestClass | RecursivePartial<DefaultType>,
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;
}
}
}
55 changes: 55 additions & 0 deletions addon-test-support/window-mock-augments.ts
Original file line number Diff line number Diff line change
@@ -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<string | symbol> = ['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;
}
Loading

0 comments on commit 66fe857

Please sign in to comment.