node tests: Compare markdown using semantic equivalence.

Fix #4367
This commit is contained in:
Andy Perez
2017-12-10 08:01:37 +00:00
committed by showell
parent 776af2e248
commit 695affd44e
4 changed files with 197 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
/*global Dict */
var path = zrequire('path', 'path');
var fs = zrequire('fs', 'fs');
zrequire('hash_util');
zrequire('katex', 'node_modules/katex/dist/katex.min.js');
zrequire('marked', 'third/marked/lib/marked');
@@ -172,18 +173,19 @@ var bugdown_data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../zerver
(function test_marked_shared() {
var tests = bugdown_data.regular_tests;
tests.forEach(function (test) {
var message = {raw_content: test.input};
markdown.apply_markdown(message);
var output = message.content;
if (test.marked_expected_output) {
assert.notEqual(test.expected_output, output);
assert.equal(test.marked_expected_output, output);
global.bugdown_assert.notEqual(test.expected_output, output);
global.bugdown_assert.equal(test.marked_expected_output, output);
} else if (test.backend_only_rendering) {
assert.equal(markdown.contains_backend_only_syntax(test.input), true);
} else {
assert.equal(test.expected_output, output);
global.bugdown_assert.equal(test.expected_output, output);
}
});
}());

View File

@@ -0,0 +1,179 @@
/**
* bugdown_assert.js
*
* Used to determine whether two Markdown HTML strings are semantically
* equivalent. Differs from the naive string-comparison approach in that
* differently typed but equivalent HTML fragments, such as '<p>&quot;</p>'
* and '<p>\"</p>', and '<span attr1="a" attr2="b"></span>' and
* '<span attr2="a" attr1="b"></span>', are still considered equal.
*
* The exported method equal() serves as a drop-in replacement for
* assert.equal(). Likewise, the exported method notEqual() replaces
* assert.notEqual().
*
* There is a default _output_formatter used to create the
* AssertionError error message; this function can be overriden using
* the exported setFormatter() function below.
*
* The HTML passed to the _output_formatter is not the original HTML, but
* rather a serialized version of a DOM element generated from the original
* HTML. This makes it easier to spot relevant differences.
*/
const jsdom = require('jsdom');
const _ = require('underscore');
// Module-level global instance of MarkdownComparer, initialized when needed
let _markdownComparerInstance = null;
class MarkdownComparer {
constructor(output_formatter) {
this._output_formatter = output_formatter || function (actual, expected) {
return ["Actual and expected output do not match.",
actual,
"!=",
expected,
].join('\n');
};
this._document = jsdom.jsdom();
}
setFormatter(output_formatter) {
this._output_formatter = output_formatter || this._output_formatter;
}
_htmlToElement(html, id) {
const template = this._document.createElement('template');
const id_node = this._document.createAttribute('id');
id_node.value = id;
template.setAttributeNode(id_node);
template.innerHTML = html;
return template;
}
_haveEqualContents(node1, node2) {
if (node1.content.childNodes.length !== node2.content.childNodes.length) {
return false;
}
return _.reduce(
_.zip(node1.content.childNodes, node2.content.childNodes),
(prev, nodePair) => { return prev && nodePair[0].isEqualNode(nodePair[1]); },
true
);
}
_reorderAttributes(node) {
// Sorts every attribute in every element by name. Ensures consistent diff HTML output
const attributeList = [];
_.forEach(node.attributes, (attr) => {
attributeList.push(attr);
});
// If put in above forEach loop, causes issues (possible nodes.attribute invalidation?)
attributeList.forEach((attr) => {node.removeAttribute(attr.name);});
attributeList.sort((a, b) => {
const name_a = a.name;
const name_b = b.name;
if (name_a < name_b) {
return -1;
} else if (name_a > name_b) {
return 1;
}
return 0;
});
// Put them back in, in order
attributeList.forEach((attribute) => {
node.setAttribute(attribute.name, attribute.value);
});
if (node.hasChildNodes()) {
_.forEach(node.children, (childNode) => {
this._reorderAttributes(childNode);
});
}
if (node.content && node.content.hasChildNodes()) {
_.forEach(node.content.children, (childNode) => {
this._reorderAttributes(childNode);
});
}
return node;
}
_compare(actual_markdown, expected_markdown) {
const ID_ACTUAL = "0";
const ID_EXPECTED = "1";
const element_actual = this._htmlToElement(actual_markdown, ID_ACTUAL);
const element_expected = this._htmlToElement(expected_markdown, ID_EXPECTED);
let are_equivalent = false;
let html = {};
are_equivalent = this._haveEqualContents(element_actual, element_expected);
if (!are_equivalent) {
html = {
actual : this._reorderAttributes(element_actual).innerHTML,
expected : this._reorderAttributes(element_expected).innerHTML,
};
}
element_actual.remove();
element_expected.remove();
return { are_equivalent, html };
}
assertEqual(actual, expected, message) {
const comparison_results = this._compare(actual, expected);
if (comparison_results.are_equivalent === false) {
throw new assert.AssertionError({
message : message || this._output_formatter(
comparison_results.html.actual,
comparison_results.html.expected
),
});
}
}
assertNotEqual(actual, expected, message) {
const comparison_results = this._compare(actual, expected);
if (comparison_results.are_equivalent) {
throw new assert.AssertionError({
message : message || [
"actual and expected output produce semantially identical HTML",
actual,
"==",
expected,
].join('\n'),
});
}
}
}
module.exports = {
equal(expected, actual, message) {
if (!_markdownComparerInstance) {
_markdownComparerInstance = new MarkdownComparer();
}
_markdownComparerInstance.assertEqual(actual, expected, message);
},
notEqual(expected, actual, message) {
if (!_markdownComparerInstance) {
_markdownComparerInstance = new MarkdownComparer();
}
_markdownComparerInstance.assertNotEqual(actual, expected, message);
},
setFormatter(output_formatter) {
if (!_markdownComparerInstance) {
_markdownComparerInstance = new MarkdownComparer();
}
_markdownComparerInstance.setFormatter(output_formatter);
},
};

