derby
Version:
MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
171 lines (170 loc) • 8.38 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertions = void 0;
var ComponentHarness_1 = require("./ComponentHarness");
require("chai");
/**
* @param { {window: Window } } [dom] - _optional_ - An object that will have a `window` property
* set during test execution. If not provided, the global `window` will be used.
* @param {Assertion} [chai.Assertion] - _optional_ - Chai's Assertion class. If provided, the
* chainable expect methods `#html(expected)` and `#render(expected)` will be added to Chai.
*/
function assertions(dom, Assertion) {
var getWindow = dom ?
function () { return dom.window; } :
function () { return window; };
function removeComments(node) {
var domDocument = getWindow().document;
var clone = domDocument.importNode(node, true);
// last two arguments for createTreeWalker are required in IE
// NodeFilter.SHOW_COMMENT === 128
var treeWalker = domDocument.createTreeWalker(clone, 128, null, false);
var toRemove = [];
for (var item = treeWalker.nextNode(); item != null; item = treeWalker.nextNode()) {
toRemove.push(item);
}
for (var i = toRemove.length; i--;) {
toRemove[i].parentNode.removeChild(toRemove[i]);
}
return clone;
}
function getHtml(node, parentTag) {
var domDocument = getWindow().document;
// We use the <ins> element, because it has a transparent content model:
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Transparent_content_model
//
// In practice, DOM validity isn't enforced by browsers when using
// appendChild and innerHTML, so specifying a valid parentTag for the node
// should not be necessary
var el = domDocument.createElement(parentTag || 'ins');
var clone = domDocument.importNode(node, true);
el.appendChild(clone);
return el.innerHTML;
}
// Executes the parts of `Page#destroy` pertaining to the model, which get
// re-done when a new Page gets created on the same model. Normally, using
// `Page#destroy` would be fine, but the `.to.render` assertion wants to do
// 3 rendering passes on the same data, so we can't completely clear the
// model's state between the rendering passes.
function resetPageModel(page) {
page._removeModelListeners();
for (var componentId in page._components) {
var component = page._components[componentId];
component.destroy();
}
page.model.silent().destroy('$components');
}
if (Assertion) {
Assertion.addMethod('html', function (expected, options) {
var obj = this._obj;
var includeComments = options && options.includeComments;
var parentTag = options && options.parentTag;
var domNode = getWindow().Node;
new Assertion(obj).instanceOf(domNode);
new Assertion(expected).is.a('string');
var fragment = (includeComments) ? obj : removeComments(obj);
var html = getHtml(fragment, parentTag);
this.assert(html === expected, 'expected DOM rendering to produce the HTML #{exp} but got #{act}', 'expected DOM rendering to not produce actual HTML #{act}', expected, html);
});
Assertion.addMethod('render', function (expected, options) {
var harness = this._obj;
if (expected && typeof expected === 'object') {
options = expected;
expected = null;
}
var domDocument = getWindow().document;
var parentTag = (options && options.parentTag) || 'ins';
var firstFailureMessage, actual;
new Assertion(harness).instanceOf(ComponentHarness_1.ComponentHarness);
// Render to a HTML string.
var renderResult = harness.renderHtml(options);
var htmlString = renderResult.html;
// Normalize `htmlString` into the same form as the DOM would give for `element.innerHTML`.
//
// derby-parsing uses htmlUtil.unescapeEntities(source) on text nodes' content. That converts
// HTML entities like ' ' to their corresponding Unicode characters. However, for this
// assertion, if the `expected` string is provided, it will not have that same transformation.
// To make the assertion work properly, normalize the actual `htmlString`.
var html = normalizeHtml_1(htmlString);
var htmlRenderingOk;
if (expected == null) {
// If `expected` is not provided, then we skip this check.
// Set `expected` as the normalized HTML string for subsequent checks.
expected = html;
htmlRenderingOk = true;
}
else {
// If `expected` was originally provided, check that the normalized HTML string is equal.
new Assertion(expected).is.a('string');
// Check HTML matches expected value
htmlRenderingOk = html === expected;
if (!htmlRenderingOk) {
if (!firstFailureMessage) {
firstFailureMessage = 'HTML string rendering does not match expected HTML';
actual = html;
}
}
}
resetPageModel(renderResult.page);
// Check DOM rendering is also equivalent.
// This uses the harness "pageRendered" event to grab the rendered DOM *before* any component
// `create()` methods are called, as `create()` methods can do DOM mutations.
var domRenderingOk;
harness.once('pageRendered', function (page) {
try {
new Assertion(page.fragment).html(expected, options);
domRenderingOk = true;
}
catch (err) {
domRenderingOk = false;
if (!firstFailureMessage) {
firstFailureMessage = err.message;
actual = err.actual;
}
}
});
renderResult = harness.renderDom(options);
resetPageModel(renderResult.page);
// Try attaching. Attachment will throw an error if HTML doesn't match
var el = domDocument.createElement(parentTag);
el.innerHTML = htmlString;
var innerHTML = el.innerHTML;
var attachError;
try {
harness.attachTo(el);
}
catch (err) {
attachError = err;
if (!firstFailureMessage) {
firstFailureMessage = 'expected success attaching to #{exp} but got #{act}.\n' +
(attachError ? (attachError.message + attachError.stack) : '');
actual = innerHTML;
}
}
var attachOk = !attachError;
// TODO: Would be nice to add a diff of the expected and actual HTML
this.assert(htmlRenderingOk && domRenderingOk && attachOk, firstFailureMessage || 'rendering failed due to an unknown reason', 'expected rendering to fail but it succeeded', expected, actual);
});
/**
* Normalize a HTML string into its `innerHTML` form.
*
* WARNING - Only use this with trusted HTML, e.g. developer-provided HTML.
*
* Assigning into `element.innerHTML` does some interesting transformations:
*
* - Certain safe HTML entities like """ are converted into their unescaped
* single-character forms.
* - Certain single characters, e.g. ">" or a non-breaking space, are converted
* into their escaped HTML entity forms, e.g. ">" or " ".
*/
var normalizeHtml_1 = function (html) {
var normalizerElement = window.document.createElement('ins');
normalizerElement.innerHTML = html;
return normalizerElement.innerHTML;
};
}
return {
removeComments: removeComments,
getHtml: getHtml
};
}
exports.assertions = assertions;