catberry
Version:
Catberry is an isomorphic framework for building universal front-end apps using components, Flux architecture and progressive rendering.
484 lines (434 loc) • 14.4 kB
JavaScript
;
const assert = require('assert');
const events = require('events');
const URI = require('catberry-uri').URI;
const ServerResponse = require('../mocks/ServerResponse');
const ModuleApiProvider = require('../../lib/providers/ModuleApiProvider');
const CookieWrapper = require('../../lib/CookieWrapper');
const ServiceLocator = require('catberry-locator');
const ContextFactory = require('../../lib/ContextFactory');
const StoreDispatcher = require('../../lib/StoreDispatcher');
const DocumentRenderer = require('../../lib/DocumentRenderer');
const componentMocks = require('../mocks/components');
const testUtils = require('../utils');
const testCases = require('../cases/lib/DocumentRenderer/test-cases.json');
const TEMPLATES_DIR = `${__dirname}/../cases/lib/DocumentRenderer/templates/`;
const EXPECTED_DIR = `${__dirname}/../cases/lib/DocumentRenderer/expected/`;
function prepareTestCase(testCase) {
const preparedTestCase = Object.create(testCase);
preparedTestCase.components = {};
preparedTestCase.stores = {};
if (testCase.components) {
preparedTestCase.components = testUtils.prepareComponents(TEMPLATES_DIR, testCase.components);
}
if (testCase.stores) {
preparedTestCase.stores = testUtils.prepareStores(testCase.stores);
}
if (preparedTestCase.expectedHTML !== '') {
preparedTestCase.expectedHTML = testUtils.getHTML(
`${EXPECTED_DIR}${testCase.expectedHTML}`
);
}
return preparedTestCase;
}
/* eslint prefer-arrow-callback:0 */
/* eslint max-nested-callbacks:0 */
/* eslint require-jsdoc:0 */
describe('lib/DocumentRenderer', function() {
describe('#render', function() {
testCases.render.forEach(testCase => {
it(testCase.name, function(done) {
const preparedTestCase = prepareTestCase(testCase);
const routingContext = createRoutingContext(
preparedTestCase.config || {},
preparedTestCase.stores, preparedTestCase.components
);
const documentRenderer = routingContext.locator.resolve('documentRenderer');
documentRenderer.render({}, routingContext);
const response = routingContext.middleware.response;
response
.on('error', done)
.on('finish', () => {
try {
assert.strictEqual(
response.result, preparedTestCase.expectedHTML
);
done();
} catch (e) {
done(e);
}
});
});
});
it('should set code 200 and required headers', function(done) {
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const documentRenderer = routingContext.locator.resolve('documentRenderer');
documentRenderer.render({}, routingContext);
const response = routingContext.middleware.response;
response
.on('error', done)
.on('finish', () => {
assert.strictEqual(response.status, 200);
assert.deepEqual(response.setHeaders, {
'Content-Type': 'text/html; charset=utf-8',
'X-Powered-By': 'Catberry'
});
done();
});
});
it('should set code 302 and Location if redirect in HEAD', function(done) {
class Head {
render() {
this.$context.redirect('/to/garden');
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document-with-head.html`)
},
head: {
name: 'head',
constructor: Head,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}head.html`)
},
'async-comp': {
name: 'async-comp',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.strictEqual(response.result, '');
assert.strictEqual(response.status, 302);
assert.strictEqual(
response.setHeaders.Location, '/to/garden'
);
done();
});
});
it('should set code 301 and Location if permanent redirection in HEAD occurs', function(done) {
class Head {
render() {
this.$context.permRedirect('/to/garden');
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document-with-head.html`)
},
head: {
name: 'head',
constructor: Head,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}head.html`)
},
'async-comp': {
name: 'async-comp',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.strictEqual(response.result, '');
assert.strictEqual(response.status, 301);
assert.strictEqual(
response.setHeaders.Location, '/to/garden'
);
done();
});
});
it('should set header if set cookie in HEAD', function(done) {
class Head {
render() {
this.$context.cookie.set({
key: 'first',
value: 'value1'
});
this.$context.cookie.set({
key: 'second',
value: 'value2'
});
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document-with-head.html`)
},
head: {
name: 'head',
constructor: Head,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}head.html`)
},
'async-comp': {
name: 'async-comp',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.strictEqual(response.status, 200);
assert.deepEqual(
response.setHeaders['Set-Cookie'], [
'first=value1',
'second=value2'
]
);
done();
});
});
it('should pass to the next middleware if notFound()', function(done) {
class Head {
render() {
this.$context.notFound();
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document-with-head.html`)
},
head: {
name: 'head',
constructor: Head,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}head.html`)
},
'async-comp': {
name: 'async-comp',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
routingContext.middleware.next = function() {
done();
};
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.fail('Should not finish the response');
});
});
it('should render inline script if clearFragment() in HEAD', function(done) {
class Head {
render() {
this.$context.clearFragment();
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document-with-head.html`)
},
head: {
name: 'head',
constructor: Head,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}head.html`)
},
'async-comp': {
name: 'async-comp',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
const expectToHave = '<body><script>window.location.hash = \'\';</script>';
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.notEqual(response.result.indexOf(expectToHave), -1);
done();
});
});
it('should render inline script if clearFragment()', function(done) {
class ClearFragmentComponent {
render() {
this.$context.clearFragment();
return this.$context;
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document.html`)
},
comp: {
name: 'comp',
constructor: ClearFragmentComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
const expectToHave = '<cat-comp>' +
'<script>window.location.hash = \'\';</script>';
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.notEqual(response.result.indexOf(expectToHave), -1);
done();
});
});
it('should render inline script if redirect()', function(done) {
class RedirectComponent {
render() {
this.$context.redirect('/to/garden');
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document.html`)
},
comp: {
name: 'comp',
constructor: RedirectComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
const expectToHave = '<cat-comp>' +
'<script>window.location.assign(\'/to/garden\');</script>';
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.notEqual(response.result.indexOf(expectToHave), -1);
done();
});
});
it('should render inline script if cookie.set()', function(done) {
class CookieComponent {
render() {
this.$context.cookie.set({
key: 'key',
value: 'value'
});
}
}
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document.html`)
},
comp: {
name: 'comp',
constructor: CookieComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
const expectToHave = '<cat-comp>' +
'<script>window.document.cookie = \'key=value\';</script>';
documentRenderer.render({}, routingContext);
response
.on('error', done)
.on('finish', () => {
assert.notEqual(response.result.indexOf(expectToHave), -1);
done();
});
});
it('should properly render debug info', function(done) {
const components = {
document: {
name: 'document',
constructor: componentMocks.AsyncComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}document.html`)
},
comp: {
name: 'comp',
constructor: componentMocks.AsyncErrorComponent,
template: testUtils.createTemplateObject(`${TEMPLATES_DIR}component.html`)
}
};
const routingContext = createRoutingContext({}, {}, components);
const response = routingContext.middleware.response;
const documentRenderer = routingContext.locator.resolve('documentRenderer');
const expectToHave = 'Error: comp';
documentRenderer.render({}, routingContext);
routingContext.middleware.response
.on('error', done)
.on('finish', () => {
assert.notEqual(response.result.indexOf(expectToHave), -1);
done();
});
});
});
});
function createRoutingContext(config, stores, components) {
const locator = new ServiceLocator();
locator.register('cookieWrapper', CookieWrapper, config);
locator.register('contextFactory', ContextFactory, config, true);
locator.register('documentRenderer', DocumentRenderer, config, true);
locator.register('moduleApiProvider', ModuleApiProvider, config, true);
locator.register('storeDispatcher', StoreDispatcher);
locator.registerInstance('componentLoader', {
load: () => Promise.resolve(),
getComponentsByNames: () => components
});
locator.registerInstance('storeLoader', {
load: () => Promise.resolve(),
getStoresByNames: () => stores
});
locator.registerInstance('serviceLocator', locator);
locator.registerInstance('config', config);
const eventBus = new events.EventEmitter();
eventBus.on('error', function() {});
locator.registerInstance('eventBus', eventBus);
const contextFactory = locator.resolve('contextFactory');
return contextFactory.create({
referrer: new URI(),
location: new URI(),
userAgent: 'test',
middleware: {
response: new ServerResponse(),
next: () => {}
}
});
}