View File

@@ -54,6 +54,9 @@ module.prototype.hot = {
accept: noop,
};
// Set up bugdown comparison helper
global.bugdown_assert = require('./bugdown_assert.js');
output.start_writing();
files.forEach(function (file) {

View File

@@ -19,6 +19,11 @@
"expected_output": "<p>Hamlet once said</p>\n<div class=\"codehilite\"><pre><span></span>def func():\n x = 1\n\n y = 2\n\n z = 3\n</pre></div>\n\n\n<p>And all was good.</p>",
"text_content": "Hamlet once said\ndef func():\n x = 1\n\n y = 2\n\n z = 3\n\n\n\nAnd all was good."
},
{
"name": "test",
"input": "it's lunch time",
"expected_output": "<p>it's lunch time</p>"
},
{
"name": "codeblock_trailing_whitespace",
"input": "Hamlet once said\n~~~~\ndef func():\n x = 1\n\n y = 2\t\t\n\n z = 3 \n~~~~\nAnd all was good.",
@@ -93,7 +98,6 @@
"name": "dangerous_block",
"input": "xxxxxx xxxxx xxxxxxxx xxxx. x xxxx xxxxxxxxxx:\n\n```\"xxxx xxxx\\xxxxx\\xxxxxx\"```\n\nxxx xxxx xxxxx:```xx.xxxxxxx(x'^xxxx$', xx.xxxxxxxxx)```\n\nxxxxxxx'x xxxx xxxxxxxxxx ```'xxxx'```, xxxxx xxxxxxxxx xxxxx ^ xxx $ xxxxxx xxxxx xxxxxxxxxxxx xxx xxxx xx x xxxx xx xxxx xx xxx xxxxx xxxxxx?",
"expected_output": "<p>xxxxxx xxxxx xxxxxxxx xxxx. x xxxx xxxxxxxxxx:</p>\n<p><code>\"xxxx xxxx\\xxxxx\\xxxxxx\"</code></p>\n<p>xxx xxxx xxxxx:<code>xx.xxxxxxx(x'^xxxx$', xx.xxxxxxxxx)</code></p>\n<p>xxxxxxx'x xxxx xxxxxxxxxx <code>'xxxx'</code>, xxxxx xxxxxxxxx xxxxx ^ xxx $ xxxxxx xxxxx xxxxxxxxxxxx xxx xxxx xx x xxxx xx xxxx xx xxx xxxxx xxxxxx?</p>",
"marked_expected_output": "<p>xxxxxx xxxxx xxxxxxxx xxxx. x xxxx xxxxxxxxxx:</p>\n<p><code>&quot;xxxx xxxx\\xxxxx\\xxxxxx&quot;</code></p>\n<p>xxx xxxx xxxxx:<code>xx.xxxxxxx(x&#39;^xxxx$&#39;, xx.xxxxxxxxx)</code></p>\n<p>xxxxxxx&#39;x xxxx xxxxxxxxxx <code>&#39;xxxx&#39;</code>, xxxxx xxxxxxxxx xxxxx ^ xxx $ xxxxxx xxxxx xxxxxxxxxxxx xxx xxxx xx x xxxx xx xxxx xx xxx xxxxx xxxxxx?</p>",
"text_content": "xxxxxx xxxxx xxxxxxxx xxxx. x xxxx xxxxxxxxxx:\n\"xxxx xxxx\\xxxxx\\xxxxxx\"\nxxx xxxx xxxxx:xx.xxxxxxx(x'^xxxx$', xx.xxxxxxxxx)\nxxxxxxx'x xxxx xxxxxxxxxx 'xxxx', xxxxx xxxxxxxxx xxxxx ^ xxx $ xxxxxx xxxxx xxxxxxxxxxxx xxx xxxx xx x xxxx xx xxxx xx xxx xxxxx xxxxxx?"
},
{
@@ -447,8 +451,7 @@
{
"name": "safe_html_messed_up_complexly_nested_script_tags",
"input": "<scr<script></script>ipt type=\"text/javascript\">alert(\"foo\");</<script></script>script<del></del>>",
"expected_output": "<p>&lt;scr&lt;script&gt;&lt;/script&gt;ipt type=\"text/javascript\"&gt;alert(\"foo\");&lt;/&lt;script&gt;&lt;/script&gt;script&lt;del&gt;&lt;/del&gt;&gt;</p>",
"marked_expected_output": "<p>&lt;scr&lt;script&gt;&lt;/script&gt;ipt type=&quot;text/javascript&quot;&gt;alert(&quot;foo&quot;);&lt;/&lt;script&gt;&lt;/script&gt;script&lt;del&gt;&lt;/del&gt;&gt;</p>"
"expected_output": "<p>&lt;scr&lt;script&gt;&lt;/script&gt;ipt type=\"text/javascript\"&gt;alert(\"foo\");&lt;/&lt;script&gt;&lt;/script&gt;script&lt;del&gt;&lt;/del&gt;&gt;</p>"
},
{
"name": "safe_html_unclosed_tag",
@@ -527,14 +530,12 @@
{
"name": "tex_inline",
"input": "$$1 \\oplus 0 = 1$$",
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mn>1</mn><mo>⊕</mo><mn>0</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding=\"application/x-tex\">1 \\oplus 0 = 1</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.72777em;vertical-align:-0.08333em;\"></span><span class=\"base\"><span class=\"mord mathrm\">1</span><span class=\"mbin\">⊕</span><span class=\"mord mathrm\">0</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">1</span></span></span></span></p>",
"marked_expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mn>1</mn><mo>⊕</mo><mn>0</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding=\"application/x-tex\">1 \\oplus 0 = 1</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.72777em;vertical-align:-0.08333em;\"></span><span class=\"base\"><span class=\"mord mathrm\">1</span><span class=\"mbin\">⊕</span><span class=\"mord mathrm\">0</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">1</span></span></span></span></p>"
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mn>1</mn><mo>⊕</mo><mn>0</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding=\"application/x-tex\">1 \\oplus 0 = 1</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.72777em;vertical-align:-0.08333em;\"></span><span class=\"base\"><span class=\"mord mathrm\">1</span><span class=\"mbin\">⊕</span><span class=\"mord mathrm\">0</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">1</span></span></span></span></p>"
},
{
"name": "tex_complex",
"input": "$$\\Phi_E = \\oint E \\cdot dA$$",
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><msub><mi mathvariant=\"normal\">Φ</mi><mi>E</mi></msub><mo>=</mo><mo>∮</mo><mi>E</mi><mo>⋅</mo><mi>d</mi><mi>A</mi></mrow><annotation encoding=\"application/x-tex\">\\Phi_E = \\oint E \\cdot dA</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.805em;\"></span><span class=\"strut bottom\" style=\"height:1.11112em;vertical-align:-0.30612em;\"></span><span class=\"base\"><span class=\"mord\"><span class=\"mord mathrm\">Φ</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.32833099999999993em;\"><span style=\"top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mathit mtight\" style=\"margin-right:0.05764em;\">E</span></span></span></span><span class=\"vlist-s\"></span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"></span></span></span></span></span><span class=\"mrel\">=</span><span class=\"mop op-symbol small-op\" style=\"margin-right:0.19445em;position:relative;top:-0.0005599999999999772em;\">∮</span><span class=\"mord mathit\" style=\"margin-right:0.05764em;\">E</span><span class=\"mbin\">⋅</span><span class=\"mord mathit\">d</span><span class=\"mord mathit\">A</span></span></span></span></p>",
"marked_expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><msub><mi mathvariant=\"normal\">Φ</mi><mi>E</mi></msub><mo>=</mo><mo>∮</mo><mi>E</mi><mo>⋅</mo><mi>d</mi><mi>A</mi></mrow><annotation encoding=\"application/x-tex\">\\Phi_E = \\oint E \\cdot dA</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"strut\" style=\"height:0.805em;\"></span><span class=\"strut bottom\" style=\"height:1.11112em;vertical-align:-0.30612em;\"></span><span class=\"base\"><span class=\"mord\"><span class=\"mord mathrm\">Φ</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.32833099999999993em;\"><span style=\"top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mathit mtight\" style=\"margin-right:0.05764em;\">E</span></span></span></span><span class=\"vlist-s\"></span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"></span></span></span></span></span><span class=\"mrel\">=</span><span class=\"mop op-symbol small-op\" style=\"margin-right:0.19445em;position:relative;top:-0.0005599999999999772em;\">∮</span><span class=\"mord mathit\" style=\"margin-right:0.05764em;\">E</span><span class=\"mbin\">⋅</span><span class=\"mord mathit\">d</span><span class=\"mord mathit\">A</span></span></span></span></p>"
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><msub><mi mathvariant=\"normal\">Φ</mi><mi>E</mi></msub><mo>=</mo><mo>∮</mo><mi>E</mi><mo>⋅</mo><mi>d</mi><mi>A</mi></mrow><annotation encoding=\"application/x-tex\">\\Phi_E = \\oint E \\cdot dA</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.805em;\"></span><span class=\"strut bottom\" style=\"height:1.11112em;vertical-align:-0.30612em;\"></span><span class=\"base\"><span class=\"mord\"><span class=\"mord mathrm\">Φ</span><span class=\"msupsub\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.32833099999999993em;\"><span style=\"top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mathit mtight\" style=\"margin-right:0.05764em;\">E</span></span></span></span><span class=\"vlist-s\"></span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.15em;\"></span></span></span></span></span><span class=\"mrel\">=</span><span class=\"mop op-symbol small-op\" style=\"margin-right:0.19445em;position:relative;top:-0.0005599999999999772em;\">∮</span><span class=\"mord mathit\" style=\"margin-right:0.05764em;\">E</span><span class=\"mbin\">⋅</span><span class=\"mord mathit\">d</span><span class=\"mord mathit\">A</span></span></span></span></p>"
},
{
"name": "tex_escaped",
@@ -562,14 +563,12 @@
{
"name": "tex_money",
"input": "Tickets are $5 to $20 for youth, $10-$30 for adults, so we are hoping to bring in $500 from the event ($$x \\approx 500\\$$$)",
"expected_output": "<p>Tickets are $5 to $20 for youth, $10-$30 for adults, so we are hoping to bring in $500 from the event (<span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>≈</mo><mn>5</mn><mn>0</mn><mn>0</mn><mi mathvariant=\"normal\">$</mi></mrow><annotation encoding=\"application/x-tex\">x \\approx 500\\$</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.75em;\"></span><span class=\"strut bottom\" style=\"height:0.80556em;vertical-align:-0.05556em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">≈</span><span class=\"mord mathrm\">5</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">$</span></span></span></span>)</p>",
"marked_expected_output": "<p>Tickets are $5 to $20 for youth, $10-$30 for adults, so we are hoping to bring in $500 from the event (<span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>≈</mo><mn>5</mn><mn>0</mn><mn>0</mn><mi mathvariant=\"normal\">$</mi></mrow><annotation encoding=\"application/x-tex\">x \\approx 500\\$</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"strut\" style=\"height:0.75em;\"></span><span class=\"strut bottom\" style=\"height:0.80556em;vertical-align:-0.05556em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">≈</span><span class=\"mord mathrm\">5</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">$</span></span></span></span>)</p>"
"expected_output": "<p>Tickets are $5 to $20 for youth, $10-$30 for adults, so we are hoping to bring in $500 from the event (<span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>≈</mo><mn>5</mn><mn>0</mn><mn>0</mn><mi mathvariant=\"normal\">$</mi></mrow><annotation encoding=\"application/x-tex\">x \\approx 500\\$</annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.75em;\"></span><span class=\"strut bottom\" style=\"height:0.80556em;vertical-align:-0.05556em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">≈</span><span class=\"mord mathrm\">5</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">0</span><span class=\"mord mathrm\">$</span></span></span></span>)</p>"
},
{
"name": "tex_inline_permissive_spacing",
"input": "$$ x = 7 $$",
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>7</mn></mrow><annotation encoding=\"application/x-tex\"> x = 7 </annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.64444em;vertical-align:0em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">7</span></span></span></span></p>",
"marked_expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>7</mn></mrow><annotation encoding=\"application/x-tex\"> x = 7 </annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.64444em;vertical-align:0em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">7</span></span></span></span></p>"
"expected_output": "<p><span class=\"katex\"><span class=\"katex-mathml\"><math><semantics><mrow><mi>x</mi><mo>=</mo><mn>7</mn></mrow><annotation encoding=\"application/x-tex\"> x = 7 </annotation></semantics></math></span><span aria-hidden=\"true\" class=\"katex-html\"><span class=\"strut\" style=\"height:0.64444em;\"></span><span class=\"strut bottom\" style=\"height:0.64444em;vertical-align:0em;\"></span><span class=\"base\"><span class=\"mord mathit\">x</span><span class=\"mrel\">=</span><span class=\"mord mathrm\">7</span></span></span></span></p>"
},
{
"name": "tex_inline_prohibited_newline",