mirror of
https://github.com/jlengrand/open-wc.git
synced 2026-03-10 08:31:19 +00:00
feat(chai-dom-equals): improve chai-dom-equals error reporting (#111)
This commit is contained in:
@@ -30,8 +30,7 @@ chai.use(chaiDomEquals);
|
||||
```js
|
||||
it('has the following dom', async () => {
|
||||
const el = await fixture(`<div><!-- comment --><h1>${'Hey'} </h1> </div>`);
|
||||
expect(el).dom.to.equal('<div><!-- comment --><h1>Hey </h1> </div>');
|
||||
expect(el).dom.to.semantically.equal('<div><h1>Hey</h1></div>');
|
||||
expect(el).dom.to.equal('<div><h1>Hey</h1></div>');
|
||||
});
|
||||
```
|
||||
|
||||
@@ -49,7 +48,35 @@ it('has the following shadow dom', async () => {
|
||||
}
|
||||
});
|
||||
const el = await fixture(`<${tag}></${tag}>`);
|
||||
expect(el).shadowDom.to.equal('<p> shadow content</p> <!-- comment --> <slot></slot>');
|
||||
expect(el).shadowDom.to.semantically.equal('<p>shadow content</p><slot>');
|
||||
expect(el).shadowDom.to.equal('<p>shadow content</p><slot>');
|
||||
});
|
||||
```
|
||||
|
||||
## Literal matching
|
||||
By default dom is diffed 'semantically'. Differences in whitespace, newlines, attributes/class order are ignored and style, script and commend nodes are removed.
|
||||
|
||||
If you want to match literally instead you can use some of the provided utilities to handle diffing on browsers with the shadow dom polyfill:
|
||||
|
||||
```javascript
|
||||
import { getOuterHtml, getCleanedShadowDom } from '@open-wc/chai-dom-equals';
|
||||
|
||||
it('literally equals', () => {
|
||||
const tag = defineCE(class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.shadowRoot.innerHTML = '<p> shadow content</p> <!-- comment --> <slot></slot>';
|
||||
}
|
||||
});
|
||||
const el = await fixture(`<${tag}></${tag}>`);
|
||||
const outerHTML = getOuterHtml(el);
|
||||
const innerHTML = getCleanedShadowDom(el);
|
||||
|
||||
expect(outerHTML).to.equal(`<${tag}></${tag}>`);
|
||||
expect(innerHTML).to.equal('<p> shadow content</p> <!-- comment --> <slot></slot>');
|
||||
});
|
||||
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getSemanticDomDiff } from '@open-wc/semantic-dom-diff';
|
||||
import { getDiffableSemanticHTML } from '@open-wc/semantic-dom-diff';
|
||||
|
||||
/**
|
||||
* el.outerHTML is not polyfilled so we need to recreate the tag + attributes and
|
||||
@@ -50,6 +50,8 @@ export const chaiDomEquals = (chai, utils) => {
|
||||
});
|
||||
|
||||
// can not be an arrow function as it gets rebound
|
||||
// TODO: this is here for backwards compatibility, removal will be
|
||||
// a breaking change
|
||||
chai.Assertion.addProperty('semantically', function shadowDom() {
|
||||
new chai.Assertion(this._obj.nodeType).to.equal(1);
|
||||
utils.flag(this, 'semantically', true);
|
||||
@@ -58,60 +60,34 @@ export const chaiDomEquals = (chai, utils) => {
|
||||
// can not be an arrow function as it gets rebound
|
||||
// prettier-ignore
|
||||
const domEquals = _super => function handleDom(value, ...args) {
|
||||
if (!utils.flag(this, 'semantically') && utils.flag(this, 'dom')) {
|
||||
const expected = getOuterHtml(this._obj);
|
||||
this.assert(
|
||||
value === expected,
|
||||
'expected dom #{exp} to equal #{act}',
|
||||
'expected dom #{exp} to not equal #{act}',
|
||||
expected,
|
||||
value,
|
||||
);
|
||||
} else if (!utils.flag(this, 'semantically') && utils.flag(this, 'shadowDom')) {
|
||||
const expected = getCleanedShadowDom(this._obj);
|
||||
this.assert(
|
||||
value === expected,
|
||||
'expected shadow dom #{exp} to equal #{act}',
|
||||
'expected shadow dom #{exp} to not equal #{act}',
|
||||
expected,
|
||||
value,
|
||||
);
|
||||
} else if (utils.flag(this, 'semantically') && utils.flag(this, 'dom')) {
|
||||
const result = getSemanticDomDiff(value, getOuterHtml(this._obj));
|
||||
const message = result ? result.message : '';
|
||||
const path = result ? result.path : '';
|
||||
const normalizedHTML = result ? result.normalizedRightHTML : '';
|
||||
this.assert(
|
||||
result === null,
|
||||
() => {
|
||||
/* eslint-disable no-console */
|
||||
console.log('Snapshot changed, want to accept the change?');
|
||||
console.log('Updated Snapshot:');
|
||||
console.log('');
|
||||
console.log(normalizedHTML);
|
||||
/* eslint-enable no-console */
|
||||
return `expected dom to be semantically equal\n- diff found: ${message}\n- in path: ${path}`;
|
||||
},
|
||||
'expected dom to not be semantically equal',
|
||||
);
|
||||
} else if (utils.flag(this, 'semantically') && utils.flag(this, 'shadowDom')) {
|
||||
const result = getSemanticDomDiff(value, getCleanedShadowDom(this._obj));
|
||||
const message = result ? result.message : '';
|
||||
const path = result ? result.path : '';
|
||||
const normalizedHTML = result ? result.normalizedRightHTML : '';
|
||||
this.assert(
|
||||
result === null,
|
||||
() => {
|
||||
/* eslint-disable no-console */
|
||||
console.log('Snapshot changed, want to accept the change?');
|
||||
console.log('Updated Snapshot:');
|
||||
console.log('');
|
||||
console.log(normalizedHTML);
|
||||
/* eslint-enable no-console */
|
||||
return `expected shadow dom to be semantically equal\n- diff found: ${message}\n- in path: ${path}`;
|
||||
},
|
||||
'expected shadow dom to not be semantically equal',
|
||||
);
|
||||
if (utils.flag(this, 'dom')) {
|
||||
const expectedHTML = getDiffableSemanticHTML(value);
|
||||
const actualHTML = getDiffableSemanticHTML(getOuterHtml(this._obj));
|
||||
|
||||
// use chai's built-in string comparison, log the updated snapshot on error
|
||||
try {
|
||||
new chai.Assertion(actualHTML).to.equal(expectedHTML);
|
||||
} catch (error) {
|
||||
console.log('Snapshot changed, want to accept the change:');
|
||||
console.log('');
|
||||
console.log(actualHTML);
|
||||
throw error;
|
||||
}
|
||||
|
||||
} else if (utils.flag(this, 'shadowDom')) {
|
||||
const expectedHTML = getDiffableSemanticHTML(value);
|
||||
const actualHTML = getDiffableSemanticHTML(getCleanedShadowDom(this._obj));
|
||||
|
||||
// use chai's built-in string comparison, log the updated snapshot on error
|
||||
try {
|
||||
new chai.Assertion(actualHTML).to.equal(expectedHTML);
|
||||
} catch (error) {
|
||||
console.log('Snapshot changed, want to accept the change:');
|
||||
console.log('');
|
||||
console.log(actualHTML);
|
||||
throw error;
|
||||
}
|
||||
|
||||
} else {
|
||||
_super.apply(this, [value, ...args]);
|
||||
}
|
||||
|
||||
@@ -152,19 +152,19 @@ describe('getCleanedShadowDom', () => {
|
||||
});
|
||||
|
||||
describe('dom', () => {
|
||||
it('can strictly compare dom nodes', async () => {
|
||||
it('can compare dom nodes', async () => {
|
||||
const el = await fixture(`<div><!-- comment --><h1>${'Hey'} </h1> </div>`);
|
||||
expect(el).dom.to.equal('<div><!-- comment --><h1>Hey </h1> </div>');
|
||||
expect(el).dom.to.equal('<div><h1>Hey</h1></div>');
|
||||
});
|
||||
|
||||
it('can semmantically compare dom nodes', async () => {
|
||||
it('handles .semantically as backwards compatibility', async () => {
|
||||
const el = await fixture(`<div><!-- comment --><h1>${'Hey'} </h1> </div>`);
|
||||
expect(el).dom.to.semantically.equal('<div><h1>Hey</h1></div>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadowDom', () => {
|
||||
it('can strict compare shadow dom nodes', async () => {
|
||||
it('can compare shadow dom nodes', async () => {
|
||||
const tag = defineCE(
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
@@ -178,12 +178,11 @@ describe('shadowDom', () => {
|
||||
},
|
||||
);
|
||||
const el = await fixture(`<${tag}><span> light content </span></${tag}>`);
|
||||
expect(el).dom.to.semantically.equal(`<${tag}><span>light content</span></${tag}>`);
|
||||
expect(el).shadowDom.to.equal('<p> shadow content</p> <!-- comment --> <slot></slot>');
|
||||
expect(el).shadowDom.to.semantically.equal('<p>shadow content</p><slot>');
|
||||
expect(el).dom.to.equal(`<${tag}><span>light content</span></${tag}>`);
|
||||
expect(el).shadowDom.to.equal('<p>shadow content</p><slot>');
|
||||
});
|
||||
|
||||
it('can semmantically compare shadow dom nodes', async () => {
|
||||
it('supports .semantically as backwards compatibility', async () => {
|
||||
const tag = defineCE(
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
|
||||
2
packages/semantic-dom-diff/index.d.ts
vendored
2
packages/semantic-dom-diff/index.d.ts
vendored
@@ -1 +1 @@
|
||||
export { getSemanticDomDiff, getAST } from './src/get-dom-diff';
|
||||
export { getDiffableSemanticHTML, getAST } from './src/get-dom-diff';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { getSemanticDomDiff } from './src/get-dom-diff.js';
|
||||
export { getDiffableSemanticHTML } from './src/get-dom-diff.js';
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"test:es5:watch": "karma start karma.es5.config.js --auto-watch=true --single-run=false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bundled-es-modules/deep-diff": "^1.0.2-rc.1",
|
||||
"@bundled-es-modules/parse5": "^5.1.0",
|
||||
"@types/parse5": "^5.0.0"
|
||||
"@types/parse5": "^5.0.0",
|
||||
"diffable-html": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bundled-es-modules/chai": "^4.2.0",
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { isParentNode, isElement } from './parse5-utils.js';
|
||||
|
||||
/**
|
||||
* @param {ASTNode | ASTNode[]} root
|
||||
* @param {string[]} path
|
||||
*/
|
||||
export function findDiffedObject(root, path) {
|
||||
let node = root;
|
||||
|
||||
for (let i = 0; i < path.length; i += 1) {
|
||||
const step = path[i];
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
const intStep = parseFloat(step);
|
||||
if (Number.isInteger(intStep)) {
|
||||
node = node[intStep];
|
||||
} else {
|
||||
throw new Error(`Non-integer step: ${step} for array node.`);
|
||||
}
|
||||
} else if (step === 'childNodes') {
|
||||
if (isParentNode(node)) {
|
||||
node = node.childNodes;
|
||||
} else {
|
||||
throw new Error('Cannot read childNodes from non-parent node.');
|
||||
}
|
||||
} else if (step === 'attrs') {
|
||||
if (isElement(node)) {
|
||||
node = node.attrs;
|
||||
} else {
|
||||
throw new Error('Cannot read attributes from non-element node.');
|
||||
}
|
||||
} else {
|
||||
// For all other steps we don't walk further
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { isElement, isTextNode } from './parse5-utils.js';
|
||||
|
||||
export const isAttribute = arg => arg && 'name' in arg && 'value' in arg;
|
||||
const { isArray } = Array;
|
||||
|
||||
/**
|
||||
* @param {ASTNode | Attribute} arg
|
||||
* @returns {string} the human readable diff identifier
|
||||
*/
|
||||
function getIdentifier(arg) {
|
||||
if (isTextNode(arg)) {
|
||||
return `text "${arg.value}"`;
|
||||
}
|
||||
|
||||
if (isElement(arg)) {
|
||||
return `tag <${arg.tagName}>`;
|
||||
}
|
||||
|
||||
if (isAttribute(arg)) {
|
||||
return arg.value ? `attribute [${arg.name}="${arg.value}"]` : `attribute [${arg.name}]`;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown arg: ${arg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the diff is an array diff, returns type assertions to remove optional props.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isArrayDiff(diff) {
|
||||
return diff.kind === 'A' && !!diff.item && typeof diff.index === 'number';
|
||||
}
|
||||
|
||||
const messageTemplates = {
|
||||
// New diff
|
||||
N: (diff, lhs, rhs) => `${getIdentifier(rhs)} has been added`,
|
||||
// Edit diff
|
||||
E: (diff, lhs, rhs) => `${getIdentifier(lhs)} was changed to ${getIdentifier(rhs)}`,
|
||||
// Delete diff
|
||||
D: (diff, lhs) => `${getIdentifier(lhs)} has been removed`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a human understandable message for a HTML diff.
|
||||
*
|
||||
* @param {object} diff The diff
|
||||
* @param {object | object[]} lhs The left hand side diffed object.
|
||||
* Can be a HTML ASTNode or an Attribute.
|
||||
* @param {object | object[]} rhs The left hand side diffed object.
|
||||
* Can be a HTML ASTNode or an Attribute.
|
||||
*
|
||||
* @returns the message
|
||||
*/
|
||||
export function getDiffMessage(diff, lhs, rhs) {
|
||||
// Array diff
|
||||
if (isArray(lhs) || isArray(rhs)) {
|
||||
if (!isArrayDiff(diff) || !isArray(lhs) || !isArray(rhs)) {
|
||||
throw new Error('Not all arguments are array diffs');
|
||||
}
|
||||
|
||||
return getDiffMessage(diff.item, lhs[diff.index], rhs[diff.index]);
|
||||
}
|
||||
|
||||
// Non-array diff
|
||||
if (diff.kind in messageTemplates) {
|
||||
return messageTemplates[diff.kind](diff, lhs, rhs);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown diff kind: ${diff.kind}`);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { isParentNode, isElement } from './parse5-utils.js';
|
||||
|
||||
/**
|
||||
* @param {*} node The AST Node
|
||||
* @returns {string | null} the AST node name
|
||||
*/
|
||||
function getNodeName(node) {
|
||||
if (!isElement(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.attrs) {
|
||||
const idAttr = node.attrs.find(attr => attr.name === 'id');
|
||||
if (idAttr) {
|
||||
return `${node.nodeName}#${idAttr.value}`;
|
||||
}
|
||||
|
||||
const classAttr = node.attrs.find(attr => attr.name === 'class');
|
||||
if (classAttr) {
|
||||
return `${node.nodeName}.${classAttr.value.split(' ').join('.')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return node.nodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode | ASTNode[]} root the root to walk from
|
||||
* @param {string[]} path the full path to the dom element
|
||||
* @returns {string[]} the human readable path to a dom element
|
||||
*/
|
||||
export function getDiffPath(root, path) {
|
||||
const names = [];
|
||||
let node = root;
|
||||
|
||||
for (let i = 0; i < path.length; i += 1) {
|
||||
const step = path[i];
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
const intStep = parseFloat(step);
|
||||
if (Number.isInteger(intStep)) {
|
||||
node = node[intStep];
|
||||
} else {
|
||||
throw new Error(`Non-integer step: ${step} for array node.`);
|
||||
}
|
||||
} else if (step === 'childNodes') {
|
||||
if (isParentNode(node)) {
|
||||
node = node.childNodes;
|
||||
} else {
|
||||
throw new Error('Cannot read childNodes from non-parent node.');
|
||||
}
|
||||
} else {
|
||||
// Break loop if we end up at a type of path section we don't want
|
||||
// walk further into
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Array.isArray(node)) {
|
||||
const name = getNodeName(node);
|
||||
if (name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names.join(' > ');
|
||||
}
|
||||
@@ -1,11 +1,4 @@
|
||||
import { DocumentFragment } from 'parse5';
|
||||
|
||||
interface DiffResult {
|
||||
message: String;
|
||||
path: String;
|
||||
normalizedLeftHTML: String;
|
||||
normalizedRightHTML: String;
|
||||
}
|
||||
|
||||
export function getAST(value, config): DocumentFragment
|
||||
export function getSemanticDomDiff(leftHTML, rightHTML, config): DiffResult
|
||||
export function getDiffableSemanticHTML(html: string): string
|
||||
|
||||
@@ -1,38 +1,8 @@
|
||||
import { parseFragment, serialize } from '@bundled-es-modules/parse5';
|
||||
import { deepDiff } from '@bundled-es-modules/deep-diff';
|
||||
import toDiffableHtml from 'diffable-html';
|
||||
|
||||
import { sanitizeHtmlString } from './sanitize-html-string.js';
|
||||
import { normalizeAST } from './normalize-ast.js';
|
||||
import { getDiffMessage } from './get-diff-message.js';
|
||||
import { findDiffedObject } from './find-diffed-object.js';
|
||||
import { getDiffPath } from './get-diff-path.js';
|
||||
|
||||
/**
|
||||
* @typedef {object} DiffResult
|
||||
* @param {string} message
|
||||
* @param {string} path
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates the DiffResult for two AST trees.
|
||||
*
|
||||
* @param {ASTNode} leftTree the left tree AST
|
||||
* @param {ASTNode} rightTree the right tree AST
|
||||
* @param {Object} diff the semantic difference between the two trees
|
||||
*
|
||||
* @returns {DiffResult} the diff result containing the human readable semantic difference
|
||||
*/
|
||||
function createDiffResult(leftTree, rightTree, diff) {
|
||||
const leftDiffObject = findDiffedObject(leftTree, diff.path);
|
||||
const rightDiffObject = findDiffedObject(rightTree, diff.path);
|
||||
|
||||
return {
|
||||
message: getDiffMessage(diff, leftDiffObject, rightDiffObject),
|
||||
path: getDiffPath(leftTree, diff.path),
|
||||
normalizedLeftHTML: serialize(leftTree),
|
||||
normalizedRightHTML: serialize(rightTree),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAST(value, config = {}) {
|
||||
const ast = parseFragment(sanitizeHtmlString(value));
|
||||
@@ -41,29 +11,18 @@ export function getAST(value, config = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses two HTML trees, and generates the semantic difference between the two trees.
|
||||
* The HTML is diffed semantically, not literally. This means that changes in attribute
|
||||
* and class order and whitespace/newlines are ignored. Also, script and style
|
||||
* tags ignored.
|
||||
* Parses HTML and returns HTML in a formatted and diffable format which is semantically
|
||||
* equal to the input, but with some transformations:
|
||||
* - whitespace and newlines in and around tags are normalized
|
||||
* - classes and attributes are ordered
|
||||
* - script, style and comments are removed
|
||||
*
|
||||
* @param leftHTML the left HTML tree
|
||||
* @param rightHTML the right HTML tree
|
||||
* @returns {DiffResult | null} the diff result, or undefined if no diffs were found
|
||||
* @param {string} html
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getSemanticDomDiff(leftHTML, rightHTML, config = {}) {
|
||||
const leftTree = getAST(leftHTML);
|
||||
const rightTree = getAST(rightHTML);
|
||||
|
||||
normalizeAST(leftTree, config.ignoredTags);
|
||||
normalizeAST(rightTree, config.ignoredTags);
|
||||
|
||||
// parentNode causes a circular reference, so ignore them.
|
||||
const ignore = (path, key) => key === 'parentNode';
|
||||
const diffs = deepDiff(leftTree, rightTree, ignore);
|
||||
|
||||
if (!diffs || !diffs.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createDiffResult(leftTree, rightTree, diffs[0]);
|
||||
export function getDiffableSemanticHTML(html) {
|
||||
const ast = getAST(html);
|
||||
normalizeAST(ast, []);
|
||||
const serialized = serialize(ast);
|
||||
return toDiffableHtml(serialized);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
export const isNode = arg => arg && 'nodeName' in arg;
|
||||
export const isElement = arg => arg && 'tagName' in arg;
|
||||
export const isParentNode = arg => arg && 'childNodes' in arg;
|
||||
export const isTextNode = arg => arg && arg.nodeName === '#text';
|
||||
export const isCommentNode = arg => arg && arg.nodeName === '#comment';
|
||||
export const isDocumentFragment = arg => arg && arg.nodeName === '#document-fragment';
|
||||
|
||||
@@ -1,93 +1,81 @@
|
||||
import { expect } from '@bundled-es-modules/chai';
|
||||
import { getSemanticDomDiff } from '../index.js';
|
||||
import { getDiffableSemanticHTML } from '../index.js';
|
||||
import largeTemplate from './large-template';
|
||||
|
||||
describe('getSemanticDomDiff()', () => {
|
||||
describe('diffs', () => {
|
||||
describe('element', () => {
|
||||
it('changed element', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<span></span>');
|
||||
const htmlA = getDiffableSemanticHTML('<div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<span></span>');
|
||||
|
||||
expect(diff.message).to.equal('tag <div> was changed to tag <span>');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('added element', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<div></div><div></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div><div></div>');
|
||||
|
||||
expect(diff.message).to.equal('tag <div> has been added');
|
||||
expect(diff.path).to.equal('');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('removed element', () => {
|
||||
const diff = getSemanticDomDiff('<div></div><div></div>', '<div></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div></div><div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
|
||||
expect(diff.message).to.equal('tag <div> has been removed');
|
||||
expect(diff.path).to.equal('');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attributes', () => {
|
||||
it('changed attribute', () => {
|
||||
const diff = getSemanticDomDiff('<div foo="bar"></div>', '<div foo="baz"></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div foo="baz"></div>');
|
||||
|
||||
expect(diff.message).to.equal('attribute [foo="bar"] was changed to attribute [foo="baz"]');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('added attribute', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<div foo="bar"></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
|
||||
expect(diff.message).to.equal('attribute [foo="bar"] has been added');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('removed attribute', () => {
|
||||
const diff = getSemanticDomDiff('<div foo="bar"></div>', '<div></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
|
||||
expect(diff.message).to.equal('attribute [foo="bar"] has been removed');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
it('changed text', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div>bar</div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>bar</div>');
|
||||
|
||||
expect(diff.message).to.equal('text "foo" was changed to text "bar"');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('removed text', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div></div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
|
||||
expect(diff.message).to.equal('text "foo" has been removed');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('added text', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<div>foo</div>');
|
||||
const htmlA = getDiffableSemanticHTML('<div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>foo</div>');
|
||||
|
||||
expect(diff.message).to.equal('text "foo" has been added');
|
||||
expect(diff.path).to.equal('div');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple diffs', () => {
|
||||
it('returns the first diff', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div>foo</div><div foo="bar"></div>',
|
||||
'<div>bar</div><span foo="baz"></span>',
|
||||
);
|
||||
|
||||
expect(diff.message).to.equal('tag <div> was changed to tag <span>');
|
||||
expect(diff.path).to.equal('div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deep changes', () => {
|
||||
it('element changes', () => {
|
||||
const a = `
|
||||
const htmlA = getDiffableSemanticHTML(`
|
||||
<div>
|
||||
<div id="foo">
|
||||
<div>
|
||||
@@ -99,8 +87,9 @@ describe('getSemanticDomDiff()', () => {
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
const b = `
|
||||
`);
|
||||
|
||||
const htmlB = getDiffableSemanticHTML(`
|
||||
<div>
|
||||
<div id="foo">
|
||||
<div>
|
||||
@@ -112,15 +101,13 @@ describe('getSemanticDomDiff()', () => {
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
const diff = getSemanticDomDiff(a, b);
|
||||
`);
|
||||
|
||||
expect(diff.message).to.equal('tag <div> was changed to tag <span>');
|
||||
expect(diff.path).to.equal('div > div#foo > div > div.foo > div.bar.baz.foo');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
|
||||
it('attribute changes', () => {
|
||||
const a = `
|
||||
const htmlA = getDiffableSemanticHTML(`
|
||||
<div>
|
||||
<div id="foo">
|
||||
<div>
|
||||
@@ -132,8 +119,9 @@ describe('getSemanticDomDiff()', () => {
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
const b = `
|
||||
`);
|
||||
|
||||
const htmlB = getDiffableSemanticHTML(`
|
||||
<div>
|
||||
<div id="foo">
|
||||
<div>
|
||||
@@ -145,11 +133,9 @@ describe('getSemanticDomDiff()', () => {
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
const diff = getSemanticDomDiff(a, b);
|
||||
`);
|
||||
|
||||
expect(diff.message).to.equal('attribute [foo="bar"] has been added');
|
||||
expect(diff.path).to.equal('div > div#foo > div > div');
|
||||
expect(htmlA).to.not.equal(htmlB);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -157,184 +143,143 @@ describe('getSemanticDomDiff()', () => {
|
||||
describe('equality', () => {
|
||||
describe('simple', () => {
|
||||
it('element', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<div></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('attribute', () => {
|
||||
const diff = getSemanticDomDiff('<div foo="bar"></div>', '<div foo="bar"></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('text', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div>foo</div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>foo</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex', () => {
|
||||
it('large template', () => {
|
||||
const diff = getSemanticDomDiff(largeTemplate, largeTemplate);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
});
|
||||
|
||||
it('self closing tags', () => {
|
||||
const diff = getSemanticDomDiff('<div><br><hr /></div>', '<div><br /><hr></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML(largeTemplate);
|
||||
const htmlB = getDiffableSemanticHTML(largeTemplate);
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ordering', () => {
|
||||
it('attributes order', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div a="1" b="2" c="3"></div>',
|
||||
'<div c="3" a="1" b="2"></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div a="1" b="2" c="3"></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div c="3" a="1" b="2"></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('class order', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div class="foo bar"></div>',
|
||||
'<div class="bar foo"></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div class="foo bar"></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div class="bar foo"></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespace', () => {
|
||||
it('trailing whitespace in attributes', () => {
|
||||
const diff = getSemanticDomDiff('<div foo="bar" ></div>', '<div foo="bar"></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div foo="bar" ></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div foo="bar"></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('trailing whitespace in class', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div class="foo bar "></div>',
|
||||
'<div class="foo bar "></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div class="foo bar "></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div class="foo bar "></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('whitespace between classes', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div class="foo bar "></div>',
|
||||
'<div class="foo bar"></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div class="foo bar "></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div class="foo bar"></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('whitespace before and after template', () => {
|
||||
const diff = getSemanticDomDiff(' <div></div> ', '<div></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML(' <div></div> ');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('whitespace in between nodes', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div> </div> foo <div> </div>',
|
||||
'<div></div>foo<div></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div> </div> foo <div> </div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>foo<div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('whitespace around text nodes', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div> foo </div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div> foo </div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
it('tabs before and after template', () => {
|
||||
const diff = getSemanticDomDiff('\t\t<div></div>\t', '<div></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('\t\t<div></div>\t');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('tabs in between nodes', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div>\t<div></div>\t \t \t</div>',
|
||||
'<div><div></div></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>\t<div></div>\t \t \t</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div><div></div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('tabs around text nodes', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div>\tfoo\t</div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>\tfoo\t</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newlines', () => {
|
||||
it('newlines before and after template', () => {
|
||||
const diff = getSemanticDomDiff('\n\n<div></div>\n', '<div></div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('\n\n<div></div>\n');
|
||||
const htmlB = getDiffableSemanticHTML('<div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('newlines in between nodes', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div>\n<div></div>\n \n \n</div>',
|
||||
'<div><div></div></div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>\n<div></div>\n \n \n</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div><div></div></div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('newlines around text nodes', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo</div>', '<div>\n\n\nfoo\n</div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo</div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>\n\n\nfoo\n</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtered nodes', () => {
|
||||
it('comments', () => {
|
||||
const diff = getSemanticDomDiff('<div>foo<!-- comment --></div>', '<div>foo</div>');
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo<!-- comment --></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>foo</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('styles', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
const htmlA = getDiffableSemanticHTML(
|
||||
'<div>foo<style> .foo { color: blue; } </style></div>',
|
||||
'<div>foo</div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlB = getDiffableSemanticHTML('<div>foo</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
|
||||
it('script', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div>foo<script>console.log("foo");</script></div>',
|
||||
'<div>foo</div>',
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
});
|
||||
|
||||
it('ignored tags', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
'<div><span>foo</span></div>',
|
||||
'<div><span>bar</span></div>',
|
||||
{ ignoredTags: ['span'] },
|
||||
);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML('<div>foo<script>console.log("foo");</script></div>');
|
||||
const htmlB = getDiffableSemanticHTML('<div>foo</div>');
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -354,35 +299,10 @@ bar
|
||||
</div>
|
||||
<div></div>
|
||||
`;
|
||||
const diff = getSemanticDomDiff(a, b);
|
||||
|
||||
expect(diff).to.equal(null);
|
||||
const htmlA = getDiffableSemanticHTML(a);
|
||||
const htmlB = getDiffableSemanticHTML(b);
|
||||
expect(htmlA).to.equal(htmlB);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('values', () => {
|
||||
it('handles strings', () => {
|
||||
const diff = getSemanticDomDiff('<div></div>', '<span></span>');
|
||||
|
||||
expect(diff.message).to.equal('tag <div> was changed to tag <span>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff tree', () => {
|
||||
it('returns the left and right side normalized trees', () => {
|
||||
const diff = getSemanticDomDiff(
|
||||
`
|
||||
<div id="foo"> <div> <div class="foo bar "> <div>
|
||||
</div> </div>
|
||||
`,
|
||||
'<span></span>',
|
||||
);
|
||||
|
||||
expect(diff.normalizedLeftHTML).to.equal(
|
||||
'<div id="foo"><div><div class="bar foo"><div></div></div></div></div>',
|
||||
);
|
||||
expect(diff.normalizedRightHTML).to.equal('<span></span>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user