From 8ba7a6b51553d4e6b0eb380479e853d063339882 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Fri, 12 Jan 2018 13:36:37 -0800 Subject: [PATCH 1/8] Added Flow types --- .flowconfig | 3 ++- src/index.js | 22 ++++++++++++++++++++-- src/mock-context.js | 13 ++++++++++--- src/simulate.js | 8 ++++++-- 4 files changed, 38 insertions(+), 8 deletions(-) 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/src/index.js b/src/index.js index c4b91637..7a7cbe96 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,18 @@ +//@flow + +import FusionApp from 'fusion-core'; + import assert from 'assert'; import {mockContext, renderContext} from './mock-context.js'; import simulate from './simulate'; -export function request(app, url, options = {}) { +declare var __BROWSER__: boolean; + +export function 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' @@ -12,7 +22,11 @@ export function request(app, url, options = {}) { return simulate(app, ctx); } -export function render(app, url, options = {}) { +export function render( + app: FusionApp, + url: string, + options: * = {} +): Promise<*> { const ctx = renderContext(url, options); return simulate(app, ctx); } @@ -20,13 +34,17 @@ export function render(app, url, options = {}) { // 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..99fc15fc 100644 --- a/src/simulate.js +++ b/src/simulate.js @@ -1,6 +1,10 @@ -import {compose} from 'fusion-core'; +// @flow -export default function simulate(app, ctx) { +// $FlowFixMe +import FusionApp, {compose} from 'fusion-core'; +import type {Context} from 'fusion-core'; + +export default function simulate(app: FusionApp, ctx: Context): Promise<*> { app.resolve(); return compose(app.plugins)(ctx, () => Promise.resolve()).then(() => ctx); } From 49a881d78119e04e0a7e8cf695804468b46f993d Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Fri, 12 Jan 2018 14:16:45 -0800 Subject: [PATCH 2/8] Implement 'registerAsTest' which returns request and render --- src/index.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 7a7cbe96..ff22ea25 100644 --- a/src/index.js +++ b/src/index.js @@ -1,18 +1,19 @@ //@flow +import assert from 'assert'; + import FusionApp from 'fusion-core'; +import type {FusionPlugin} from 'fusion-core'; -import assert from 'assert'; import {mockContext, renderContext} from './mock-context.js'; import simulate from './simulate'; declare var __BROWSER__: boolean; -export function request( - app: FusionApp, +const request = (app: FusionApp) => ( url: string, options: * = {} -): Promise<*> { +): Promise<*> => { if (__BROWSER__) { throw new Error( '[fusion-test-utils] Request api not support from the browser. Please use `render` instead' @@ -20,15 +21,24 @@ export function request( } const ctx = mockContext(url, options); return simulate(app, ctx); -} +}; -export function render( - app: FusionApp, +const render = (app: FusionApp) => ( url: string, options: * = {} -): Promise<*> { +): Promise<*> => { const ctx = renderContext(url, options); return simulate(app, ctx); +}; + +export function registerAsTest(app: FusionApp, testPlugin: FusionPlugin<*, *>) { + app.register(testPlugin); + app.resolve(); + + return { + request: request(app), + render: render(app), + }; } // Export test runner functions from jest From ce5cda583b93ae58603000a6d8d5449a51b19f12 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Fri, 12 Jan 2018 14:55:09 -0800 Subject: [PATCH 3/8] Update unit tests to leverage 'registerAsTest' --- src/__tests__/index.js | 26 ++++++++++++++++++++++---- src/index.js | 9 +++++++-- src/simulate.js | 1 - 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 652b0901..688807a4 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -1,7 +1,7 @@ import test from 'tape-cup'; import App from 'fusion-core'; -import {render, request, test as exportedTest} from '../index.js'; +import {registerAsTest, test as exportedTest} from '../index.js'; test('simulate render request', async t => { const flags = {render: false}; @@ -10,12 +10,29 @@ test('simulate render request', async t => { flags.render = true; }; const app = new App(element, renderFn); - const ctx = await render(app, '/'); + var testApp = registerAsTest(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 = registerAsTest(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 +40,10 @@ test('simulate non-render request', async t => { flags.render = true; }; const app = new App(element, renderFn); + const testApp = registerAsTest(app); if (__BROWSER__) { try { - await request(app, '/'); + testApp.request('/'); t.fail('should have thrown'); } catch (e) { t.ok(e, 'throws an error'); @@ -33,7 +51,7 @@ 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(); diff --git a/src/index.js b/src/index.js index ff22ea25..d2be9044 100644 --- a/src/index.js +++ b/src/index.js @@ -31,8 +31,13 @@ const render = (app: FusionApp) => ( return simulate(app, ctx); }; -export function registerAsTest(app: FusionApp, testPlugin: FusionPlugin<*, *>) { - app.register(testPlugin); +export function registerAsTest( + app: FusionApp, + testPlugin?: FusionPlugin<*, *> +) { + if (testPlugin) { + app.register(testPlugin); + } app.resolve(); return { diff --git a/src/simulate.js b/src/simulate.js index 99fc15fc..45857be4 100644 --- a/src/simulate.js +++ b/src/simulate.js @@ -5,6 +5,5 @@ import FusionApp, {compose} from 'fusion-core'; import type {Context} from 'fusion-core'; export default function simulate(app: FusionApp, ctx: Context): Promise<*> { - app.resolve(); return compose(app.plugins)(ctx, () => Promise.resolve()).then(() => ctx); } From 791a2235cd3f9326b9640b3195b0ab29a25f5969 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Fri, 12 Jan 2018 16:01:59 -0800 Subject: [PATCH 4/8] Test plugin resolution with dependency injection --- package.json | 4 +++- src/__tests__/index.js | 33 ++++++++++++++++++++++++++++++++- yarn.lock | 4 ++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 02ba00d1..c8c04ff4 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 688807a4..5a13a63c 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -1,5 +1,6 @@ import test from 'tape-cup'; -import App from 'fusion-core'; +import App, {withDependencies} from 'fusion-core'; +import {createToken} from 'fusion-tokens'; import {registerAsTest, test as exportedTest} from '../index.js'; @@ -58,6 +59,36 @@ test('simulate non-render request', async t => { } }); +test('simulate with plugin dependencies', async t => { + // Dependency-less plugin + const msgProviderPluginToken = createToken('MessageProviderPluginToken'); + const msgProviderPlugin = {msg: 'it works!'}; + + // Register plugins + const app = new App('hi', el => el); + app.register(msgProviderPluginToken, () => msgProviderPlugin); + + t.plan(3); + registerAsTest( + 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/yarn.lock b/yarn.lock index 8373c5e0..9afbe027 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2802,6 +2802,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://unpm.uberinternal.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" From 0ed13c8a1e2b81392bd515d8c0a3bf2b1b7555e4 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Fri, 12 Jan 2018 16:38:29 -0800 Subject: [PATCH 5/8] Use registry.yarnpkg.com instead of internal registry --- yarn.lock | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9afbe027..621c33e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1665,8 +1665,8 @@ camelcase@^1.0.2: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" caniuse-lite@^1.0.30000789: - version "1.0.30000790" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000790.tgz#c954cca780046f34c4b433d324ef419e1db51a53" + version "1.0.30000791" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000791.tgz#8e35745efd483a3e23bb7d350990326d2319fc16" caseless@~0.12.0: version "0.12.0" @@ -2080,10 +2080,14 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" -depd@1.1.1, depd@^1.1.0, depd@~1.1.1: +depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" +depd@^1.1.0, depd@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -2804,7 +2808,7 @@ fusion-core@0.3.0-2: fusion-tokens@^0.0.4: version "0.0.4" - resolved "https://unpm.uberinternal.com/fusion-tokens/-/fusion-tokens-0.0.4.tgz#b84c58e2de8e06d3e63c2c182da7e023ccfb50ec" + resolved "https://registry.yarnpkg.com/fusion-tokens/-/fusion-tokens-0.0.4.tgz#b84c58e2de8e06d3e63c2c182da7e023ccfb50ec" gauge@~2.7.3: version "2.7.4" From 517fd341e48e57f7da405605d72268de559b11b2 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Tue, 16 Jan 2018 14:26:06 -0800 Subject: [PATCH 6/8] Added a test fixture example for registerAsTest --- src/__tests__/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 5a13a63c..6dbe04cc 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -59,14 +59,17 @@ test('simulate non-render request', async t => { } }); -test('simulate with plugin dependencies', async t => { +test('use simulator with fixture and plugin dependencies', async t => { // Dependency-less plugin const msgProviderPluginToken = createToken('MessageProviderPluginToken'); const msgProviderPlugin = {msg: 'it works!'}; - - // Register plugins - const app = new App('hi', el => el); - app.register(msgProviderPluginToken, () => msgProviderPlugin); + function getTestFixture() { + // Register plugins + const app = new App('hi', el => el); + app.register(msgProviderPluginToken, () => msgProviderPlugin); + return app; + } + const app = getTestFixture(); t.plan(3); registerAsTest( From aca89cd25264330a44987986fdd263cb59f7321b Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Tue, 16 Jan 2018 14:26:48 -0800 Subject: [PATCH 7/8] Rename 'registerAsTest' to 'getSimulator' for clarity-sake --- src/__tests__/index.js | 10 +++++----- src/index.js | 5 +---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 6dbe04cc..e404be41 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -2,7 +2,7 @@ import test from 'tape-cup'; import App, {withDependencies} from 'fusion-core'; import {createToken} from 'fusion-tokens'; -import {registerAsTest, test as exportedTest} from '../index.js'; +import {getSimulator, test as exportedTest} from '../index.js'; test('simulate render request', async t => { const flags = {render: false}; @@ -11,7 +11,7 @@ test('simulate render request', async t => { flags.render = true; }; const app = new App(element, renderFn); - var testApp = registerAsTest(app); + var testApp = getSimulator(app); const ctx = await testApp.render('/'); t.ok(flags.render, 'triggered ssr'); t.ok(ctx.element, 'sets ctx.element'); @@ -24,7 +24,7 @@ test('simulate multi-render requests', async t => { counter.renderCount++; }; const app = new App('hello', renderFn); - var testApp = registerAsTest(app); + var testApp = getSimulator(app); for (var i = 1; i <= 5; i++) { await testApp.render('/'); @@ -41,7 +41,7 @@ test('simulate non-render request', async t => { flags.render = true; }; const app = new App(element, renderFn); - const testApp = registerAsTest(app); + const testApp = getSimulator(app); if (__BROWSER__) { try { testApp.request('/'); @@ -72,7 +72,7 @@ test('use simulator with fixture and plugin dependencies', async t => { const app = getTestFixture(); t.plan(3); - registerAsTest( + getSimulator( app, withDependencies({ msgProvider: msgProviderPluginToken, diff --git a/src/index.js b/src/index.js index d2be9044..23a66aff 100644 --- a/src/index.js +++ b/src/index.js @@ -31,10 +31,7 @@ const render = (app: FusionApp) => ( return simulate(app, ctx); }; -export function registerAsTest( - app: FusionApp, - testPlugin?: FusionPlugin<*, *> -) { +export function getSimulator(app: FusionApp, testPlugin?: FusionPlugin<*, *>) { if (testPlugin) { app.register(testPlugin); } From 5b3323df4f2d329567c078806de0c72c12fd43c2 Mon Sep 17 00:00:00 2001 From: Alexander Smith Date: Tue, 16 Jan 2018 14:39:10 -0800 Subject: [PATCH 8/8] Update documentation --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) 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.