Skip to content
This repository has been archived by the owner on May 17, 2019. It is now read-only.

Commit

Permalink
Implement wrapper in fusion-test-utils to encapsulate plugin resolu…
Browse files Browse the repository at this point in the history
…tion (#38)

* Added Flow types

* Implement 'registerAsTest' which returns request and render

* Update unit tests to leverage 'registerAsTest'

* Test plugin resolution with dependency injection

* Use registry.yarnpkg.com instead of internal registry

* Added a test fixture example for registerAsTest

* Rename 'registerAsTest' to 'getSimulator' for clarity-sake

* Update documentation
  • Loading branch information
AlexMSmithCA committed Jan 16, 2018
1 parent 21a899f commit 395defd
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .flowconfig
@@ -1,14 +1,15 @@
[ignore]
.*/node_modules/.*[^(package)]\.json$
<PROJECT_ROOT>/dist/.*

[include]
./src/

[libs]
./node_modules/fusion-core/flow-typed

[lints]

[options]
suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore

[strict]
23 changes: 15 additions & 8 deletions README.md
Expand Up @@ -14,20 +14,22 @@ yarn add fusion-test-utils

```js
import App from 'fusion-core';
import {render, request} from 'fusion-test-utils';
import {getSimulator} from 'fusion-test-utils';

// test renders of your application
// create simulator
const app = new App();
const ctx = await render(app, '/test-url', {
const simulator = getSimulator(app /*, (optional) test plugin with assertions on dependencies */);

// test renders of your application
const ctx = await simulator.render(app, '/test-url', {
headers: {
'x-header': 'value',
}
});
// do assertions on ctx

// test requests to your application
const app = new App();
const ctx = await request(app, '/test-url', {
const ctx = await simulator.request(app, '/test-url', {
headers: {
'x-header': 'value',
}
Expand All @@ -39,17 +41,22 @@ const ctx = await request(app, '/test-url', {

### API

#### `request(app: FusionApp, url: String, options: ?Object)` => Promise<ctx>
#### `getSimulator(app: FusionApp, testPlugin?: FusionPlugin) => { request, render }`

Simulates a request through your application.
Creates a simulator which exposes functionality to simulate requests and renders through your application.
`app` - instance of a FusionApp
`testPlugin` - optional plugin to make assertions on dependencies

#### `getSimulator(...).request(url: String, options: ?Object)` => Promise<ctx>

Simulates a request through your application.
`url` - path for request
`options` - optional object containing custom settings for the request
`options.method` - the request method, e.g., GET, POST,
`options.headers` - headers to be added to the request
`options.body` - body for the request

#### `render(app: FusionApp, url: String, options: ?Object)` => Promise<ctx>
#### `getSimulator(...).render(url: String, options: ?Object)` => Promise<ctx>

This is the same as `request`, but defaults the `accept` header to `text/html` which will trigger a render of your application.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -38,6 +38,7 @@
"eslint-plugin-react": "7.5.1",
"flow-bin": "0.63.1",
"fusion-core": "0.3.0-2",
"fusion-tokens": "^0.0.4",
"jest": "22.0.6",
"jest-cli": "22.0.6",
"nyc": "11.4.1",
Expand All @@ -61,7 +62,8 @@
"node-mocks-http": "^1.6.6"
},
"peerDependencies": {
"fusion-core": ">=0.3.0-2"
"fusion-core": ">=0.3.0-2",
"fusion-tokens": "^0.0.4"
},
"engines": {
"node": ">= 8.9.0"
Expand Down
62 changes: 57 additions & 5 deletions src/__tests__/index.js
@@ -1,7 +1,8 @@
import test from 'tape-cup';
import App from 'fusion-core';
import App, {withDependencies} from 'fusion-core';
import {createToken} from 'fusion-tokens';

import {render, request, test as exportedTest} from '../index.js';
import {getSimulator, test as exportedTest} from '../index.js';

test('simulate render request', async t => {
const flags = {render: false};
Expand All @@ -10,36 +11,87 @@ test('simulate render request', async t => {
flags.render = true;
};
const app = new App(element, renderFn);
const ctx = await render(app, '/');
var testApp = getSimulator(app);
const ctx = await testApp.render('/');
t.ok(flags.render, 'triggered ssr');
t.ok(ctx.element, 'sets ctx.element');
t.end();
});

test('simulate multi-render requests', async t => {
const counter = {renderCount: 0};
const renderFn = () => {
counter.renderCount++;
};
const app = new App('hello', renderFn);
var testApp = getSimulator(app);

for (var i = 1; i <= 5; i++) {
await testApp.render('/');
t.equal(counter.renderCount, i, `#${i} ssr render successful`);
}

t.end();
});

test('simulate non-render request', async t => {
const flags = {render: false};
const element = 'hi';
const renderFn = () => {
flags.render = true;
};
const app = new App(element, renderFn);
const testApp = getSimulator(app);
if (__BROWSER__) {
try {
await request(app, '/');
testApp.request('/');
t.fail('should have thrown');
} catch (e) {
t.ok(e, 'throws an error');
} finally {
t.end();
}
} else {
const ctx = await request(app, '/');
const ctx = testApp.request('/');
t.notok(ctx.element, 'does not set ctx.element');
t.ok(!flags.render, 'did not trigger ssr');
t.end();
}
});

