apostrophe
Version:
The Apostrophe Content Management System.
509 lines (457 loc) • 18.9 kB
JavaScript
const t = require('../test-lib/test.js');
const assert = require('assert/strict');
const cheerio = require('cheerio');
const Promise = require('bluebird');
describe('Templates', function() {
let apos;
this.timeout(t.timeout);
after(async function() {
return t.destroy(apos);
});
/**
* Helper ofr grabbing output between --label-- ... --endlabel--, split it to
* lines and remove all whitespace
*
* @param {String} result page output
* @param {String} label a test case marker
* @returns {Array<String>} array of non-empty lines
*/
const parseOutput = (result, label) => {
const regx = new RegExp(`--${label}--([\\S\\s]*?)--end${label}--`, 'g');
const m = result.match(regx);
const arr = m[0].split('\n');
return arr
.slice(1, arr.length - 1)
.filter(s => !!s.trim())
.map(s => s.trim());
};
before(async function() {
apos = await t.create({
root: module,
modules: {
'express-test': {},
'template-test': {
options: {
ignoreNoCodeWarning: true
}
},
'template-subclass-test': {
options: {
ignoreNoCodeWarning: true
}
},
'template-options-test': {},
'inject-test': {},
'with-layout-page': {
extend: '@apostrophecms/page-type'
},
'fragment-page': {
extend: '@apostrophecms/page-type',
components(self) {
return {
async test(req, input) {
// Be very async
await Promise.delay(100);
input.afterDelay = true;
return input;
}
};
}
},
'fragment-all': {
components(self) {
return {
async test(req, input) {
// Be very async
await Promise.delay(100);
input.afterDelay = true;
return input;
}
};
}
},
'inject-nodes': {
init(self) {
self.prependNodes('head', 'prependHeadTest');
self.appendNodes('head', 'appendHeadTest');
self.prependNodes('main', 'prependMainTest');
self.appendNodes('main', 'appendMainTest');
self.appendNodes('body', 'appendBodyTest');
},
methods(self) {
return {
prependHeadTest(req) {
return [
{
name: 'meta',
attrs: {
name: 'prepend-node-head-test>'
}
}
];
},
appendHeadTest(req) {
return [
{
name: 'meta',
attrs: {
name: 'append-node-head-test>'
}
}
];
},
prependMainTest(req) {
return [
{
name: 'h4',
body: [
{
text: 'prepend-node-main-test<test>'
}
]
}
];
},
appendMainTest(req) {
return [
{
name: 'h4',
body: [
{
text: 'append-node-main-test<test>'
}
]
}
];
},
appendBodyTest(req) {
return [
{
comment: 'append-node-body-comment-test'
},
{
raw: '<h4>append-node-body-raw-test</h4>'
},
{
name: 'h3',
attrs: {
boolean: true,
ignored1: false,
ignored2: null,
ignored3: undefined,
truthy: 'true',
falsy: 'false'
},
body: [
{
text: 'append-node-body-misc-test'
}
]
}
];
}
};
}
}
}
});
});
it('should be able to render a template relative to a module', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].render(req, 'test', { age: 50 });
assert(result === '<h1>50</h1>\n');
});
it('should respect templateData at module level', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].render(req, 'test');
assert(result === '<h1>30</h1>\n');
});
it('should respect template overrides', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-subclass-test'].render(req, 'override-test');
assert(result === '<h1>I am overridden</h1>\n');
});
it('should inherit in the absence of overrides', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-subclass-test'].render(req, 'inherit-test');
assert(result === '<h1>I am inherited</h1>\n');
});
it('should be able to see the options of the module via module.options', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-options-test'].render(req, 'options-test');
assert(result.match(/nifty/));
});
it('should be able to call helpers on the modules object', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-options-test'].render(req, 'options-test');
assert(result.match(/4/));
});
it('should render pages successfully with outerLayout, with core data-apos attribute', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].renderPage(req, 'page');
const $ = cheerio.load(result);
const $body = $('body');
assert($body.length);
const aposData = JSON.parse($body.attr('data-apos'));
assert(aposData);
assert(aposData.shortName);
assert(aposData.csrfCookieName);
assert(!aposData.modules['@apostrophecms/admin-bar']);
assert(result.indexOf('<title>I am the title</title>') !== -1);
assert(result.indexOf('<h2>I am the main content</h2>') !== -1);
});
it('should render pages successfully with outerLayout for admin user, with expanded data-apos attribute', async function() {
const req = apos.task.getReq();
const result = await apos.modules['template-test'].renderPage(req, 'page');
const $ = cheerio.load(result);
const $body = $('body');
assert($body.length);
const aposData = JSON.parse($body.attr('data-apos'));
assert(aposData);
assert(aposData.shortName);
assert(aposData.modules['@apostrophecms/admin-bar'].items.length);
assert(result.indexOf('<title>I am the title</title>') !== -1);
assert(result.indexOf('<h2>I am the main content</h2>') !== -1);
});
it('cross-module-included files should be able to include/extend other files relative to their own module', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].renderPage(req, 'pageWithLayout');
assert(result.indexOf('<title>I am the title</title>') !== -1);
assert(result.indexOf('<h2>I am the inner content</h2>') !== -1);
assert(result.indexOf('<h3>I am in the layout</h3>') !== -1);
assert(result.indexOf('<p>I am included</p>') !== -1);
});
it('should render pages successfully with prepend and append to locations', async function() {
const req = apos.task.getReq();
const result = await apos.modules['with-layout-page'].renderPage(req, 'page');
const titleIndex = result.indexOf('<title>');
const beforeTestIndex = result.indexOf('<meta name="prepend-head-test" />');
const afterTestIndex = result.indexOf('<meta name="append-head-test" />');
const bodyIndex = result.indexOf('<body');
const appendBody = result.indexOf('<h4>append-body-test</h4>');
assert(titleIndex !== -1);
assert(beforeTestIndex !== -1);
assert(afterTestIndex !== -1);
assert(bodyIndex !== -1);
assert(appendBody !== -1);
assert(beforeTestIndex < titleIndex);
assert(afterTestIndex > titleIndex);
assert(afterTestIndex < bodyIndex);
});
it('should render pages successfully with prepend, append and conditions', async function() {
const req = apos.task.getReq();
const result = (await apos.modules['with-layout-page'].renderPage(req, 'page'))
.split('<body')[0];
const separatorIndex = result.indexOf('<meta name="condition-separator-test" />');
const prependViteIndex = result.indexOf('<meta name="prepend-vite-test" />');
const appendDevViteIndex = result.indexOf('<meta name="append-dev-vite-test" />');
const appendProdWebpackIndex = result.indexOf('<meta name="append-prod-webpack-test" />');
const prependProdIndex = result.indexOf('<meta name="prepend-prod-test" />');
const prependDevViteIndex = result.indexOf('<meta name="prepend-dev-vite-test" />');
const prependWebpackIndex = result.indexOf('<meta name="prepend-webpack-test" />');
const prependDevIndex = result.indexOf('<meta name="prepend-dev-test" />');
const prependDevWebpackIndex = result.indexOf('<meta name="prepend-dev-webpack-test" />');
const appendDevWebpackIndex = result.indexOf('<meta name="append-dev-webpack-test" />');
const appendDevIndex = result.indexOf('<meta name="append-dev-test" />');
assert.ok(separatorIndex !== -1);
assert.equal(prependViteIndex, -1);
assert.equal(appendDevViteIndex, -1);
assert.equal(appendProdWebpackIndex, -1);
assert.equal(prependProdIndex, -1);
assert.equal(prependDevViteIndex, -1);
assert.ok(prependWebpackIndex !== -1);
assert.ok(appendDevWebpackIndex !== -1);
assert.ok(appendDevIndex !== -1);
assert.ok(prependDevIndex !== -1);
assert.ok(prependDevWebpackIndex !== -1);
assert.ok(prependWebpackIndex < separatorIndex);
// because the inject `when` dev is in our test,
// while this one goes into the original outerLayoutBase.html
assert.ok(prependWebpackIndex < appendDevWebpackIndex);
assert.ok(prependDevIndex < separatorIndex);
assert.ok(prependDevWebpackIndex < separatorIndex);
assert.ok(prependDevIndex < prependDevWebpackIndex);
assert.ok(appendDevWebpackIndex > separatorIndex);
assert.ok(appendDevIndex > separatorIndex);
assert.ok(appendDevWebpackIndex < appendDevIndex);
});
it('should render pages successfully with nodes prepend and append', async function() {
const req = apos.task.getReq();
const html = (await apos.modules['with-layout-page'].renderPage(req, 'page'))
.split('<body');
const head = html[0];
const body = html[1];
const prependNodeHeadTestIndex = head.indexOf('<meta name="prepend-node-head-test>" />');
const appendNodeHeadTestIndex = head.indexOf('<meta name="append-node-head-test>" />');
const prependNodeMainTestIndex = body.indexOf('<h4>prepend-node-main-test<test></h4>');
const appendNodeMainTestIndex = body.indexOf('<h4>append-node-main-test<test></h4>');
// Duplicate checks
const prependNodeHeadLastIndex = head.lastIndexOf('<meta name="prepend-node-head-test>" />');
const appendNodeHeadLastIndex = head.lastIndexOf('<meta name="append-node-head-test>" />');
const prependNodeMainLastIndex = body.lastIndexOf('<h4>prepend-node-main-test<test></h4>');
const appendNodeMainLastIndex = body.lastIndexOf('<h4>append-node-main-test<test></h4>');
// Comment/raw checks
const appendNodeBodyCommentTestIndex = body.indexOf('<!-- append-node-body-comment-test -->');
const appendNodeBodyRawTestIndex = body.indexOf('<h4>append-node-body-raw-test</h4>');
// Attribute handling checks
const appendNodeBodyMiscTestIndex = body.indexOf(
'<h3 boolean truthy="true" falsy="false">append-node-body-misc-test</h3>'
);
const actual = {
prependHeadExist: prependNodeHeadTestIndex !== -1,
appendHeadExist: appendNodeHeadTestIndex !== -1,
prependHeadNoDuplicate: prependNodeHeadTestIndex === prependNodeHeadLastIndex,
appendHeadNoDuplicate: appendNodeHeadTestIndex === appendNodeHeadLastIndex,
headOrder: prependNodeHeadTestIndex < appendNodeHeadTestIndex,
prependMainExist: prependNodeMainTestIndex !== -1,
appendMainExist: appendNodeMainTestIndex !== -1,
prependMainNoDuplicate: prependNodeMainTestIndex === prependNodeMainLastIndex,
appendMainNoDuplicate: appendNodeMainTestIndex === appendNodeMainLastIndex,
mainOrder: prependNodeMainTestIndex < appendNodeMainTestIndex,
appendBodyCommentExist: appendNodeBodyCommentTestIndex !== -1,
appendBodyRawExist: appendNodeBodyRawTestIndex !== -1,
appendNodeBodyAttrsCheck: appendNodeBodyMiscTestIndex !== -1
};
const expected = {
prependHeadExist: true,
appendHeadExist: true,
prependHeadNoDuplicate: true,
appendHeadNoDuplicate: true,
headOrder: true,
prependMainExist: true,
appendMainExist: true,
prependMainNoDuplicate: true,
appendMainNoDuplicate: true,
mainOrder: true,
appendBodyCommentExist: true,
appendBodyRawExist: true,
appendNodeBodyAttrsCheck: true
};
assert.deepEqual(
actual,
expected,
'There was an issue with the prepend/append node rendering.'
);
});
it('should not escape <br /> generated by the nlbr filter, but should escape tags in its input', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].render(req, 'testWithNlbrFilter');
assert.strictEqual(result, '<p>first line<br />\nsecond line<br />\n<a href="javascript:alert(\'oh no\')">CSRF attempt</a></p>\n');
});
it('should not escape <br /> generated by the nlp filter, but should escape tags in its input', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].render(req, 'testWithNlpFilter');
assert.strictEqual(result, '<p>first line</p>\n<p>second line</p>\n<p><a href="javascript:alert(\'oh no\')">CSRF attempt</a></p>\n');
});
it('should not escape <br /> generated by the nlbr filter or markup in the test input', async function() {
const req = apos.task.getAnonReq();
const result = await apos.modules['template-test'].render(req, 'testWithNlbrFilterSafe');
assert.strictEqual(result, '<p>first line<br />\nsecond line<br />\n<a href="http://niceurl.com">This is okay</a></p>\n');
});
it('should render fragments containing async components correctly', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-page'].renderPage(req, 'page');
const aboveFragment = result.indexOf('Above Fragment');
const beforeComponent = result.indexOf('Before Component');
const componentText = result.indexOf('Component Text');
const afterDelay = result.indexOf('After Delay');
const afterComponent = result.indexOf('After Component');
const belowFragment = result.indexOf('Below Fragment');
assert(aboveFragment !== -1);
assert(beforeComponent !== -1);
assert(componentText !== -1);
assert(afterDelay !== -1);
assert(afterComponent !== -1);
assert(belowFragment !== -1);
assert(aboveFragment < beforeComponent);
assert(beforeComponent < componentText);
assert(componentText < afterDelay);
assert(afterDelay < afterComponent);
assert(afterComponent < belowFragment);
});
it('should render fragment without passing any keyword arguments', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'test1');
assert.deepStrictEqual(data, [
'pos1',
'pos2',
'kw1_default',
'kw2_default',
'kw3_default'
]);
});
it('should support keyword arguments and render macros and fragments from other fragments', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'test2');
assert.deepStrictEqual(data, [
'Above Fragment',
'pos1',
'pos2',
'pos3',
'kw1_default',
'kw2',
'kw3_default',
'Below Fragment'
]);
});
it('should render rendercall blocks', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'test3');
assert.deepStrictEqual(data, [
'Above Call Fragment',
'pos1',
'pos2',
'pos3',
'Start Call Body',
'Text is called',
'After Delay',
'End Call Body',
'kw1_default',
'kw2',
'kw3_default',
'Below Call Fragment'
]);
});
it('should skip positional arguments when there is keyword arguments (1)', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'issue_3056_1');
assert.deepStrictEqual(data, [
'val_1',
'val_',
'val_9',
'val_4'
]);
});
it('should skip positional arguments when there is keyword arguments (2)', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'issue_3056_2');
assert.deepStrictEqual(data, [
'val_1',
'val_',
'val_9',
'val_4'
]);
});
it('should filter out unknown keyword arguments', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'page');
const data = parseOutput(result, 'issue_3102');
assert.deepStrictEqual(data, [
'val_',
'val_1',
'val_2',
'val_'
]);
});
it('should support apos helpers and localization in fragments', async function() {
const req = apos.task.getReq();
const result = await apos.modules['fragment-all'].renderPage(req, 'aux-test');
assert(result.includes('gee-whiz'));
assert(result.includes('Modify / Delete'));
});
});