Skip to content

Commit

Permalink
Merge pull request #581 from derbyjs/test-util-improvements
Browse files Browse the repository at this point in the history
Improve test-utils assertions
  • Loading branch information
ericyhwang authored Apr 21, 2020
2 parents 57d2863 + 191f8ff commit 1e7570c
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 18 deletions.
16 changes: 16 additions & 0 deletions test-utils/ComponentHarness.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var EventEmitter = require('events').EventEmitter;
var qs = require('qs');
var urlParse = require('url').parse;
var Model = require('racer').Model;
var racerUtil = require('racer/lib/util');
var App = require('../lib/App');
var AppForServer = require('../lib/AppForServer');

Expand Down Expand Up @@ -35,6 +37,7 @@ module.exports = ComponentHarness;
* If arguments are provided, then `#setup` is called with the arguments.
*/
function ComponentHarness() {
EventEmitter.call(this);
this.app = new AppForHarness(this);
this.model = new Model();

Expand All @@ -43,6 +46,8 @@ function ComponentHarness() {
}
}

racerUtil.mergeInto(ComponentHarness.prototype, EventEmitter.prototype);

/** @typedef { {view: {is: string, source?: string}} } InlineComponent */
/**
* Sets up the harness with a HTML template, which should contain a `<view is="..."/>` for the
Expand Down Expand Up @@ -172,7 +177,18 @@ ComponentHarness.prototype._get = function(render, options) {
// pulls URL info from the model or page.
this.app.history = { push: setPageUrl, replace: setPageUrl };

// The `#render` assertion in assertions.js wants to compare the results of HTML and DOM
// rendering, to make sure they match. However, component `create()` methods can modify the DOM
// immediately after initial rendering, which can break assertions.
//
// To get around this, we trigger a "pageRendered" event on the harness before `create()` methods
// get called. This is done by pausing the context, which prevents create() methods from getting
// called until the pause-count drops to 0.
page.context.pause();
render(page);
this.emit('pageRendered', page);
page.context.unpause();

// HACK: Implement getting an instance as a side-effect of rendering. This
// code relies on the fact that while rendering, components are instantiated,
// and a reference is kept on page._components. Since we just created the
Expand Down
96 changes: 78 additions & 18 deletions test-utils/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ module.exports = function(dom, Assertion) {

this.assert(
html === expected,
'expected the HTML #{exp} but got #{act}',
'expected to not have HTML #{act}',
'expected DOM rendering to produce the HTML #{exp} but got #{act}',
'expected DOM rendering to not produce actual HTML #{act}',
expected,
html
);
Expand All @@ -71,42 +71,102 @@ module.exports = function(dom, Assertion) {
}
var domDocument = getWindow().document;
var parentTag = (options && options.parentTag) || 'ins';
var firstFailureMessage, actual;

new Assertion(harness).instanceOf(ComponentHarness);

// Check HTML matches expected value
var html = harness.renderHtml(options).html;
// Use the HTML as the expected value if null. This allows the user to
// test that all modes of rendering will be equivalent
if (expected == null) expected = html;
new Assertion(expected).is.a('string');
new Assertion(html).equal(expected);
// Render to a HTML string.
var htmlString = harness.renderHtml(options).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 '&nbsp;' 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(htmlString);

// Check DOM rendering is also equivalent
var fragment = harness.renderDom(options).fragment;
new Assertion(fragment).html(expected, options);
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;
}
}
}

// 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;
}
}
});
harness.renderDom(options);

// Try attaching. Attachment will throw an error if HTML doesn't match
var el = domDocument.createElement(parentTag);
el.innerHTML = html;
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(
!attachError,
'expected success attaching to #{exp} but got #{act}.\n' +
(attachError ? (attachError.message + attachError.stack) : ''),
'expected render to fail but matched #{exp}',
htmlRenderingOk && domRenderingOk && attachOk,
firstFailureMessage || 'rendering failed due to an unknown reason',
'expected rendering to fail but it succeeded',
expected,
innerHTML
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 "&quot;" 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. "&gt;" or "&nbsp;".
*/
var normalizeHtml = function(html) {
var normalizerElement = window.document.createElement('ins');
normalizerElement.innerHTML = html;
return normalizerElement.innerHTML;
};
}

return {
Expand Down
19 changes: 19 additions & 0 deletions test/dom/ComponentHarness.mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ describe('ComponentHarness', function() {
var harness = runner.createHarness('<view is="box" />', Box);
expect(harness).to.render('');
});

it('ignores DOM mutations in components\' create()', function() {
function Box() {}
Box.view = {
is: 'box',
source: '<index:><div class="box" as="boxElement"></div>'
};
Box.prototype.create = function() {
this.boxElement.className = 'box-changed-in-create';
};
var harness = runner.createHarness('<view is="box" />', Box);
expect(harness).to.render('<div class="box"></div>');
});

it('works with HTML entities like &nbsp;', function() {
var harness = runner.createHarness('&lt;&nbsp;&quot;&gt;');
expect(harness).to.render();
expect(harness).to.render('&lt;&nbsp;"&gt;');
});
});

describe('fake app.history implementation', function() {
Expand Down

0 comments on commit 1e7570c

Please sign in to comment.