mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			191 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * 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>"</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 overridden 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');
 | 
						|
 | 
						|
const mdiff = require('./mdiff.js');
 | 
						|
 | 
						|
// 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 = new JSDOM().window.document;
 | 
						|
    }
 | 
						|
 | 
						|
    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 _.zip(node1.content.childNodes, node2.content.childNodes).every(([child1, child2]) =>
 | 
						|
            child1.isEqualNode(child2)
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    _reorderAttributes(node) {
 | 
						|
        // Sorts every attribute in every element by name.  Ensures consistent diff HTML output
 | 
						|
 | 
						|
        const attributeList = [];
 | 
						|
 | 
						|
        for (const attr of node.attributes) {
 | 
						|
            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()) {
 | 
						|
            for (const childNode of node.children) {
 | 
						|
                this._reorderAttributes(childNode);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (node.content && node.content.hasChildNodes()) {
 | 
						|
            for (const childNode of node.content.children) {
 | 
						|
                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);
 | 
						|
 | 
						|
        message = message || '';
 | 
						|
        message += '\n';
 | 
						|
 | 
						|
        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);
 | 
						|
 | 
						|
        message = message || '';
 | 
						|
        message += '\n';
 | 
						|
 | 
						|
        if (comparison_results.are_equivalent) {
 | 
						|
            throw new assert.AssertionError({
 | 
						|
                message: message + [
 | 
						|
                    "actual and expected output produce semantially identical HTML",
 | 
						|
                    actual,
 | 
						|
                    "==",
 | 
						|
                    expected,
 | 
						|
                ].join('\n'),
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function returnComparer() {
 | 
						|
    if (!_markdownComparerInstance) {
 | 
						|
        _markdownComparerInstance = new MarkdownComparer((actual, expected) => {
 | 
						|
            return [
 | 
						|
                "Actual and expected output do not match.  Showing diff",
 | 
						|
                mdiff.diff_strings(actual, expected),
 | 
						|
            ].join('\n');
 | 
						|
        });
 | 
						|
    }
 | 
						|
    return _markdownComparerInstance;
 | 
						|
}
 | 
						|
 | 
						|
module.exports = {
 | 
						|
    equal(expected, actual, message) {
 | 
						|
        returnComparer().assertEqual(actual, expected, message);
 | 
						|
    },
 | 
						|
 | 
						|
    notEqual(expected, actual, message) {
 | 
						|
        returnComparer().assertNotEqual(actual, expected, message);
 | 
						|
    },
 | 
						|
 | 
						|
    setFormatter(output_formatter) {
 | 
						|
        returnComparer().setFormatter(output_formatter);
 | 
						|
    },
 | 
						|
};
 |