Skip to content

Commit 45c4ef0

Browse files
authored
Merge pull request #567 from travi/alpha
Alpha
2 parents df852f1 + 86298c3 commit 45c4ef0

File tree

9 files changed

+99
-28
lines changed

9 files changed

+99
-28
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ deploy:
1111
provider: script
1212
skip_cleanup: true
1313
script: npx semantic-release@beta
14+
on:
15+
all_branches: true
1416
env:
1517
global:
1618
- FORCE_COLOR=1

example/layout.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
</head>
77
<body>
8-
<div id="wrap"><div>{{{ renderedContent }}}</div></div>
8+
<div id="wrap"><div>{{{ renderedContent.html }}}</div></div>
99
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"> </script>
1010
</body>
1111
</html>

src/default-render-factory.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import {renderToString} from 'react-dom/server';
3+
import {RouterContext} from 'react-router';
4+
5+
export default function (request, store, renderProps, Root) {
6+
return otherProps => renderToString(
7+
<Root request={request} store={store} {...otherProps}>
8+
<RouterContext {...renderProps} />
9+
</Root>
10+
);
11+
}

src/route.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const plugin = {
88
method: 'GET',
99
path: '/html',
1010
handler: (request, h) => renderThroughReactRouter(request, h, {
11+
render: options.render,
1112
routes: options.routes,
1213
respond: options.respond,
1314
Root: options.Root,

src/router-wrapper.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import React from 'react';
2-
import {renderToString} from 'react-dom/server';
3-
import {RouterContext} from 'react-router';
41
import Boom from 'boom';
52
import {MOVED_PERMANENTLY, MOVED_TEMPORARILY} from 'http-status-codes';
63
import matchRoute from './route-matcher';
74
import fetchData from './data-fetcher';
5+
import defaultRenderFactory from './default-render-factory';
86

9-
export default async function renderThroughReactRouter(request, h, {routes, respond, Root, store}) {
7+
export default async function renderThroughReactRouter(request, h, {render, routes, respond, Root, store}) {
108
try {
119
const {renderProps, status, redirectLocation} = await matchRoute(request.raw.req.url, routes);
1210

@@ -24,14 +22,12 @@ export default async function renderThroughReactRouter(request, h, {routes, resp
2422
} else {
2523
await fetchData({renderProps, store});
2624

25+
const defaultRender = defaultRenderFactory(request, store, renderProps, Root);
26+
2727
return respond(h, {
2828
store,
2929
status,
30-
renderedContent: renderToString((
31-
<Root request={request} store={store}>
32-
<RouterContext {...renderProps} />
33-
</Root>
34-
))
30+
renderedContent: render ? render(defaultRender) : {html: defaultRender()}
3531
});
3632
}
3733
} catch (e) {

test/integration/features/step_definitions/render-steps.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import {OK} from 'http-status-codes';
22
import {assert} from 'chai';
33
import {When, Then} from 'cucumber';
44

5-
When('a request is made for an existing route', function () {
5+
When(/^a request is made for an existing route$/, function () {
66
return this.makeRequest({url: '/existing-route'});
77
});
88

9-
Then('the route is rendered successfully', function (callback) {
9+
Then(/^the route is rendered successfully$/, function (callback) {
1010
assert.equal(this.serverResponse.statusCode, OK);
1111
assert.equal(this.serverResponse.headers['content-type'], 'text/html; charset=utf-8');
1212

1313
callback();
1414
});
1515

16-
Then('asynchronously fetched data is included in the page', function (callback) {
16+
Then(/^asynchronously fetched data is included in the page$/, function (callback) {
1717
assert.include(this.serverResponse.payload, this.dataPoint);
1818

1919
callback();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import domServer from 'react-dom/server';
3+
import {RouterContext} from 'react-router';
4+
import sinon from 'sinon';
5+
import any from '@travi/any';
6+
import {assert} from 'chai';
7+
import defaultRenderFactory from '../../src/default-render-factory';
8+
9+
suite('default-render factory', () => {
10+
let sandbox;
11+
const Root = any.simpleObject();
12+
const store = any.simpleObject();
13+
14+
setup(() => {
15+
sandbox = sinon.createSandbox();
16+
17+
sandbox.stub(React, 'createElement');
18+
sandbox.stub(domServer, 'renderToString');
19+
});
20+
21+
teardown(() => sandbox.restore());
22+
23+
test('that the router-context is rendered within the provided root component', () => {
24+
const renderProps = any.simpleObject();
25+
const rootComponent = any.simpleObject();
26+
const html = any.string();
27+
const request = any.simpleObject();
28+
React.createElement.withArgs(RouterContext, renderProps).returns(context);
29+
React.createElement.withArgs(Root, {request, store}, context).returns(rootComponent);
30+
domServer.renderToString.withArgs(rootComponent).returns(html);
31+
32+
assert.equal(defaultRenderFactory(request, store, renderProps, Root)(), html);
33+
});
34+
35+
test('that the additional props are passed to the root component, when provided', () => {
36+
const renderProps = any.simpleObject();
37+
const rootComponent = any.simpleObject();
38+
const html = any.string();
39+
const request = any.simpleObject();
40+
const otherProps = any.simpleObject();
41+
React.createElement.withArgs(RouterContext, renderProps).returns(context);
42+
React.createElement.withArgs(Root, {request, store, ...otherProps}, context).returns(rootComponent);
43+
domServer.renderToString.withArgs(rootComponent).returns(html);
44+
45+
assert.equal(defaultRenderFactory(request, store, renderProps, Root)(otherProps), html);
46+
});
47+
});

test/unit/route-test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ suite('route', () => {
2020
});
2121

2222
test('that the request for html is handled', async () => {
23+
const render = () => undefined;
2324
const route = sinon.stub();
2425
const respond = sinon.spy();
2526
const routes = sinon.spy();
@@ -32,7 +33,7 @@ suite('route', () => {
3233
const configureStore = sinon.stub();
3334
configureStore.withArgs({session: {auth: auth.credentials}, server}).returns(store);
3435

35-
await plugin.register(server, {respond, routes, Root, configureStore});
36+
await plugin.register(server, {render, respond, routes, Root, configureStore});
3637

3738
assert.calledWith(route, sinon.match({
3839
method: 'GET',
@@ -41,6 +42,6 @@ suite('route', () => {
4142

4243
route.yieldTo('handler', request, reply);
4344

44-
assert.calledWith(routerWrapper.default, request, reply, {routes, respond, Root, store});
45+
assert.calledWith(routerWrapper.default, request, reply, {render, routes, respond, Root, store});
4546
});
4647
});

test/unit/router-wrapper-test.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import React from 'react';
2-
import {RouterContext} from 'react-router';
3-
import domServer from 'react-dom/server';
4-
import {MOVED_TEMPORARILY, MOVED_PERMANENTLY} from 'http-status-codes';
1+
import {MOVED_PERMANENTLY, MOVED_TEMPORARILY} from 'http-status-codes';
52
import sinon from 'sinon';
63
import {assert} from 'chai';
74
import any from '@travi/any';
85
import Boom from 'boom';
96
import renderThroughReactRouter from '../../src/router-wrapper';
7+
import * as defaultRenderFactory from '../../src/default-render-factory';
108
import * as routeMatcher from '../../src/route-matcher';
119
import * as dataFetcher from '../../src/data-fetcher';
1210

@@ -25,8 +23,7 @@ suite('router-wrapper', () => {
2523
sandbox.stub(routeMatcher, 'default');
2624
sandbox.stub(dataFetcher, 'default');
2725
sandbox.stub(Boom, 'wrap');
28-
sandbox.stub(React, 'createElement');
29-
sandbox.stub(domServer, 'renderToString');
26+
sandbox.stub(defaultRenderFactory, 'default');
3027
});
3128

3229
teardown(() => sandbox.restore());
@@ -36,20 +33,36 @@ suite('router-wrapper', () => {
3633
const reply = sinon.spy();
3734
const renderProps = any.simpleObject();
3835
const status = any.integer();
39-
const context = any.simpleObject();
40-
const rootComponent = any.simpleObject();
41-
const renderedContent = any.string();
36+
const html = any.string();
4237
const response = any.string();
38+
const defaultRender = sinon.stub();
4339
routeMatcher.default.withArgs(url, routes).resolves({renderProps, status});
4440
dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status});
45-
React.createElement.withArgs(RouterContext, sinon.match(renderProps)).returns(context);
46-
React.createElement.withArgs(Root, {request, store}).returns(rootComponent);
47-
domServer.renderToString.withArgs(rootComponent).returns(renderedContent);
48-
respond.withArgs(reply, {renderedContent, store, status}).returns(response);
41+
defaultRender.returns(html);
42+
defaultRenderFactory.default.withArgs(request, store, renderProps, Root).returns(defaultRender);
43+
respond.withArgs(reply, {renderedContent: {html}, store, status}).returns(response);
4944

5045
return assert.becomes(renderThroughReactRouter(request, reply, {routes, respond, Root, store}), response);
5146
});
5247

48+
test('that response contains the custom-rendered content when a custom renderer is provided', async () => {
49+
const respond = sinon.stub();
50+
const reply = sinon.spy();
51+
const renderProps = any.simpleObject();
52+
const status = any.integer();
53+
const response = any.string();
54+
const render = sinon.stub();
55+
const renderedContent = any.simpleObject();
56+
const defaultRender = () => undefined;
57+
routeMatcher.default.withArgs(url, routes).resolves({renderProps, status});
58+
dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status});
59+
respond.withArgs(reply, {renderedContent, store, status}).returns(response);
60+
defaultRenderFactory.default.withArgs(request, store, renderProps, Root).returns(defaultRender);
61+
render.withArgs(defaultRender).returns(renderedContent);
62+
63+
assert.equal(await renderThroughReactRouter(request, reply, {render, routes, respond, Root, store}), response);
64+
});
65+
5366
test('that a temporary redirect results when a redirectLocation is defined with a 302 status', () => {
5467
const respond = sinon.stub();
5568
const redirect = sinon.stub();

0 commit comments

Comments
 (0)