diff --git a/.flowconfig b/.flowconfig index 9b82a174..ee14ab9e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,14 +1,15 @@ [ignore] .*/node_modules/.*[^(package)]\.json$ +/dist/.* [include] ./src/ [libs] +./node_modules/fusion-core/flow-typed [lints] [options] -suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore [strict] diff --git a/README.md b/README.md index 4c3870de..ea635646 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,14 @@ 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', } @@ -26,8 +29,7 @@ const ctx = await render(app, '/test-url', { // 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', } @@ -39,17 +41,22 @@ const ctx = await request(app, '/test-url', { ### API -#### `request(app: FusionApp, url: String, options: ?Object)` => Promise +#### `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 + +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 +#### `getSimulator(...).render(url: String, options: ?Object)` => Promise This is the same as `request`, but defaults the `accept` header to `text/html` which will trigger a render of your application. diff --git a/package.json b/package.json index 42e6781d..01209d8c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 652b0901..e404be41 100644 --- a/src/__tests__/index.js +++ b/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}; @@ -10,12 +11,29 @@ 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'; @@ -23,9 +41,10 @@ test('simulate non-render request', async t => { 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'); @@ -33,13 +52,46 @@ test('simulate non-render request', async t => { 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(); diff --git a/src/index.js b/src/index.js index c4b91637..23a66aff 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,19 @@ +//@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' @@ -10,23 +21,42 @@ export function request(app, url, options = {}) { } 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 = () => { diff --git a/src/mock-context.js b/src/mock-context.js index e3d2051b..67c73499 100644 --- a/src/mock-context.js +++ b/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; @@ -23,7 +27,9 @@ 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()]; @@ -31,11 +37,12 @@ export function mockContext(url, options) { 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); } diff --git a/src/simulate.js b/src/simulate.js index 97a15d86..45857be4 100644 --- a/src/simulate.js +++ b/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); } diff --git a/yarn.lock b/yarn.lock index 4ba557e6..874c4140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"