test('use simulator with fixture and plugin dependencies', async t => {
// Dependency-less plugin
const msgProviderPluginToken = createToken('MessageProviderPluginToken');
const msgProviderPlugin = {msg: 'it works!'};
function getTestFixture() {
// Register plugins
const app = new App('hi', el => el);
app.register(msgProviderPluginToken, () => msgProviderPlugin);
return app;
}
const app = getTestFixture();

t.plan(3);
getSimulator(
app,
withDependencies({
msgProvider: msgProviderPluginToken,
})(deps => {
t.ok(deps, 'some dependencies successfully resolved');
t.ok(deps.msgProvider, 'requested dependency successfully resolved');
const {msgProvider} = deps;
t.equal(
msgProvider.msg,
msgProviderPlugin.msg,
'dependency payload is correct'
);
return 'yay!';
})
);

t.end();
});

test('test throws when not using test-app', async t => {
try {
exportedTest();
Expand Down
36 changes: 33 additions & 3 deletions src/index.js
@@ -1,32 +1,62 @@
//@flow

import assert from 'assert';

import FusionApp from 'fusion-core';
import type {FusionPlugin} from 'fusion-core';

import {mockContext, renderContext} from './mock-context.js';
import simulate from './simulate';

export function request(app, url, options = {}) {
declare var __BROWSER__: boolean;

const request = (app: FusionApp) => (
url: string,
options: * = {}
): Promise<*> => {
if (__BROWSER__) {
throw new Error(
'[fusion-test-utils] Request api not support from the browser. Please use `render` instead'
);
}
const ctx = mockContext(url, options);
return simulate(app, ctx);
}
};

export function render(app, url, options = {}) {
const render = (app: FusionApp) => (
url: string,
options: * = {}
): Promise<*> => {
const ctx = renderContext(url, options);
return simulate(app, ctx);
};

export function getSimulator(app: FusionApp, testPlugin?: FusionPlugin<*, *>) {
if (testPlugin) {
app.register(testPlugin);
}
app.resolve();

return {
request: request(app),
render: render(app),
};
}

// Export test runner functions from jest
// eslint-disable-next-line import/no-mutable-exports
let mockFunction, test;
// $FlowFixMe
if (typeof it !== 'undefined') {
// Surface snapshot testing
// $FlowFixMe
assert.matchSnapshot = tree => expect(tree).toMatchSnapshot();

/* eslint-env node, jest */
// $FlowFixMe
test = (description, callback, ...rest) =>
it(description, () => callback(assert), ...rest);
// $FlowFixMe
mockFunction = (...args) => jest.fn(...args);
} else {
const notSupported = () => {
Expand Down
13 changes: 10 additions & 3 deletions src/mock-context.js
@@ -1,10 +1,14 @@
/* eslint-env node */
// @flow

import {parse} from 'url';
import type {Context} from 'fusion-core';

export function mockContext(url, options) {
declare var __BROWSER__: boolean;

export function mockContext(url: string, options: *): Context {
if (__BROWSER__) {
const parsedUrl = parse(url);
const parsedUrl = {...parse(url)};
const {path} = parsedUrl;
parsedUrl.path = parsedUrl.pathname;
parsedUrl.url = path;
Expand All @@ -23,19 +27,22 @@ export function mockContext(url, options) {
* https://github.com/koajs/koa/blob/master/LICENSE
*/
const socket = new Stream.Duplex();
//$FlowFixMe
req = Object.assign({headers: {}, socket}, Stream.Readable.prototype, req);
//$FlowFixMe
res = Object.assign({_headers: {}, socket}, Stream.Writable.prototype, res);
req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1';
res.getHeader = k => res._headers[k.toLowerCase()];
res.setHeader = (k, v) => (res._headers[k.toLowerCase()] = v);
res.removeHeader = k => delete res._headers[k.toLowerCase()];

const app = new Koa();
//$FlowFixMe
const ctx = app.createContext(req, res);
return ctx;
}

export function renderContext(url, options) {
export function renderContext(url: string, options: any): Context {
options = Object.assign(options, {headers: {accept: 'text/html'}});
return mockContext(url, options);
}
9 changes: 6 additions & 3 deletions src/simulate.js
@@ -1,6 +1,9 @@
import {compose} from 'fusion-core';
// @flow

export default function simulate(app, ctx) {
app.resolve();
// $FlowFixMe
import FusionApp, {compose} from 'fusion-core';
import type {Context} from 'fusion-core';

export default function simulate(app: FusionApp, ctx: Context): Promise<*> {
return compose(app.plugins)(ctx, () => Promise.resolve()).then(() => ctx);
}
4 changes: 4 additions & 0 deletions yarn.lock
Expand Up @@ -2824,6 +2824,10 @@ fusion-core@0.3.0-2:
node-mocks-http "^1.6.6"
toposort "^1.0.6"

fusion-tokens@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/fusion-tokens/-/fusion-tokens-0.0.4.tgz#b84c58e2de8e06d3e63c2c182da7e023ccfb50ec"

gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
Expand Down

0 comments on commit 395defd

Please sign in to comment.