Skip to content

Commit

Permalink
Add testing harness that replicates FastBoot environment.
Browse files Browse the repository at this point in the history
Prior to this we were simulating FastBoot in a non-isolated VM context.
That meant that the running Node context was available within rendering
attempts. However, when ran with "real" FastBoot that is **not** the
case. In the real FastBoot environment, we create an isolated `vm`
context and evaluate both Ember and the application within that context.
This ensures that the running context does not have access to arbitrary
Node-land APIs (e.g. `require`).
  • Loading branch information
rwjblue committed Sep 2, 2020
1 parent 269d590 commit f9ef7dc
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 17 deletions.
14 changes: 0 additions & 14 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,6 @@ module.exports = {
'disable-features/disable-generator-functions': 'off',
}),
},
{
// matches node-land files that aren't shipped to consumers (allows using Node 6+ features)
files: [
'broccoli/**/*.js',
'tests/node/**/*.js',
'ember-cli-build.js',
'rollup.config.js',
'd8-runner.js',
],

rules: {
'node/no-unsupported-features': ['error', { version: 6 }],
}
},
{
files: [ 'node-tests/**/*.js' ],

Expand Down
8 changes: 8 additions & 0 deletions tests/node/component-rendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ QUnit.module('Components can be rendered without a DOM dependency', function(hoo

assert.ok(html.match(/rel="canonical"/));
});

QUnit.test('attributes requiring protocol sanitization do not error', function(assert) {
this.set('someHref', 'https://foo.com/');

let html = this.render('<a href={{this.someHref}}>Some Link</a>');

assert.ok(html.match(/<a href="https:\/\/foo.com\/">Some Link<\/a>/));
});
});
137 changes: 137 additions & 0 deletions tests/node/fastboot-sandbox-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const fs = require('fs');
const vm = require('vm');
const SimpleDOM = require('simple-dom');
const { emberPath, loadEmber, clearEmber } = require('./helpers/load-ember');

function assertHTMLMatches(assert, actualHTML, expectedHTML) {
assert.ok(actualHTML.match(expectedHTML), actualHTML + ' matches ' + expectedHTML);
}

function handleError(assert) {
return function(error) {
assert.ok(false, error.stack);
};
}

// This is based on what fastboot-server does
let HTMLSerializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);

async function fastbootVisit(context, url) {
let doc = new SimpleDOM.Document();
let rootElement = doc.body;
let options = { isBrowser: false, document: doc, rootElement: rootElement };

let { app } = context;

await app.boot();

let instance = await app.buildInstance();

try {
await instance.boot(options);
await instance.visit(url, options);

return {
url: instance.getURL(),
title: doc.title,
body: HTMLSerializer.serialize(rootElement),
};
} finally {
instance.destroy();
}
}

function assertFastbootResult(assert, expected) {
return function(actual) {
assert.equal(actual.url, expected.url);
assertHTMLMatches(assert, actual.body, expected.body);
};
}

// essentially doing the same as what is done in FastBoot 3.1.0
// https://github.com/ember-fastboot/fastboot/blob/v3.1.0/src/sandbox.js
function buildSandboxContext(precompile) {
let URL = require('url');

let sandbox = {
console,
setTimeout,
clearTimeout,
URL,

// Convince jQuery not to assume it's in a browser
module: { exports: {} },
};

// Set the global as `window`
sandbox.window = sandbox;
sandbox.window.self = sandbox;

let context = vm.createContext(sandbox);

let environmentSetupScript = new vm.Script(
`
var EmberENV = {
_TEMPLATE_ONLY_GLIMMER_COMPONENTS: true,
_APPLICATION_TEMPLATE_WRAPPER: false,
_DEFAULT_ASYNC_OBSERVERS: true,
_JQUERY_INTEGRATION: false,
};`,
{ filename: 'prepend.js' }
);
environmentSetupScript.runInContext(context);

let emberSource = fs.readFileSync(emberPath, { encoding: 'utf-8' });
let emberScript = new vm.Script(emberSource, { filename: emberPath });
emberScript.runInContext(context);

let applicationSource = `
class Router extends Ember.Router {}
Router.map(function() {
this.route('a');
this.route('b');
});
const registry = {
'router:main': Router,
'template:application': ${precompile('<h1>Hello world!</h1>\n{{outlet}}')}
};
class Resolver extends Ember.Object {
resolve(specifier) {
return registry[specifier];
}
}
var app = Ember.Application.extend().create({
autoboot: false,
Resolver,
});
`;
let appScript = new vm.Script(applicationSource, { filename: 'app.js' });
appScript.runInContext(context);

return context;
}

QUnit.module('Ember.Application - visit() Integration Tests', function(hooks) {
hooks.beforeEach(function() {
let { precompile } = loadEmber();
this.context = buildSandboxContext(precompile);
});

hooks.afterEach(function() {
clearEmber();
});

QUnit.test('FastBoot: basic', async function(assert) {
let result = await fastbootVisit(this.context, '/');

assert.equal(result.url, '/', 'landed on correct url');
assert.equal(
result.body,
'<body><h1>Hello world!</h1>\n<!----></body>',
'results in expected HTML'
);
});
});
14 changes: 11 additions & 3 deletions tests/node/helpers/load-ember.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ const templateCompilerPath = path.join(distPath, 'ember-template-compiler');
// properly to avoid the @glimmer/validator assertion
const originalGlobalSymbols = Object.getOwnPropertySymbols(global).map(sym => [sym, global[sym]]);

module.exports.emberPath = require.resolve(emberPath);

module.exports.loadEmber = function() {
let Ember = require(emberPath);

let precompile = require(templateCompilerPath).precompile;
let _precompile = require(templateCompilerPath).precompile;

let precompile = function(templateString, options) {
let templateSpec = _precompile(templateString, options);

return `Ember.HTMLBars.template(${templateSpec})`;
};

let compile = function(templateString, options) {
let templateSpec = precompile(templateString, options);
let templateSpec = _precompile(templateString, options);
let template = new Function('return ' + templateSpec)();

return Ember.HTMLBars.template(template);
};

return { Ember, compile };
return { Ember, compile, precompile };
};

module.exports.clearEmber = function() {
Expand Down

0 comments on commit f9ef7dc

Please sign in to comment.