├── .gitignore
├── samples
├── actual.xml
└── expected.xml
├── lib
├── normalize_newlines.js
├── collapse_spaces.js
├── node_types.js
├── reporters
│ └── groupingReporter.js
├── revxpath.js
├── canonizer.js
├── compare.js
└── collector.js
├── .travis.yml
├── index.js
├── CHANGELOG.md
├── package.json
├── LICENSE
├── test
├── test-reporter.js
├── test-normalize_newlines.js
├── test-revxpath.js
├── test-canonize.js
├── test-collector.js
└── test-compare.js
├── bin
└── domcompare
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | lcov.info
3 | *.seed
4 | *.log
5 | *.csv
6 | *.dat
7 | *.out
8 | *.pid
9 | *.gz
10 |
11 | pids
12 | logs
13 | results
14 |
15 | .idea
16 | node_modules
17 | npm-debug.log
18 |
--------------------------------------------------------------------------------
/samples/actual.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | text content
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/samples/expected.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | text content
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/normalize_newlines.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | module.exports = function normalizeNewlines(str) {
6 | // First replace all CR+LF newlines then replace CR newlines
7 | return str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
8 | };
9 |
10 | })();
11 |
--------------------------------------------------------------------------------
/lib/collapse_spaces.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | module.exports = function collapseSpaces(str) {
6 | // Replace all whitespace with the space character and then collapse all white space to one space character
7 | return str.replace(/\s/g, ' ').replace(/\s\s+/g, ' ');
8 | };
9 |
10 | })();
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "12"
5 | - "11"
6 | - "10"
7 | - "9"
8 | - "8"
9 | - "7"
10 | - "6"
11 | - "5.1"
12 | - "5.0"
13 | - "4.2"
14 | - "4.1"
15 | - "4.0"
16 |
17 | script: npm test && npm run-script test-cov && (cat lcov.info | ./node_modules/.bin/coveralls) && (rm -rf lib-cov lcov.info)
18 |
--------------------------------------------------------------------------------
/lib/node_types.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | module.exports = {
6 | ELEMENT_NODE: 1,
7 | ATTRIBUTE_NODE: 2,
8 | TEXT_NODE: 3,
9 | CDATA_SECTION_NODE: 4,
10 | ENTITY_REFERENCE_NODE: 5,
11 | ENTITY_NODE: 6,
12 | PROCESSING_INSTRUCTION_NODE: 7,
13 | COMMENT_NODE: 8,
14 | DOCUMENT_NODE: 9,
15 | DOCUMENT_TYPE_NODE: 10,
16 | DOCUMENT_FRAGMENT_NODE: 11,
17 | NOTATION_NODE: 12
18 | };
19 |
20 | })();
--------------------------------------------------------------------------------
/lib/reporters/groupingReporter.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | "use strict";
4 |
5 | module.exports = {
6 | report: function(res) {
7 | var _res = this.getDifferences(res);
8 | return Object.keys(_res).map(function(path){
9 | return [path, "\n\t", _res[path].join('\n\t')].join('');
10 | }.bind(this)).join('\n');
11 | },
12 | getDifferences: function(res) {
13 | var _res = {};
14 | res.getDifferences().forEach(function(f){
15 | (_res[f.node] = (_res[f.node] || [])).push(f.message);
16 | }.bind(this));
17 | return _res;
18 | }
19 | };
20 |
21 | })();
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | var libPrefix = process.env.COVER ? './lib-cov' : './lib';
6 |
7 | module.exports = process.browser ? {
8 | compare: require('./lib/compare'),
9 | XMLSerializer: require('./lib/canonizer'),
10 | revXPath: require('./lib/revxpath'),
11 | GroupingReporter: require('./lib/reporters/groupingReporter.js')
12 | } :
13 | {
14 | compare: require(libPrefix + '/compare'),
15 | XMLSerializer: require(libPrefix + '/canonizer'),
16 | revXPath: require(libPrefix + '/revxpath'),
17 | GroupingReporter: require(libPrefix + '/reporters/groupingReporter.js')
18 | };
19 |
20 | })();
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v0.6.0, 14.10.2019
4 |
5 | * Fixed [#43](https://github.com/Olegas/dom-compare/issues/43). Thanks to @Sh33pman
6 |
7 | ## v0.5.0, 21.09.2019
8 |
9 | * Added `normalizeNewlines` option. See docs for details. Thanks to @jordandh
10 |
11 | ## v0.4.0, 05.09.2019
12 |
13 | * Added `collapseSpaces` option. See docs for details. Thanks to @jordandh
14 |
15 | ## v0.3.1, 01.05.2017
16 |
17 | * Added support for document fragments. Closing #26
18 |
19 | ## v0.3.0, 27.04.2017
20 |
21 | * **BREAKING CHANGES**. Fixed a typo in groupingReporter.getDifferences method. Thanks to @jonathanp
22 |
23 | ## v0.2.1, 25.04.2015
24 |
25 | * Browserify support
26 | * Fixed dependencies versions to make all test pass
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dom-compare",
3 | "description": "Library to compare two DOM trees",
4 | "version": "0.6.0",
5 | "author": "Oleg Elifantiev ",
6 | "contributors": [],
7 | "keywords": [
8 | "dom",
9 | "comparison"
10 | ],
11 | "bin": {
12 | "domcompare": "bin/domcompare"
13 | },
14 | "dependencies": {
15 | "argparse": "^1.0.10",
16 | "colors": "0.6.2",
17 | "xmldom": "0.1.19"
18 | },
19 | "devDependencies": {
20 | "coveralls": "^3.0.2",
21 | "istanbul": "^0.4.5",
22 | "mocha": "^5.2.0",
23 | "mocha-istanbul": "0.2.0"
24 | },
25 | "scripts": {
26 | "instrument": "istanbul instrument --output lib-cov --no-compact --variable global.__coverage__ lib",
27 | "test-cov": "npm run-script instrument && COVER=1 ISTANBUL_REPORTERS=lcovonly mocha -R mocha-istanbul",
28 | "test": "mocha -R spec"
29 | },
30 | "repository": "git@github.com:Olegas/dom-compare.git",
31 | "engines": {
32 | "node": "*"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Oleg Elifantiev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/test-reporter.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 | var xmldom = require("xmldom");
3 | var parser = new xmldom.DOMParser();
4 | var domCompare = require("../");
5 | var compare = domCompare.compare;
6 | var reporter = domCompare.GroupingReporter;
7 |
8 | describe("Differences reporting", function(){
9 |
10 | describe("Grouping reporter", function(){
11 |
12 | it("grouping differences by source node XPath", function(){
13 |
14 | var doc = parser.parseFromString("");
15 | var doc2 = parser.parseFromString("");
16 |
17 | var failures = reporter.getDifferences(compare(doc, doc2));
18 |
19 | assert.equal(3, Object.keys(failures).length);
20 | assert.equal(3, failures['/doc'].length);
21 | assert.equal(2, failures['/doc/node1'].length);
22 | assert.equal(1, failures['/doc/node2'].length);
23 |
24 | // check for results doplication
25 | failures = reporter.getDifferences(compare(doc, doc2));
26 |
27 | assert.equal(3, Object.keys(failures).length);
28 | assert.equal(3, failures['/doc'].length);
29 | assert.equal(2, failures['/doc/node1'].length);
30 | assert.equal(1, failures['/doc/node2'].length);
31 |
32 | });
33 |
34 | });
35 |
36 | });
--------------------------------------------------------------------------------
/test/test-normalize_newlines.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var normalizeNewlines = require('../lib/normalize_newlines');
3 |
4 | describe('normalize_newlines', function () {
5 | it('should transform CR+LF to LF', function() {
6 | var input = '\r\n';
7 | var expected = '\n';
8 | assert.equal(normalizeNewlines(input), expected);
9 | });
10 |
11 | it('should transform CR to LF', function() {
12 | var input = '\r';
13 | var expected = '\n';
14 | assert.equal(normalizeNewlines(input), expected);
15 | });
16 |
17 | it('should transform mixed CR+LF and LF to LF', function() {
18 | var input = '\n\r\n\n\r\n\n';
19 | var expected = '\n\n\n\n\n';
20 | assert.equal(normalizeNewlines(input), expected);
21 | });
22 |
23 | it('should transform mixed CR+LF and CR to LF', function() {
24 | var input = '\r\n\r\n\r \r\n\r';
25 | var expected = '\n\n\n \n\n';
26 | assert.equal(normalizeNewlines(input), expected);
27 | });
28 |
29 | it('should transform mixed CR and LF to LF', function() {
30 | var input = '\n\n\r \n\r';
31 | var expected = '\n\n\n \n\n';
32 | assert.equal(normalizeNewlines(input), expected);
33 | });
34 |
35 | it('should transform mixed CR+LF and CR and LF to LF', function() {
36 | var input = '\n\r\n\r\n\n\r \n\r\n\r';
37 | var expected = '\n\n\n\n\n \n\n\n';
38 | assert.equal(normalizeNewlines(input), expected);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/lib/revxpath.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | var type = require('./node_types');
6 |
7 | function _describeNode(node) {
8 | var parent = node.parentNode,
9 | myName = node.nodeName,
10 | sameSiblings,
11 | i, l;
12 |
13 | // Find all siblings that have the same name.
14 | if (parent && parent.childNodes && parent.childNodes.length) {
15 | sameSiblings = [];
16 | for(i = 0, l = parent.childNodes.length; i < l; i++) {
17 | if (parent.childNodes[i].nodeName === myName) {
18 | sameSiblings.push(parent.childNodes[i]);
19 | }
20 | }
21 | }
22 |
23 | if(node.nodeType == type.DOCUMENT_NODE)
24 | return "";
25 |
26 | if(sameSiblings && sameSiblings.length > 1) {
27 | if(node.hasAttribute('id'))
28 | return myName + "[@id='" + node.getAttribute('id') + "']";
29 | for(i = 0, l = sameSiblings.length; i < l; i++) {
30 | if(sameSiblings[i] == node)
31 | return myName + "[" + (i + 1) + "]";
32 | }
33 | throw new Error("Node is not found, but should be!");
34 | } else
35 | return myName;
36 | }
37 |
38 | function _processNode(node, res) {
39 |
40 | res.unshift(_describeNode(node));
41 | if(node.parentNode)
42 | _processNode(node.parentNode, res);
43 |
44 | }
45 |
46 | module.exports = function revXPath(node) {
47 |
48 | var res;
49 |
50 | if(!node)
51 | return "";
52 |
53 | if(node.nodeType == type.DOCUMENT_NODE)
54 | return "/";
55 |
56 | _processNode(node, res = []);
57 | return res.join('/');
58 |
59 | };
60 |
61 |
62 | })();
--------------------------------------------------------------------------------
/test/test-revxpath.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 | var xmldom = require("xmldom");
3 | var parser = new xmldom.DOMParser();
4 | var domCompare = require("../");
5 | var revxpath = domCompare.revXPath;
6 |
7 | describe("Reverse XPath", function(){
8 |
9 | it("Builds a node path by its name", function(){
10 |
11 | var doc = parser.parseFromString(" ");
12 |
13 | var path = revxpath(doc.getElementsByTagName('item')[0]);
14 |
15 | assert.equal("/doc/child/item", path);
16 |
17 | path = revxpath(doc.documentElement);
18 |
19 | assert.equal("/doc", path);
20 |
21 | path = revxpath(doc);
22 |
23 | assert.equal("/", path);
24 |
25 | });
26 |
27 | describe("Multiple same named siblings", function(){
28 |
29 | it("number is added to a node name", function(){
30 |
31 | var doc = parser.parseFromString(" ");
32 |
33 | var path = revxpath(doc.getElementsByTagName('item')[0]);
34 |
35 | assert.equal("/doc/child/item[1]", path);
36 |
37 | path = revxpath(doc.getElementsByTagName('item')[1]);
38 |
39 | assert.equal("/doc/child/item[2]", path);
40 |
41 | path = revxpath(doc.getElementsByTagName('inner')[0]);
42 |
43 | assert.equal("/doc/child/item[2]/inner", path);
44 |
45 | });
46 |
47 | it("number is added correctly with repeating node names on multiple levels", function() {
48 |
49 | var doc = parser.parseFromString("");
50 |
51 | // Select Some Text Part 2
and get the xpath
52 | var path = revxpath(doc.getElementsByTagName('div')[9]);
53 |
54 | assert.equal("/html/body/div[2]/section[2]/div", path);
55 | });
56 |
57 | it("if ID attribute is present, it is used instead of number", function(){
58 |
59 | var doc = parser.parseFromString(" ");
60 |
61 | var path = revxpath(doc.getElementById('x'));
62 |
63 | assert.equal("/doc/child/item[@id='x']", path);
64 |
65 | path = revxpath(doc.getElementById('y'));
66 |
67 | assert.equal("/doc/child/item[@id='y']", path);
68 |
69 | });
70 | });
71 |
72 | });
--------------------------------------------------------------------------------
/test/test-canonize.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 | var xmldom = require("xmldom");
3 | var parser = new xmldom.DOMParser();
4 | var domCompare = require('../');
5 | var canonize = (new (domCompare.XMLSerializer)()).serializeToString;
6 |
7 | describe("Canonize", function(){
8 | it("can make a canonical form of a document", function(){
9 | var doc = parser.parseFromString("" +
10 | "");
11 | var docCanon =
12 | "\n" +
16 | " \n" +
20 | " \n" +
21 | " \n" +
22 | " \n" +
23 | " \n" +
24 | " \n" +
25 | " \n" +
26 | " \n" +
27 | " \n" +
28 | " \n" +
29 | " \n" +
30 | "";
31 | assert.equal(docCanon, canonize(doc));
32 | });
33 |
34 | it("Default indent - 2 spaces for attributes and 4 spaces for nested elements", function(){
35 | var doc = parser.parseFromString("text");
36 | var docCanon =
37 | "\n" +
39 | " \n" +
40 | " text\n" +
41 | " \n" +
42 | "";
43 | assert.equal(docCanon, canonize(doc));
44 | });
45 |
46 | it("Empty tags serialized in short form", function(){
47 | var doc = parser.parseFromString("");
48 | var docCanon = "";
49 | assert.equal(docCanon, canonize(doc));
50 |
51 | doc = parser.parseFromString("");
52 | docCanon =
53 | "";
55 | assert.equal(docCanon, canonize(doc));
56 | });
57 |
58 | it("Any leading/triling whitespace in text and comment nodes is trimmed", function(){
59 | var doc = parser.parseFromString(" test aaa ");
60 | var docCanon =
61 | "\n" +
62 | " test\n" +
63 | " \n" +
64 | " aaa\n" +
65 | " \n" +
66 | " \n" +
67 | "";
68 | assert.equal(docCanon, canonize(doc));
69 | });
70 |
71 | it("Attribute values serialized in double quotes", function(){
72 | var doc = parser.parseFromString("");
73 | var docCanon =
74 | "\n" +
75 | " \n" +
78 | "";
79 | assert.equal(docCanon, canonize(doc));
80 | });
81 |
82 | });
--------------------------------------------------------------------------------
/bin/domcompare:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | var domcompare = require('../'),
6 | colors = require('colors'),
7 | compare = domcompare.compare,
8 | reporter = domcompare.GroupingReporter,
9 | fs = require('fs'),
10 | ArgumentParser = require('argparse').ArgumentParser,
11 | version = require('../package.json').version,
12 | xmldom = require('xmldom'),
13 | path = require('path'),
14 | domparser = new (xmldom.DOMParser)(),
15 | args, dom1, dom2, f1ext, f2ext, mimeMap, result, argparser;
16 |
17 | var ERR_BAD_FILES = 254,
18 | ERR_FILES_DIFFER = 1,
19 | ERR_OK = 0;
20 |
21 | mimeMap = {
22 | '.html' : 'text/html',
23 | '.xml' : 'text/xml'
24 | }
25 |
26 | argparser = new ArgumentParser({
27 | version: version,
28 | addHelp: true,
29 | description: 'domcompare - simple DOM comparison utility'
30 | });
31 |
32 | argparser.addArgument([ '-s', '--stripspaces' ], {
33 | defaultValue: false,
34 | dest: 'stripSpaces',
35 | action: 'storeTrue',
36 | help: "Strip spaces when comparing strings (exclude CDATA nodes)"
37 | });
38 |
39 | argparser.addArgument([ '-l', '--collapsespaces' ], {
40 | defaultValue: false,
41 | dest: 'collapseSpaces',
42 | action: 'storeTrue',
43 | help: "Collapse spaces when comparing strings (exclude CDATA nodes)"
44 | });
45 |
46 | argparser.addArgument([ '-n', '--normalizenewlines' ], {
47 | defaultValue: false,
48 | dest: 'normalizeNewlines',
49 | action: 'storeTrue',
50 | help: "Normalize newlines when comparing strings (include CDATA nodes)"
51 | });
52 |
53 | argparser.addArgument([ '-c', '--comments' ], {
54 | defaultValue: false,
55 | dest: 'compareComments',
56 | action: 'storeTrue',
57 | help: "Compare comments"
58 | });
59 |
60 | argparser.addArgument([ '-q', '--quiet' ], {
61 | defaultValue: false,
62 | dest: 'quiet',
63 | action: 'storeTrue',
64 | help: "Be quiet, don't output anything"
65 | });
66 |
67 | argparser.addArgument([ 'FILE1' ], {
68 | help: 'Reference file'
69 | });
70 |
71 | argparser.addArgument([ 'FILE2' ], {
72 | help: 'File to compare'
73 | });
74 |
75 |
76 |
77 | args = argparser.parseArgs();
78 | args.FILE1 = path.resolve(args.FILE1);
79 | args.FILE2 = path.resolve(args.FILE2);
80 |
81 | function getMime(f) {
82 | return mimeMap[path.extname(f)] || 'text/xml';
83 | }
84 |
85 | function log(s) {
86 | if(!args.quiet) {
87 | console.log(s);
88 | }
89 | }
90 |
91 | if(fs.existsSync(args.FILE1) && fs.existsSync(args.FILE2)) {
92 | dom1 = domparser.parseFromString(fs.readFileSync(args.FILE1, 'utf8'), getMime(args.FILE1));
93 | dom2 = domparser.parseFromString(fs.readFileSync(args.FILE2, 'utf8'), getMime(args.FILE2));
94 |
95 | result = compare(dom1, dom2, args);
96 |
97 | if(result.getResult()) {
98 | log('Documents are equal'.green);
99 | process.exit(ERR_OK);
100 | } else {
101 | log('Documents are not equal'.red);
102 | log(reporter.report(result));
103 | process.exit(ERR_FILES_DIFFER);
104 | }
105 |
106 | } else {
107 | log('Error'.red);
108 | process.exit(ERR_BAD_FILES);
109 | }
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/lib/canonizer.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | var c = require('./node_types'), spaces = ' ';
6 |
7 |
8 | function _sortAttributes(a, b) {
9 | return a.nodeName < b.nodeName ? -1 : 1;
10 | }
11 |
12 | function _canonizeNode(node, indent) {
13 | var i = new Array(indent + 1).join(spaces), hasChildren = node.childNodes.length > 0, ret;
14 | ret = i + "<" + node.nodeName +
15 | _canonizeAttributes(node.attributes, indent + 1);
16 | if(hasChildren) {
17 | ret += ">" + _canonizeNodeList(node.childNodes, indent + 2) +
18 | "\n" + i + "" + node.nodeName + ">";
19 | } else
20 | ret += " />";
21 | return ret;
22 | }
23 |
24 | function _canonizeAttributes(attrs, indent) {
25 |
26 | var aList = [], ret = "", i = new Array(indent + 1);
27 | if (attrs && attrs.length) {
28 |
29 | Object.keys(attrs).map(function (i) {
30 | return parseInt(i, 10);
31 | }).filter(function (i) {
32 | return typeof(i) == 'number' && i >= 0;
33 | }).sort(function (a, b) {
34 | return a < b ? -1 : 1;
35 | }).forEach(function (k) {
36 | aList.push(attrs[k]);
37 | });
38 | aList.sort(_sortAttributes);
39 | aList.forEach(function(a){
40 | ret += "\n" + i.join(spaces) + a.nodeName + "=\"" + (a.nodeValue + "").replace(/"/g, '"') + "\"";
41 | });
42 | }
43 | return ret;
44 | }
45 |
46 | function _canonizeNodeList(list, indent) {
47 | var ret = '',
48 | i, l;
49 | if(list){
50 | l = list.length;
51 | for(i=0; i < l; i++) {
52 | ret += "\n" + canonize(list.item(i), indent);
53 | }
54 | }
55 | return ret;
56 | }
57 |
58 | function _canonizeText(nodeType, text, indent) {
59 | var ret = [], i = new Array(indent + 1).join(spaces);
60 | switch (nodeType) {
61 | case c.CDATA_SECTION_NODE:
62 | ret[0] = "";
64 | break;
65 | case c.COMMENT_NODE:
66 | ret[0] = "";
68 | break;
69 | case c.TEXT_NODE:
70 | break;
71 | }
72 | if(nodeType !== c.CDATA_SECTION_NODE)
73 | text = text.trim();
74 | ret[1] = text;
75 | return i + ret.join('');
76 | }
77 |
78 | function canonize(node, indent) {
79 | indent = indent || 0;
80 | switch (node.nodeType) {
81 | case c.DOCUMENT_NODE:
82 | return canonize(node.documentElement, indent);
83 | case c.ELEMENT_NODE:
84 | return _canonizeNode(node, indent);
85 | case c.CDATA_SECTION_NODE:
86 | // fallthrough
87 | case c.COMMENT_NODE:
88 | // fallthrough
89 | case c.TEXT_NODE:
90 | return _canonizeText(node.nodeType, node.nodeValue, indent);
91 | default:
92 | throw Error("Node type " + node.nodeType + " serialization is not implemented");
93 | }
94 |
95 | }
96 |
97 | function XMLSerializer() {}
98 |
99 | XMLSerializer.prototype.serializeToString = function(doc) {
100 | return canonize(doc);
101 | };
102 |
103 | module.exports = XMLSerializer;
104 |
105 | })();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | dom-compare
2 | ===========
3 |
4 | [](https://travis-ci.org/Olegas/dom-compare)
5 | [](https://coveralls.io/r/Olegas/dom-compare)
6 | [](http://badge.fury.io/js/dom-compare)
7 |
8 | NodeJS module to compare two DOM-trees
9 |
10 | * [DOM Comparison](#dom-comparison)
11 | * [Comparison options](#comparison-options)
12 | * [Comments comparison](#comments-comparison)
13 | * [Whitespace comparison](#whitespace-comparison)
14 | * [Cli utility](#cli-utility)
15 | * [DOM Canonic Form](#dom-canonic-form)
16 |
17 | Works with Node.JS v0.10+
18 |
19 | DOM Comparison
20 | --------------
21 |
22 | Consider two documents. Expected:
23 | ```xml
24 |
25 |
26 |
27 | text content
28 |
29 |
30 |
31 |
32 |
33 |
34 | ```
35 | and actual one:
36 | ```xml
37 |
38 |
39 | text content
40 |
41 |
42 |
43 |
44 |
45 | ```
46 |
47 | One can compare them, get the result (is them equals, or not), and get extended report (why them are different).
48 |
49 | ```javascript
50 | var compare = require('dom-compare').compare,
51 | reporter = require('dom-compare').GroupingReporter,
52 | expected = ..., // expected DOM tree
53 | actual = ..., // actual one
54 | result, diff, groupedDiff;
55 |
56 | // compare to DOM trees, get a result object
57 | result = compare(expected, actual);
58 |
59 | // get comparison result
60 | console.log(result.getResult()); // false cause' trees are different
61 |
62 | // get all differences
63 | diff = result.getDifferences(); // array of diff-objects
64 |
65 | // differences, grouped by node XPath
66 | groupedDiff = reporter.getDifferences(result); // object, key - node XPATH, value - array of differences (strings)
67 |
68 | // string representation
69 | console.log(reporter.report(result));
70 | ```
71 |
72 | Diff-object has a following form:
73 |
74 | ```javascript
75 | {
76 | node: "/document/element",
77 | message: "Attribute 'attribute': expected value '10' instead of '100'";
78 | }
79 | ```
80 |
81 | By using `GroupingReporter` one can get a result of a following type
82 |
83 | ```javascript
84 | {
85 | '/document/element': [
86 | "Attribute 'attribute': expected value '10' instead of '100'",
87 | "Extra attribute 'attributeX'"
88 | ]
89 | }
90 | ```
91 |
92 | ### Comparison options
93 |
94 | Comparison function can take a third argument with options like this:
95 | ```javascript
96 | var options = {
97 | stripSpaces: true,
98 | compareComments: true,
99 | collapseSpaces: true,
100 | normalizeNewlines: true
101 | };
102 |
103 | result = compare(expected, actual, options);
104 | ```
105 | #### Comments comparison
106 | By default, all comments are ignored. Set `compareComments` options to `true` to compare them too.
107 |
108 |
109 | #### Whitespace comparison
110 | By default, all text nodes (text, CDATA, comments if enabled as mentioned above) compared with respect
111 | to leading, trailing, and internal whitespaces.
112 | Set `stripSpaces` option to `true` to automatically strip spaces in text and comment nodes. This option
113 | doesn't change the way CDATA sections is compared, they are always compared with respect to whitespaces.
114 | Set `collapseSpaces` option to `true` to automatically collapse all spaces in text and comment nodes.
115 | This option doesn't change the way CDATA sections is compared, they are always compared with respect to
116 | whitespaces.
117 | Set `normalizeNewlines` option to `true` to automatically normalize new line characters in text,
118 | comment, and CDATA nodes.
119 |
120 | ### Cli utility
121 |
122 | When installed globally with `npm install -g dom-compare` cli utility is available.
123 | See usage information and command-line options with `domcompare --help`
124 |
125 | You can try it on bundled samples:
126 | ```
127 | $ cd samples
128 | $ domcompare -s ./expected.xml ./actual.xml
129 | Documents are not equal
130 | /document/element
131 | Attribute 'attribute': expected value '10' instead of '100'
132 | Attribute 'attributeX' is missed
133 | Extra element 'inner2'
134 | /document/element/inner
135 | Element 'node' is missed
136 | /document
137 | Expected CDATA value ' cdata node' instead of 'cdata node '
138 | ```
139 |
140 |
141 | DOM Canonic Form
142 | ----------------
143 |
144 | Implemented as [XMLSerializer](https://developer.mozilla.org/en-US/docs/XMLSerializer) interface
145 |
146 |
147 | Simple rules
148 | 1. Every node (text, node, attribute) on a new line
149 | 2. Empty tags - in a short form
150 | 3. Node indent - 4 spaces, attribute indent - 2 spaces
151 | 4. Attributes are sorted alphabetically
152 | 5. Attribute values are serialized in double quotes
153 |
154 | Consider the following XML-document...
155 | ```xml
156 |
157 | DOM Compare
158 |
159 |
160 | Text node
161 |
162 |
163 | ```
164 | ...and code snippet...
165 | ```javascript
166 | var canonizingSerializer = new (require('dom-compare').XMLSerializer)();
167 | var doc = ...; // parse above document somehow
168 | console.log(canonizingSerializer.serializeToString(doc));
169 | ```
170 | You'll receive the following output
171 | ```xml
172 |
173 |
174 | DOM Compare
175 |
176 |
177 |
180 |
181 | Text node
182 |
183 |
184 |
185 | ```
186 |
--------------------------------------------------------------------------------
/lib/compare.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | var type = require('./node_types');
6 | var Collector = require('./collector');
7 | var collapseSpaces = require('./collapse_spaces');
8 | var normalizeNewlines = require('./normalize_newlines');
9 |
10 | function Comparator(options, collector) {
11 | this._options = options || {};
12 | if(!collector)
13 | throw new Error("Collector instance must be specified");
14 | this._collector = collector;
15 | }
16 |
17 | Comparator.prototype._filterNodes = function(list) {
18 | var ret = [],
19 | i, l, item;
20 | for (i = 0, l = list.length; i < l; i++) {
21 | item = list.item(i);
22 | if (item.nodeType == type.COMMENT_NODE && !this._options.compareComments)
23 | continue;
24 | if (item.nodeType == type.TEXT_NODE && ("" + item.nodeValue).trim() == "")
25 | continue;
26 | ret.push(item);
27 | }
28 | return ret;
29 | };
30 |
31 | Comparator.prototype._compareNodeList = function(left, right) {
32 | var lLeft = this._filterNodes(left),
33 | lRight = this._filterNodes(right),
34 | i, l, result = true;
35 |
36 | for (i = 0, l = Math.max(lLeft.length, lRight.length); i < l; i++) {
37 | if(lLeft[i] && lRight[i]) {
38 | if (!this.compareNode(lLeft[i], lRight[i])) {
39 | result = false;
40 | }
41 | }
42 | else {
43 | this._collector.collectFailure(lLeft[i], lRight[i]);
44 | result = false;
45 | }
46 | }
47 | return result;
48 | };
49 |
50 | Comparator.prototype._compareAttributes = function(expected, actual) {
51 | var aExpected = {}, aActual = {},
52 | i, l;
53 |
54 | if (!expected && !actual)
55 | return true;
56 |
57 |
58 |
59 | for(i = 0, l = expected.length; i < l; i++) {
60 | aExpected[expected[i].nodeName] = expected[i];
61 | }
62 |
63 | for(i = 0, l = actual.length; i < l; i++) {
64 | aActual[actual[i].nodeName] = actual[i];
65 | }
66 |
67 | for(i in aExpected) {
68 | // both nodes has an attribute
69 | if(aExpected.hasOwnProperty(i) && aActual.hasOwnProperty(i)) {
70 | // but values is differ
71 | var vExpected = aExpected[i].nodeValue;
72 | var vActual = aActual[i].nodeValue;
73 | if(this._options.stripSpaces && aExpected[i].nodeType != type.CDATA_SECTION_NODE) {
74 | vExpected = vExpected.trim();
75 | vActual = vActual.trim();
76 | }
77 | if(this._options.collapseSpaces && aExpected[i].nodeType != type.CDATA_SECTION_NODE) {
78 | vExpected = collapseSpaces(vExpected);
79 | vActual = collapseSpaces(vActual);
80 | }
81 | if(this._options.normalizeNewlines) {
82 | vExpected = normalizeNewlines(vExpected);
83 | vActual = normalizeNewlines(vActual);
84 | }
85 | if(vExpected !== vActual) {
86 | if(!this._collector.collectFailure(aExpected[i], aActual[i]))
87 | return false;
88 | }
89 | // remove to check for extra/missed attributes;
90 | delete aActual[i];
91 | delete aExpected[i];
92 | }
93 | }
94 |
95 | // report all missed attributes
96 | for(i in aExpected) {
97 | if(aExpected.hasOwnProperty(i))
98 | if(!this._collector.collectFailure(aExpected[i], null))
99 | return false;
100 | }
101 |
102 | // report all extra attributes
103 | for(i in aActual) {
104 | if(aActual.hasOwnProperty(i))
105 | if(!this._collector.collectFailure(null, aActual[i]))
106 | return false;
107 | }
108 |
109 | return true;
110 | };
111 |
112 | Comparator.prototype.compareNode = function(left, right) {
113 | var vLeft, vRight, r;
114 |
115 | if (typeof left === 'string' || typeof right === 'string') {
116 | throw new Error('String comparison is not supported. You must parse string to document to perform comparison.');
117 | }
118 |
119 | if (left.nodeName === right.nodeName && left.nodeType == right.nodeType) {
120 | switch (left.nodeType) {
121 | case type.DOCUMENT_NODE:
122 | return this.compareNode(left.documentElement, right.documentElement);
123 | case type.ELEMENT_NODE:
124 | return this._compareAttributes(left.attributes, right.attributes) &&
125 | this._compareNodeList(left.childNodes, right.childNodes);
126 | case type.TEXT_NODE:
127 | // fallthrough
128 | case type.CDATA_SECTION_NODE:
129 | // fallthrough
130 | case type.DOCUMENT_FRAGMENT_NODE:
131 | // fallthrough
132 | case type.COMMENT_NODE:
133 | if (left.nodeType == type.COMMENT_NODE && !this._options.compareComments)
134 | return true;
135 | vLeft = "" + left.nodeValue;
136 | vRight = "" + right.nodeValue;
137 | if (this._options.stripSpaces && left.nodeType !== type.CDATA_SECTION_NODE) {
138 | vLeft = vLeft.trim();
139 | vRight = vRight.trim();
140 | }
141 | if (this._options.collapseSpaces && left.nodeType !== type.CDATA_SECTION_NODE) {
142 | vLeft = collapseSpaces(vLeft);
143 | vRight = collapseSpaces(vRight);
144 | }
145 | if (this._options.normalizeNewlines) {
146 | vLeft = normalizeNewlines(vLeft);
147 | vRight = normalizeNewlines(vRight);
148 | }
149 | r = vLeft === vRight;
150 | return !r ? this._collector.collectFailure(left, right) : r;
151 | default:
152 | throw Error("Node type " + left.nodeType + " comparison is not implemented");
153 | }
154 | } else
155 | return this._collector.collectFailure(left, right);
156 | };
157 |
158 | module.exports = function(a, b, options) {
159 |
160 |
161 | var collector = new Collector(options);
162 | var comparator = new Comparator(options, collector);
163 | comparator.compareNode(a, b);
164 |
165 | return collector;
166 |
167 | };
168 |
169 | })();
170 |
--------------------------------------------------------------------------------
/lib/collector.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | "use strict";
4 |
5 | var type = require('./node_types');
6 | var revxpath = require('./revxpath.js');
7 | var collapseSpaces = require('./collapse_spaces');
8 | var normalizeNewlines = require('./normalize_newlines');
9 |
10 | var typeMap = {},
11 | comparatorTypeMap = {};
12 |
13 | typeMap[type.ATTRIBUTE_NODE] = "attribute";
14 | typeMap[type.ELEMENT_NODE] = "element";
15 | typeMap[type.TEXT_NODE] = "text node";
16 | typeMap[type.COMMENT_NODE] = "comment node";
17 | typeMap[type.CDATA_SECTION_NODE] = "CDATA node";
18 | typeMap[type.DOCUMENT_NODE] = "document";
19 | typeMap[type.DOCUMENT_FRAGMENT_NODE] = "document fragment";
20 |
21 | Object.keys(type).forEach(function(k){
22 | comparatorTypeMap[type[k]] = k;
23 | });
24 |
25 | function Collector(options) {
26 | this._diff = [];
27 | this._options = options || {};
28 | }
29 |
30 | Collector.prototype._describeNode = function(node) {
31 | if(node.nodeType == type.TEXT_NODE ||
32 | node.nodeType == type.CDATA_SECTION_NODE ||
33 | node.nodeType == type.COMMENT_NODE) {
34 | var nodeValue = node.nodeValue;
35 |
36 | if(this._options.stripSpaces) {
37 | nodeValue = nodeValue.trim();
38 | }
39 | if(this._options.collapseSpaces) {
40 | nodeValue = collapseSpaces(nodeValue);
41 | }
42 | if(this._options.normalizeNewlines) {
43 | nodeValue = normalizeNewlines(nodeValue);
44 | }
45 |
46 | return "'" + nodeValue + "'";
47 | }
48 | else
49 | return "'" + node.nodeName + "'";
50 | };
51 |
52 | Collector.prototype.getDifferences = function() {
53 | return this._diff;
54 | };
55 |
56 | Collector.prototype.getResult = function() {
57 | return this._diff.length == 0
58 | };
59 |
60 | Collector.prototype.collectFailure = function(expected, actual) {
61 |
62 | var msg, canContinue = true, vExpected, vActual, ref = expected || actual, cmprtr, r;
63 |
64 | if(this._options.comparators && (cmprtr = this._options.comparators[comparatorTypeMap[ref.nodeType]])) {
65 | if(!(cmprtr instanceof Array))
66 | cmprtr = [ cmprtr ];
67 | for(var i = 0, l = cmprtr.length; i < l; i++) {
68 | r = cmprtr[i](expected, actual);
69 | if(r) {
70 | // true -> ignore differences. Stop immediately, continue;
71 | if(r === true) {
72 | return true;
73 | }
74 | // string - treat as error message, continue;
75 | else if(typeof r == 'string') {
76 | msg = r;
77 | canContinue = true;
78 | }
79 | // object - .message = error message, .stop - stop flag
80 | else if(typeof r == 'object') {
81 | msg = r.message;
82 | canContinue = !(!!r.stop);
83 | }
84 | break;
85 | }
86 |
87 | }
88 | }
89 |
90 | if(!msg) {
91 |
92 | if(expected && !actual) {
93 | msg = typeMap[expected.nodeType].charAt(0).toUpperCase() + typeMap[expected.nodeType].substr(1) +
94 | " " + this._describeNode(expected) + " is missed";
95 | canContinue = true;
96 | }
97 | else if(!expected && actual) {
98 | msg = "Extra " + typeMap[actual.nodeType] + " " + this._describeNode(actual);
99 | canContinue = true;
100 | }
101 | else {
102 | if(expected.nodeType == actual.nodeType) {
103 | if(expected.nodeName == actual.nodeName) {
104 | vExpected = expected.nodeValue;
105 | vActual = actual.nodeValue;
106 | if(this._options.stripSpaces && expected.nodeType != type.CDATA_SECTION_NODE) {
107 | vExpected = vExpected.trim();
108 | vActual = vActual.trim();
109 | }
110 | if(this._options.collapseSpaces && expected.nodeType != type.CDATA_SECTION_NODE) {
111 | vExpected = collapseSpaces(vExpected);
112 | vActual = collapseSpaces(vActual);
113 | }
114 | if(this._options.normalizeNewlines) {
115 | vExpected = normalizeNewlines(vExpected);
116 | vActual = normalizeNewlines(vActual);
117 | }
118 | if(vExpected == vActual)
119 | throw new Error("Nodes are considered equal but shouldn't");
120 | else {
121 | switch(expected.nodeType) {
122 | case type.ATTRIBUTE_NODE:
123 | msg = "Attribute '" + expected.nodeName + "': expected value '" + vExpected + "' instead of '" + vActual + "'";
124 | break;
125 | case type.COMMENT_NODE:
126 | msg = "Expected comment value '" + vExpected + "' instead of '" + vActual + "'";
127 | break;
128 | case type.CDATA_SECTION_NODE:
129 | msg = "Expected CDATA value '" + vExpected + "' instead of '" + vActual + "'";
130 | break;
131 | case type.TEXT_NODE:
132 | msg = "Expected text '" + vExpected + "' instead of '" + vActual + "'";
133 | break;
134 | default:
135 | throw new Error("nodeValue is not equal, but nodeType is unexpected");
136 | }
137 | canContinue = true;
138 | }
139 | }
140 | else {
141 | msg = "Expected " + typeMap[expected.nodeType] +
142 | " '" + expected.nodeName + "' instead of '" + actual.nodeName + "'";
143 | canContinue = false;
144 | }
145 | }
146 | else {
147 | msg = "Expected node of type " + expected.nodeType +
148 | " (" + typeMap[expected.nodeType] + ") instead of " +
149 | actual.nodeType + " (" + typeMap[actual.nodeType] + ")";
150 | canContinue = false;
151 | }
152 | }
153 | }
154 |
155 | this._diff.push({
156 | node: revxpath(ref.ownerElement || ref.parentNode),
157 | message: msg
158 | });
159 |
160 | return canContinue;
161 | };
162 |
163 | module.exports = Collector;
164 |
165 |
166 | })();
167 |
--------------------------------------------------------------------------------
/test/test-collector.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 | var xmldom = require("xmldom");
3 | var parser = new xmldom.DOMParser();
4 | var domCompare = require("../");
5 | var compare = domCompare.compare;
6 |
7 | describe("Error collection", function(){
8 |
9 | it("In case root node differs - no any other checks are made", function(){
10 |
11 | var doc1 = parser.parseFromString("");
12 | var doc2 = parser.parseFromString("");
13 |
14 | var result = compare(doc1, doc2, {});
15 |
16 | var failures = result.getDifferences();
17 |
18 | assert.equal(1, failures.length);
19 | assert.equal("Expected element 'root1' instead of 'root2'", failures[0].message);
20 |
21 | });
22 |
23 | describe("Attributes", function(){
24 |
25 | it("All attributes at single node are compared", function(){
26 |
27 | var doc1 = parser.parseFromString("");
28 | var doc2 = parser.parseFromString("");
29 |
30 | var result = compare(doc1, doc2, {});
31 |
32 | var failures = result.getDifferences();
33 |
34 | assert.equal(3, failures.length);
35 | assert.equal("Attribute 'attr1': expected value '1' instead of '10'", failures[0].message);
36 | assert.equal("Attribute 'attr3' is missed", failures[1].message);
37 | assert.equal("Extra attribute 'attr4'", failures[2].message);
38 |
39 | });
40 |
41 | it("All attributes that are not found is reported", function(){
42 |
43 | var doc1 = parser.parseFromString("");
44 | var doc2 = parser.parseFromString("");
45 |
46 | var result = compare(doc1, doc2, {});
47 |
48 | var failures = result.getDifferences();
49 |
50 | assert.equal(2, failures.length);
51 | assert.equal("Attribute 'attr2' is missed", failures[0].message);
52 | assert.equal("Attribute 'attr3' is missed", failures[1].message);
53 |
54 | // Case: Target has no attributes at all
55 | doc1 = parser.parseFromString("");
56 | doc2 = parser.parseFromString("");
57 |
58 | result = compare(doc1, doc2, {});
59 |
60 | failures = result.getDifferences();
61 |
62 | assert.equal(2, failures.length);
63 | assert.equal("Attribute 'attr2' is missed", failures[0].message);
64 | assert.equal("Attribute 'attr3' is missed", failures[1].message);
65 |
66 | });
67 |
68 | it("All extra attributes is reported", function(){
69 |
70 | var doc1 = parser.parseFromString("");
71 | var doc2 = parser.parseFromString("");
72 |
73 | var result = compare(doc1, doc2, {});
74 |
75 | var failures = result.getDifferences();
76 |
77 | assert.equal(2, failures.length);
78 | assert.equal("Extra attribute 'attr2'", failures[0].message);
79 | assert.equal("Extra attribute 'attr3'", failures[1].message);
80 |
81 | // Case: Source has no attributes at all
82 | doc1 = parser.parseFromString("");
83 | doc2 = parser.parseFromString("");
84 |
85 | result = compare(doc1, doc2, {});
86 |
87 | failures = result.getDifferences();
88 |
89 | assert.equal(2, failures.length);
90 | assert.equal("Extra attribute 'attr2'", failures[0].message);
91 | assert.equal("Extra attribute 'attr3'", failures[1].message);
92 |
93 | });
94 |
95 | });
96 |
97 | describe("Nodes comparison", function(){
98 |
99 | it("Differences reported by types first", function(){
100 | var doc1 = parser.parseFromString("");
101 | var doc2 = parser.parseFromString("TextTextText");
102 |
103 | var result = compare(doc1, doc2, {});
104 |
105 | var failures = result.getDifferences();
106 |
107 | assert.equal(1, failures.length);
108 | assert.equal("Expected node of type 1 (element) instead of 3 (text node)", failures[0].message);
109 | });
110 |
111 | it("... and by node names then", function(){
112 | var doc1 = parser.parseFromString("");
113 | var doc2 = parser.parseFromString("");
114 |
115 | var result = compare(doc1, doc2, {});
116 |
117 | var failures = result.getDifferences();
118 |
119 | assert.equal(1, failures.length);
120 | assert.equal("Expected element 'a' instead of 'b'", failures[0].message);
121 | });
122 |
123 | describe("Elements", function(){
124 |
125 | });
126 |
127 | describe("Text nodes", function(){
128 |
129 | it("Extra nodes reported", function(){
130 | var doc1 = parser.parseFromString("First");
131 | var doc2 = parser.parseFromString("FirstSecond");
132 |
133 | var result = compare(doc1, doc2, {});
134 |
135 | var failures = result.getDifferences();
136 |
137 | assert.equal(1, failures.length);
138 | assert.equal("Extra text node 'Second'", failures[0].message);
139 | });
140 |
141 | it("Not found nodes reported", function(){
142 | var doc1 = parser.parseFromString("FirstSecond");
143 | var doc2 = parser.parseFromString("First");
144 |
145 | var result = compare(doc1, doc2, {});
146 |
147 | var failures = result.getDifferences();
148 |
149 | assert.equal(1, failures.length);
150 | assert.equal("Text node 'Second' is missed", failures[0].message);
151 |
152 | doc1 = parser.parseFromString("First Second");
153 | doc2 = parser.parseFromString(" First");
154 |
155 | result = compare(doc1, doc2, { stripSpaces: true });
156 |
157 | failures = result.getDifferences();
158 |
159 | assert.equal(1, failures.length);
160 | assert.equal("Text node 'Second' is missed", failures[0].message);
161 | });
162 |
163 | it("Different content reported", function(){
164 | var doc1 = parser.parseFromString("Foo");
165 | var doc2 = parser.parseFromString("Bar");
166 |
167 | var result = compare(doc1, doc2, {});
168 |
169 | var failures = result.getDifferences();
170 |
171 | assert.equal(1, failures.length);
172 | assert.equal("Expected text 'Foo' instead of 'Bar'", failures[0].message);
173 | })
174 |
175 | })
176 |
177 | });
178 |
179 |
180 | describe("Custom comparison routine", function(){
181 |
182 | it("User can provide custom comparison routine, it can be used to extended reporting", function(){
183 |
184 | var doc1 = parser.parseFromString("");
185 | var doc2 = parser.parseFromString("");
186 |
187 | var result = compare(doc1, doc2, {
188 | comparators: {
189 | ATTRIBUTE_NODE: function(e, a) {
190 | if(e.nodeValue > a.nodeValue)
191 | return "Actual value is less than expected";
192 | else if(e.nodeValue < a.nodeValue)
193 | return "Actual value is greater than expected";
194 | }
195 | }
196 | });
197 |
198 | var failures = result.getDifferences();
199 |
200 | assert.equal(1, failures.length);
201 | assert.equal("Actual value is greater than expected", failures[0].message);
202 |
203 |
204 | });
205 |
206 | it("Custom comparison routine can ignore node differences", function(){
207 |
208 | var doc1 = parser.parseFromString("");
209 | var doc2 = parser.parseFromString("");
210 |
211 | var result = compare(doc1, doc2, {
212 | comparators: {
213 | ATTRIBUTE_NODE: function() {
214 | return true;
215 | }
216 | }
217 | });
218 |
219 | var failures = result.getDifferences();
220 |
221 | assert.equal(0, failures.length);
222 |
223 | });
224 |
225 | it("Custom comparison routine can skip node checking. It will be processed by common routine.", function(){
226 |
227 | var doc1 = parser.parseFromString("");
228 | var doc2 = parser.parseFromString("");
229 |
230 | var result = compare(doc1, doc2, {
231 | comparators: {
232 | ATTRIBUTE_NODE: function(e, a) {
233 | if(e.nodeName == 'attr') {
234 | if(e.nodeValue > a.nodeValue)
235 | return "Actual value is less than expected";
236 | else if(e.nodeValue < a.nodeValue)
237 | return "Actual value is greater than expected";
238 | }
239 | }
240 | }
241 | });
242 |
243 | var failures = result.getDifferences();
244 |
245 | assert.equal(2, failures.length);
246 | assert.equal("Actual value is greater than expected", failures[0].message);
247 | assert.equal("Attribute 'm': expected value '2' instead of '3'", failures[1].message);
248 |
249 |
250 | });
251 |
252 | });
253 |
254 |
255 | });
--------------------------------------------------------------------------------
/test/test-compare.js:
--------------------------------------------------------------------------------
1 | var assert = require("assert");
2 | var xmldom = require("xmldom");
3 | var parser = new xmldom.DOMParser();
4 | var domCompare = require("../");
5 | var compare = domCompare.compare;
6 |
7 | describe('Compare', function () {
8 |
9 | it('equal documents reports no differences', function() {
10 | assert.equal(true, compare(
11 | parser.parseFromString(''),
12 | parser.parseFromString('')
13 | ).getResult());
14 | });
15 |
16 | describe('Strings', function() {
17 | it('string comparison is not supported, error is thrown', function() {
18 | var errorText;
19 | try {
20 | compare('', '');
21 | } catch(e) {
22 | errorText = '' + e;
23 | }
24 | assert.equal(
25 | 'Error: String comparison is not supported. You must parse string to document to perform comparison.',
26 | errorText);
27 | });
28 | });
29 |
30 |
31 | describe('Documents', function () {
32 | it('with different root node names are different', function () {
33 | var doc1 = parser.parseFromString("");
34 | var doc2 = parser.parseFromString("");
35 | assert.equal(false, compare(doc1, doc2).getResult());
36 | });
37 |
38 | it('with same root node names are same', function () {
39 | var doc1 = parser.parseFromString("");
40 | var doc2 = parser.parseFromString("");
41 | assert.equal(true, compare(doc1, doc2).getResult());
42 | });
43 |
44 | it('with same attribute set but different order are same', function () {
45 | var doc1 = parser.parseFromString("");
46 | var doc2 = parser.parseFromString("");
47 | assert.equal(true, compare(doc1, doc2).getResult());
48 | });
49 |
50 | it('with different attributes set is different', function () {
51 | var doc1 = parser.parseFromString("");
52 | var doc2 = parser.parseFromString("");
53 | assert.equal(false, compare(doc1, doc2).getResult());
54 | });
55 |
56 | it('with same attributes but with different values is different', function () {
57 | var doc1 = parser.parseFromString("");
58 | var doc2 = parser.parseFromString("");
59 | assert.equal(false, compare(doc1, doc2).getResult());
60 | });
61 | });
62 |
63 | describe('Document fragments', function () {
64 | it('supported', function () {
65 | var doc = parser.parseFromString("");
66 |
67 | var frag1 = doc.createDocumentFragment();
68 | var frag2 = doc.createDocumentFragment();
69 |
70 | frag1.appendChild(doc.createElement('div'));
71 | frag2.appendChild(doc.createElement('div'));
72 |
73 | assert.equal(true, compare(frag1, frag2).getResult());
74 | });
75 | });
76 |
77 | describe('Nodes', function () {
78 | it('with different names are different', function () {
79 | var doc1 = parser.parseFromString("");
80 | var doc2 = parser.parseFromString("");
81 | assert.equal(false, compare(doc1, doc2).getResult());
82 | });
83 |
84 | it('with same names are same', function () {
85 | var doc1 = parser.parseFromString("");
86 | var doc2 = parser.parseFromString("");
87 | assert.equal(true, compare(doc1, doc2).getResult());
88 | });
89 |
90 | it('with same attribute set but different order are same', function () {
91 | var doc1 = parser.parseFromString("");
92 | var doc2 = parser.parseFromString("");
93 | assert.equal(true, compare(doc1, doc2).getResult());
94 | });
95 |
96 | it('with different attributes set is different', function () {
97 | var doc1 = parser.parseFromString("");
98 | var doc2 = parser.parseFromString("");
99 | assert.equal(false, compare(doc1, doc2).getResult());
100 | });
101 |
102 | it('with same attributes but with different values is different', function () {
103 | var doc1 = parser.parseFromString("");
104 | var doc2 = parser.parseFromString("");
105 | assert.equal(false, compare(doc1, doc2).getResult());
106 |
107 | describe("spaces matters", function(){
108 |
109 | it("", function(){
110 | var doc1 = parser.parseFromString("");
111 | var doc2 = parser.parseFromString("");
112 | assert.equal(false, compare(doc1, doc2).getResult());
113 | });
114 |
115 | it("but can be omitted", function(){
116 | doc1 = parser.parseFromString("");
117 | doc2 = parser.parseFromString("");
118 | assert.equal(true, compare(doc1, doc2, { stripSpaces: true }).getResult());
119 | });
120 |
121 | it("but can be collapsed", function(){
122 | doc1 = parser.parseFromString("");
123 | doc2 = parser.parseFromString("");
124 | assert.equal(true, compare(doc1, doc2, { collapseSpaces: true }).getResult());
125 | });
126 |
127 | it("but newlines can be normalized", function(){
128 | doc1 = parser.parseFromString("");
129 | doc2 = parser.parseFromString("");
130 | assert.equal(true, compare(doc1, doc2, { normalizeNewlines: true }).getResult());
131 | });
132 | });
133 |
134 | });
135 | });
136 |
137 | describe('Node Lists', function () {
138 | it("order of child nodes matters", function () {
139 | var doc1 = parser.parseFromString("");
140 | var doc2 = parser.parseFromString("");
141 | assert.equal(false, compare(doc1, doc2).getResult());
142 | });
143 |
144 | it("full list of child nodes is compared", function() {
145 | var doc1 = parser.parseFromString("");
146 | var doc2 = parser.parseFromString("");
147 | assert.equal(false, compare(doc1, doc2).getResult());
148 | assert.equal(2, compare(doc1, doc2).getDifferences().length);
149 | });
150 | });
151 |
152 | describe("Text nodes", function () {
153 |
154 | describe('Comments', function () {
155 | it("normally comments are ignored", function () {
156 | var doc1 = parser.parseFromString("");
157 | var doc2 = parser.parseFromString("");
158 | assert.equal(true, compare(doc1, doc2).getResult());
159 |
160 | doc1 = parser.parseFromString("");
161 | doc2 = parser.parseFromString("");
162 | assert.equal(true, compare(doc1, doc2).getResult());
163 | });
164 |
165 | it("when `compareComments` options is set - comments are compared too", function () {
166 | var doc1 = parser.parseFromString("");
167 | var doc2 = parser.parseFromString("");
168 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
169 |
170 | doc1 = parser.parseFromString("");
171 | doc2 = parser.parseFromString("");
172 | assert.equal(true, compare(doc1, doc2, { compareComments: true }).getResult());
173 |
174 | doc1 = parser.parseFromString("");
175 | doc2 = parser.parseFromString("");
176 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
177 | });
178 |
179 | describe("Extra whitespace stripping", function () {
180 | it("normally all whitespaces at the beginning/end are preserved", function () {
181 | var doc1 = parser.parseFromString("");
182 | var doc2 = parser.parseFromString("");
183 | assert.equal(true, compare(doc1, doc2, { compareComments: true }).getResult());
184 |
185 | doc1 = parser.parseFromString("");
186 | doc2 = parser.parseFromString("");
187 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
188 |
189 | doc1 = parser.parseFromString("");
190 | doc2 = parser.parseFromString("");
191 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
192 | });
193 |
194 | it("`stripSpaces` option strips them", function () {
195 | var doc1 = parser.parseFromString("");
196 | var doc2 = parser.parseFromString("");
197 | assert.equal(true, compare(doc1, doc2, {
198 | compareComments: true,
199 | stripSpaces: true
200 | }).getResult());
201 |
202 | doc1 = parser.parseFromString("");
203 | doc2 = parser.parseFromString("");
204 | assert.equal(true, compare(doc1, doc2, {
205 | compareComments: true,
206 | stripSpaces: true
207 | }).getResult());
208 |
209 | doc1 = parser.parseFromString("");
210 | doc2 = parser.parseFromString("");
211 | assert.equal(true, compare(doc1, doc2, {
212 | compareComments: true,
213 | stripSpaces: true
214 | }).getResult());
215 | });
216 | });
217 |
218 | describe("Whitespace collapsing", function () {
219 | it("normally all whitespaces are preserved", function () {
220 | var doc1 = parser.parseFromString("");
221 | var doc2 = parser.parseFromString("");
222 | assert.equal(true, compare(doc1, doc2, { compareComments: true }).getResult());
223 |
224 | doc1 = parser.parseFromString("");
225 | doc2 = parser.parseFromString("");
226 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
227 | });
228 |
229 | it("`collapseSpaces` option strips them", function () {
230 | var doc1 = parser.parseFromString("");
231 | var doc2 = parser.parseFromString("");
232 | assert.equal(true, compare(doc1, doc2, {
233 | compareComments: true,
234 | collapseSpaces: true
235 | }).getResult());
236 |
237 | doc1 = parser.parseFromString("");
238 | doc2 = parser.parseFromString("");
239 | assert.equal(true, compare(doc1, doc2, {
240 | compareComments: true,
241 | collapseSpaces: true
242 | }).getResult());
243 |
244 | doc1 = parser.parseFromString("");
245 | doc2 = parser.parseFromString("");
246 | assert.equal(true, compare(doc1, doc2, {
247 | compareComments: true,
248 | collapseSpaces: true
249 | }).getResult());
250 |
251 | doc1 = parser.parseFromString("");
252 | doc2 = parser.parseFromString("");
253 | assert.equal(true, compare(doc1, doc2, {
254 | compareComments: true,
255 | collapseSpaces: true
256 | }).getResult());
257 | });
258 | });
259 |
260 | describe("Newline normalizing", function () {
261 | it("normally all whitespaces at the beginning/end are preserved", function () {
262 | var doc1 = parser.parseFromString("");
263 | var doc2 = parser.parseFromString("");
264 | assert.equal(true, compare(doc1, doc2, { compareComments: true }).getResult());
265 |
266 | doc1 = parser.parseFromString("");
267 | doc2 = parser.parseFromString("");
268 | assert.equal(false, compare(doc1, doc2, { compareComments: true }).getResult());
269 | });
270 |
271 | it("`normalizeNewlines` option normalizes them", function () {
272 | var doc1 = parser.parseFromString("");
273 | var doc2 = parser.parseFromString("");
274 | assert.equal(true, compare(doc1, doc2, {
275 | compareComments: true,
276 | normalizeNewlines: true
277 | }).getResult());
278 | });
279 | });
280 | });
281 |
282 | describe("Text", function () {
283 | it("compared by default with all whitespaces", function () {
284 | var doc1 = parser.parseFromString("AB");
285 | var doc2 = parser.parseFromString("AB");
286 | assert.equal(true, compare(doc1, doc2).getResult());
287 |
288 | doc1 = parser.parseFromString("AB");
289 | doc2 = parser.parseFromString("A B");
290 | assert.equal(false, compare(doc1, doc2).getResult());
291 |
292 | doc1 = parser.parseFromString("BA");
293 | doc2 = parser.parseFromString("AB");
294 | assert.equal(false, compare(doc1, doc2).getResult());
295 |
296 | doc1 = parser.parseFromString("A");
297 | doc2 = parser.parseFromString(" A ");
298 | assert.equal(false, compare(doc1, doc2).getResult());
299 | });
300 |
301 | it("empty text nodes are always ignored", function(){
302 |
303 | var doc1 = parser.parseFromString(" ");
304 | var doc2 = parser.parseFromString(" ");
305 | assert.equal(true, compare(doc1, doc2).getResult());
306 |
307 | });
308 |
309 | it("set `stripSpaces` option to get rid of them", function () {
310 | var doc1 = parser.parseFromString("A");
311 | var doc2 = parser.parseFromString(" A ");
312 | assert.equal(true, compare(doc1, doc2, { stripSpaces: true }).getResult());
313 | });
314 |
315 | it("set `collapseSpaces` option to collapse them", function () {
316 | var doc1 = parser.parseFromString(" A \n\r\t B ");
317 | var doc2 = parser.parseFromString(" A \t\n\r B ");
318 | assert.equal(true, compare(doc1, doc2, { collapseSpaces: true }).getResult());
319 |
320 | doc1 = parser.parseFromString(" \n\r\t A B ");
321 | doc2 = parser.parseFromString(" \t\n\r A B ");
322 | assert.equal(true, compare(doc1, doc2, { collapseSpaces: true }).getResult());
323 |
324 | doc1 = parser.parseFromString(" A B \n\r\t ");
325 | doc2 = parser.parseFromString(" A B \t\n\r ");
326 | assert.equal(true, compare(doc1, doc2, { collapseSpaces: true }).getResult());
327 |
328 | doc1 = parser.parseFromString(" \n\r\t A B \n\r\t ");
329 | doc2 = parser.parseFromString(" A \t\n\r B \t\n\r ");
330 | assert.equal(true, compare(doc1, doc2, { collapseSpaces: true }).getResult());
331 | });
332 |
333 | it("set `normalizeNewlines` option to normalize newlines", function () {
334 | var doc1 = parser.parseFromString(" \r\n A \r B \n ");
335 | var doc2 = parser.parseFromString(" \n A \r\n B \r ");
336 | assert.equal(true, compare(doc1, doc2, { normalizeNewlines: true }).getResult());
337 | });
338 | });
339 |
340 | describe("CDATA", function () {
341 | it("compared as text nodes but `stripSpaces` and `collapseSpaces` are not respected", function () {
342 | var doc1 = parser.parseFromString("");
343 | var doc2 = parser.parseFromString("");
344 | assert.equal(true, compare(doc1, doc2).getResult());
345 |
346 | doc1 = parser.parseFromString("");
347 | doc2 = parser.parseFromString("");
348 | assert.equal(false, compare(doc1, doc2).getResult());
349 |
350 | doc1 = parser.parseFromString("");
351 | doc2 = parser.parseFromString("");
352 | assert.equal(false, compare(doc1, doc2).getResult());
353 |
354 | doc1 = parser.parseFromString("");
355 | doc2 = parser.parseFromString("");
356 | assert.equal(false, compare(doc1, doc2, { stripSpaces: true }).getResult());
357 |
358 | doc1 = parser.parseFromString("");
359 | doc2 = parser.parseFromString("");
360 | assert.equal(false, compare(doc1, doc2, { collapseSpaces: true }).getResult());
361 |
362 | doc1 = parser.parseFromString("");
363 | doc2 = parser.parseFromString("");
364 | assert.equal(false, compare(doc1, doc2, { collapseSpaces: true }).getResult());
365 |
366 | doc1 = parser.parseFromString("");
367 | doc2 = parser.parseFromString("");
368 | assert.equal(false, compare(doc1, doc2, { collapseSpaces: true }).getResult());
369 |
370 | doc1 = parser.parseFromString("");
371 | doc2 = parser.parseFromString("");
372 | assert.equal(false, compare(doc1, doc2, { collapseSpaces: true }).getResult());
373 | });
374 |
375 | it("set `normalizeNewlines` option to normalize newlines", function () {
376 | var doc1 = parser.parseFromString("");
377 | var doc2 = parser.parseFromString("");
378 | assert.equal(true, compare(doc1, doc2, { normalizeNewlines: true }).getResult());
379 | });
380 | });
381 | });
382 |
383 | describe('Regression tests', function() {
384 | it('Issue #36', function() {
385 | var str1 = `
386 | Tove
387 | Jani
388 | Reminder
389 | I got changed
390 | `;
391 | var str2 = `
392 | Tove
393 | Jani
394 | Reminder
395 | Don't forget me this weekend!
396 | seventeen
397 | street of freedom
398 |
399 | Hiyo
400 |
401 | `;
402 |
403 | var doc1 = parser.parseFromString(str1);
404 | var doc2 = parser.parseFromString(str2);
405 | assert.equal(false, compare(doc1, doc2).getResult());
406 | var diff = compare(doc1, doc2).getDifferences();
407 | assert.equal(4, diff.length);
408 | assert.equal(
409 | "Expected text 'I got changed' instead of 'Don't forget me this weekend!'",
410 | diff[0].message);
411 | assert.equal(
412 | "Extra element 'age'",
413 | diff[1].message);
414 | assert.equal(
415 | "Extra element 'address'",
416 | diff[2].message);
417 | assert.equal(
418 | "Extra element 'anotherelement'",
419 | diff[3].message);
420 | });
421 | });
422 | });
423 |
--------------------------------------------------------------------------------