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

Implement wrapper in fusion-test-utils to encapsulate plugin resolution #38

Merged
merged 9 commits into from
Jan 16, 2018
3 changes: 2 additions & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[ignore]
.*/node_modules/.*[^(package)]\.json$
<PROJECT_ROOT>/dist/.*

[include]
./src/

[libs]
./node_modules/fusion-core/flow-typed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using this for anything yet? Or just getting our configs to match?

Copy link
Member Author

@AlexMSmithCA AlexMSmithCA Jan 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question- it's used here for Context and FusionApp. At least internally, it looks like our plan is to ship libdef files like we are will soon be doing in fusion-core.


[lints]

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

[strict]
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow for an array of test plugins?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just the public API from the DI system. Whether multiple registration makes sense is something that we can discuss on Giancarlo's new proposal.

}
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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