@swimlane/cy-dom-diff
Advanced tools
Comparing version 0.0.0 to 0.0.1
@@ -1,21 +0,23 @@ | ||
import { clean, diff, disambiguateArgs, getDom } from './lib/util'; | ||
import { clean, disambiguateArgs, getDom } from './lib/util'; | ||
import { chaiDomMatch } from './lib/assertion'; | ||
chai.use(chaiDomMatch); | ||
function logDiff(subject, re, options) { | ||
if (!re.rec) { | ||
function logDiff(name, state, $el, re, options) { | ||
if (!re.pattern) { | ||
throw new Error(`Cannot generate a diff against ${re}`); | ||
} | ||
const a = clean(getDom(subject), options); | ||
const d = diff(a, re.rec(a)); | ||
const Actual = clean(getDom($el), options); | ||
const Expected = re.replace(re.pattern); | ||
const Difference = re.diff(Actual); | ||
Cypress.log({ | ||
name: 'Dom Diff', | ||
displayName: 'Dom Diff', | ||
name, | ||
message: 'DOM Difference', | ||
$el, | ||
// @ts-ignore | ||
state: 'failed', | ||
state, | ||
consoleProps: () => { | ||
return { | ||
Pattern: re.pattern, | ||
Regexp: re, | ||
Actual: a, | ||
Difference: d, | ||
Subject: $el, | ||
Expected, | ||
Actual, | ||
Difference, | ||
}; | ||
@@ -25,3 +27,5 @@ }, | ||
} | ||
Cypress.Commands.add('domDiff', { prevSubject: 'element' }, logDiff); | ||
Cypress.Commands.add('domDiff', { prevSubject: 'element' }, ($el, re, options) => { | ||
logDiff('domDiff', 'passed', $el, re, options); | ||
}); | ||
Cypress.Commands.add('domMatch', { prevSubject: 'element' }, (subject, re, ...args) => { | ||
@@ -34,5 +38,6 @@ const [message, options] = disambiguateArgs(args); | ||
catch (e) { | ||
// this is a hack to only show the log after all retries have failed | ||
setTimeout(() => { | ||
if (!e.onFail) { | ||
logDiff(subject, re); | ||
logDiff('domMatch', 'failed', subject, re, options); | ||
} | ||
@@ -42,4 +47,4 @@ }); | ||
} | ||
logDiff(subject, re); | ||
logDiff('domMatch', 'passed', subject, re, options); | ||
}); | ||
}); |
@@ -15,4 +15,3 @@ import { getDom, clean, disambiguateArgs } from './util'; | ||
} | ||
; | ||
chai.Assertion.addMethod('domMatch', assertDomMatch); | ||
}; |
import { clean } from './util'; | ||
import { createPatch } from 'diff'; | ||
function escape(source) { | ||
@@ -7,21 +8,42 @@ if (source instanceof RegExp) | ||
} | ||
function formatPatch(text) { | ||
return text | ||
.replace(/^([^\n]+)\n([^\n]+)\n/m, '') | ||
.replace(/--- \t\n/g, '') // headers | ||
.replace(/\+\+\+ \t\n/g, '') | ||
.split('\n') | ||
.filter((x) => !x.includes('--- removed')) // Explanation | ||
.filter((x) => !x.includes('+++ added')) | ||
.filter((x) => !x.includes('@@ ')) | ||
.filter((x) => !x.includes('No newline at end of file')) | ||
.join('\n') | ||
.replace(/\n+$/, '\n') | ||
.trim(); | ||
} | ||
export class PatternRegExp extends RegExp { | ||
constructor(source, pattern, template, matchers) { | ||
constructor(source, pattern, matchers) { | ||
super(source); | ||
this.pattern = pattern; | ||
this.template = template; | ||
this.matchers = matchers; | ||
this.rec = function (b) { | ||
let pattern = this.pattern; | ||
const m = b.match(this.template); | ||
if (m) { | ||
m.shift(); | ||
m.forEach((v, i) => { | ||
const s = this.matchers[i]; | ||
const r = new RegExp(`^${s}$`); | ||
pattern = pattern.replace(`__arg${i}__`, r.test(v) ? v : `\${/${s}/}`); | ||
}); | ||
this.compare = function (left, right) { | ||
if (right.includes('__arg')) { | ||
const source = this.matchers.reduce((acc, m, i) => { | ||
return acc.replace(`__arg${i}__`, escape(m)); | ||
}, escape(right)); | ||
return new RegExp(source).test(left); | ||
} | ||
return pattern; | ||
return left === right; | ||
}; | ||
this.replace = function (str) { | ||
return this.matchers.reduce((acc, m, i) => { | ||
return acc.replace(`__arg${i}__`, `\${${m}}`); | ||
}, str); | ||
}; | ||
this.diff = function (str) { | ||
const options = { | ||
comparator: (l, r) => this.compare(l, r), | ||
}; | ||
const patch = createPatch('', this.pattern, str, '', '', options); | ||
return formatPatch(this.replace(patch)); | ||
}; | ||
} | ||
@@ -35,10 +57,6 @@ } | ||
const pattern = clean(result.join('')); | ||
let source = escape(pattern); | ||
let s0 = source; | ||
const matchers = args.map((arg, i) => { | ||
source = source.replace(`__arg${i}__`, escape(arg)); | ||
s0 = s0.replace(`__arg${i}__`, '(.*)'); | ||
return arg.source || arg; | ||
}); | ||
return new PatternRegExp(`^${source}$`, pattern, new RegExp(s0), matchers); | ||
const source = args.reduce((acc, arg, i) => { | ||
return acc.replace(`__arg${i}__`, escape(arg)); | ||
}, escape(pattern)); | ||
return new PatternRegExp(`^${source}$`, pattern, args); | ||
} |
@@ -1,3 +0,3 @@ | ||
import disparity from 'disparity'; | ||
import { getDiffableHTML } from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
import { getDiffableHTML, } from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
import unindent from 'strip-indent'; | ||
const { isJquery, isElement } = Cypress.dom; | ||
@@ -11,29 +11,17 @@ export function getDom($el) { | ||
} | ||
return $el; // TODO: errror | ||
return $el; // TODO: errror? | ||
} | ||
const domparser = new DOMParser(); | ||
export function clean(html, options) { | ||
// const parser = new DOMParser(); | ||
// const doc = parser.parseFromString(html, 'text/html'); | ||
// return getDiffableHTML(doc.body).replace(/\s+/g, '\n').trim() | ||
// return doc.body.innerHTML.replace(/\s+/g, '\n').trim(); | ||
return getDiffableHTML(html, options); | ||
// Create a Node using DOMParser to avoid rendering | ||
const doc = domparser.parseFromString(`<diff-container>${html}</diff-container>`, 'text/html'); | ||
return unindent(getDiffableHTML(doc.body.firstChild, options)); | ||
} | ||
function removeExplanation(text) { | ||
return text | ||
.split('\n') | ||
.filter((x) => !x.includes('--- removed')) | ||
.filter((x) => !x.includes('+++ added')) | ||
.filter((x) => !x.includes('@@ ')) | ||
.filter((x) => !x.includes('No newline at end of file')) | ||
.join('\n') | ||
.replace(/\n+$/, '\n'); | ||
} | ||
export function diff(a, b) { | ||
const textDiff = disparity.unifiedNoColor(a, b, {}); | ||
return removeExplanation(textDiff); | ||
} | ||
export function disambiguateArgs(args) { | ||
// @ts-ignore | ||
return args.length === 2 ? args : | ||
(typeof args[0] === 'object' ? [undefined, args[0]] : [args[0], undefined]); | ||
if (args.length === 2) { | ||
return args; | ||
} | ||
return typeof args[0] === 'object' | ||
? [undefined, args[0]] | ||
: [args[0], undefined]; | ||
} |
{ | ||
"name": "@swimlane/cy-dom-diff", | ||
"version": "0.0.0", | ||
"version": "0.0.1", | ||
"description": "matching chunks of DOM against HTML; including dynamic content.", | ||
@@ -26,3 +26,4 @@ "main": "index.js", | ||
"np": "np && npm run clean", | ||
"prepack": "npm run build" | ||
"prepack": "npm run build", | ||
"prepublishOnly": "npm run build" | ||
}, | ||
@@ -33,7 +34,7 @@ "author": "J. Harshbarger", | ||
"@open-wc/semantic-dom-diff": "^0.18.0", | ||
"diff-dom": "^4.2.0", | ||
"disparity": "^3.1.0", | ||
"typescript": "^4.0.3" | ||
"diff": "^4.0.2", | ||
"strip-indent": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/diff": "^4.0.2", | ||
"@types/faker": "^5.1.2", | ||
@@ -48,3 +49,4 @@ "chg": "^0.4.0", | ||
"tslint": "^6.1.3", | ||
"tslint-config-prettier": "^1.18.0" | ||
"tslint-config-prettier": "^1.18.0", | ||
"typescript": "^4.0.3" | ||
}, | ||
@@ -51,0 +53,0 @@ "peerDependencies": { |
# cy-dom-diff | ||
`cy-dom-diff` allows matching chunks of DOM against HTML; including dynamic content. `cy-dom-diff` consist of two parts. First is a Cypress command that matches a DOM element against a regular expression. When a DOM element is matched its HTML is normalized to produce consistent diffable output while maintaining sematic meaning. This includes: | ||
`cy-dom-diff` allows matching chunks of DOM against HTML; including dynamic content. | ||
## Introduction | ||
`cy-dom-diff` consist of two parts. First is a Cypress command that matches a DOM element against a regular expression. When a DOM element is matched its HTML is normalized to produce consistent diffable output while maintaining sematic meaning. This includes: | ||
* whitespace and newlines are normalized | ||
@@ -43,2 +47,4 @@ * tags and attributes are printed on individual lines | ||
![](diff.png) | ||
### `cy.domDiff` Cypress command | ||
@@ -45,0 +51,0 @@ |
import { DiffOptions } from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
import { PatternRegExp } from './lib/matchers'; | ||
import { clean, diff, disambiguateArgs, getDom } from './lib/util'; | ||
import { clean, disambiguateArgs, getDom } from './lib/util'; | ||
import { chaiDomMatch } from './lib/assertion'; | ||
@@ -9,23 +9,33 @@ | ||
type Options = Partial<Cypress.Loggable & Cypress.Timeoutable & DiffOptions> | undefined | ||
type Options = | ||
| Partial<Cypress.Loggable & Cypress.Timeoutable & DiffOptions> | ||
| undefined; | ||
function logDiff(subject: any, re: PatternRegExp, options?: Options) { | ||
if (!re.rec) { | ||
function logDiff( | ||
name: string, | ||
state: string, | ||
$el: any, | ||
re: PatternRegExp, | ||
options?: Options | ||
) { | ||
if (!re.pattern) { | ||
throw new Error(`Cannot generate a diff against ${re}`); | ||
} | ||
const a = clean(getDom(subject), options); | ||
const d = diff(a, re.rec(a)); | ||
const Actual = clean(getDom($el), options); | ||
const Expected = re.replace(re.pattern); | ||
const Difference = re.diff(Actual); | ||
Cypress.log({ | ||
name: 'Dom Diff', | ||
displayName: 'Dom Diff', | ||
name, | ||
message: 'DOM Difference', | ||
$el, | ||
// @ts-ignore | ||
state: 'failed', | ||
state, | ||
consoleProps: () => { | ||
return { | ||
Pattern: re.pattern, | ||
Regexp: re, | ||
Actual: a, | ||
Difference: d, | ||
Subject: $el, | ||
Expected, | ||
Actual, | ||
Difference, | ||
}; | ||
@@ -36,3 +46,9 @@ }, | ||
Cypress.Commands.add('domDiff', { prevSubject: 'element' }, logDiff); | ||
Cypress.Commands.add( | ||
'domDiff', | ||
{ prevSubject: 'element' }, | ||
($el: any, re: PatternRegExp, options?: Options) => { | ||
logDiff('domDiff', 'passed', $el, re, options); | ||
} | ||
); | ||
@@ -43,3 +59,6 @@ Cypress.Commands.add( | ||
(subject: any, re: PatternRegExp, ...args: [string | Options, Options]) => { | ||
const [message, options] = disambiguateArgs(args as any) as [string | undefined, Options | undefined]; | ||
const [message, options] = disambiguateArgs(args as any) as [ | ||
string | undefined, | ||
Options | undefined | ||
]; | ||
@@ -50,5 +69,6 @@ cy.wrap(subject, options).should((el: any) => { | ||
} catch (e) { | ||
// this is a hack to only show the log after all retries have failed | ||
setTimeout(() => { | ||
if (!e.onFail) { | ||
logDiff(subject, re); | ||
logDiff('domMatch', 'failed', subject, re, options); | ||
} | ||
@@ -59,5 +79,5 @@ }); | ||
logDiff(subject, re); | ||
logDiff('domMatch', 'passed', subject, re, options); | ||
}); | ||
} | ||
); |
@@ -6,3 +6,7 @@ import { DiffOptions } from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
export const chaiDomMatch = (chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) => { | ||
function assertDomMatch(this: Chai.AssertionStatic, re: RegExp, ...args: [string | DiffOptions, DiffOptions]) { | ||
function assertDomMatch( | ||
this: Chai.AssertionStatic, | ||
re: RegExp, | ||
...args: [string | DiffOptions, DiffOptions] | ||
) { | ||
const [message, options] = disambiguateArgs(args) as [string, DiffOptions]; | ||
@@ -20,5 +24,5 @@ | ||
} | ||
}; | ||
} | ||
chai.Assertion.addMethod('domMatch', assertDomMatch); | ||
}; |
import { clean } from './util'; | ||
import { createPatch } from 'diff'; | ||
@@ -8,2 +9,17 @@ function escape(source: RegExp | string) { | ||
function formatPatch(text: string) { | ||
return text | ||
.replace(/^([^\n]+)\n([^\n]+)\n/m, '') | ||
.replace(/--- \t\n/g, '') // headers | ||
.replace(/\+\+\+ \t\n/g, '') | ||
.split('\n') | ||
.filter((x) => !x.includes('--- removed')) // Explanation | ||
.filter((x) => !x.includes('+++ added')) | ||
.filter((x) => !x.includes('@@ ')) | ||
.filter((x) => !x.includes('No newline at end of file')) | ||
.join('\n') | ||
.replace(/\n+$/, '\n') | ||
.trim(); | ||
} | ||
export class PatternRegExp extends RegExp { | ||
@@ -13,3 +29,2 @@ constructor( | ||
public readonly pattern: string, | ||
public readonly template: RegExp, | ||
public readonly matchers: any[] | ||
@@ -20,18 +35,32 @@ ) { | ||
rec = function (this: PatternRegExp, b: string) { | ||
let pattern = this.pattern; | ||
const m = b.match(this.template); | ||
if (m) { | ||
m.shift(); | ||
m.forEach((v, i) => { | ||
const s = this.matchers[i]; | ||
const r = new RegExp(`^${s}$`); | ||
pattern = pattern.replace(`__arg${i}__`, r.test(v) ? v : `\${/${s}/}`); | ||
}); | ||
compare = function (this: PatternRegExp, left: string, right: string) { | ||
if (right.includes('__arg')) { | ||
const source = this.matchers.reduce((acc, m, i) => { | ||
return acc.replace(`__arg${i}__`, escape(m)); | ||
}, escape(right)); | ||
return new RegExp(source).test(left); | ||
} | ||
return pattern; | ||
return left === right; | ||
}; | ||
replace = function (this: PatternRegExp, str: string) { | ||
return this.matchers.reduce((acc, m, i) => { | ||
return acc.replace(`__arg${i}__`, `\${${m}}`); | ||
}, str); | ||
}; | ||
diff = function (this: PatternRegExp, str: string) { | ||
const options = { | ||
comparator: (l: string, r: string) => this.compare(l, r), | ||
}; | ||
const patch = createPatch('', this.pattern, str, '', '', options as any); | ||
return formatPatch(this.replace(patch)); | ||
}; | ||
} | ||
export function html(strings: TemplateStringsArray, ...args: any[]): PatternRegExp { | ||
export function html( | ||
strings: TemplateStringsArray, | ||
...args: any[] | ||
): PatternRegExp { | ||
const result = [strings[0]]; | ||
@@ -42,10 +71,6 @@ args.forEach((arg, i) => { | ||
const pattern = clean(result.join('')); | ||
let source = escape(pattern); | ||
let s0 = source; | ||
const matchers = args.map((arg, i) => { | ||
source = source.replace(`__arg${i}__`, escape(arg)); | ||
s0 = s0.replace(`__arg${i}__`, '(.*)'); | ||
return arg.source || arg; | ||
}); | ||
return new PatternRegExp(`^${source}$`, pattern, new RegExp(s0), matchers); | ||
const source = args.reduce((acc, arg, i) => { | ||
return acc.replace(`__arg${i}__`, escape(arg)); | ||
}, escape(pattern)); | ||
return new PatternRegExp(`^${source}$`, pattern, args); | ||
} |
@@ -1,4 +0,8 @@ | ||
import disparity from 'disparity'; | ||
import { DiffOptions, getDiffableHTML } from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
import { | ||
DiffOptions, | ||
getDiffableHTML, | ||
} from '@open-wc/semantic-dom-diff/get-diffable-html'; | ||
import unindent from 'strip-indent'; | ||
const { isJquery, isElement } = Cypress.dom; | ||
@@ -13,33 +17,25 @@ | ||
} | ||
return $el; // TODO: errror | ||
return $el; // TODO: errror? | ||
} | ||
export function clean(html: string, options?: DiffOptions) { | ||
// const parser = new DOMParser(); | ||
// const doc = parser.parseFromString(html, 'text/html'); | ||
// return getDiffableHTML(doc.body).replace(/\s+/g, '\n').trim() | ||
// return doc.body.innerHTML.replace(/\s+/g, '\n').trim(); | ||
return getDiffableHTML(html, options); | ||
} | ||
const domparser = new DOMParser(); | ||
function removeExplanation(text: string) { | ||
return text | ||
.split('\n') | ||
.filter((x) => !x.includes('--- removed')) | ||
.filter((x) => !x.includes('+++ added')) | ||
.filter((x) => !x.includes('@@ ')) | ||
.filter((x) => !x.includes('No newline at end of file')) | ||
.join('\n') | ||
.replace(/\n+$/, '\n'); | ||
export function clean(html: string, options?: DiffOptions): string { | ||
// Create a Node using DOMParser to avoid rendering | ||
const doc = domparser.parseFromString( | ||
`<diff-container>${html}</diff-container>`, | ||
'text/html' | ||
); | ||
return unindent(getDiffableHTML(doc.body.firstChild as Node, options)); | ||
} | ||
export function diff(a: string, b: string) { | ||
const textDiff = disparity.unifiedNoColor(a, b, {}); | ||
return removeExplanation(textDiff); | ||
export function disambiguateArgs( | ||
args: [string | object | undefined, object | undefined] | ||
): [string | undefined, object | undefined] { | ||
if (args.length === 2) { | ||
return args as [string | undefined, object | undefined]; | ||
} | ||
return typeof args[0] === 'object' | ||
? [undefined, args[0]] | ||
: [args[0], undefined]; | ||
} | ||
export function disambiguateArgs(args: [string | object, object]): [string | undefined, object | undefined] { | ||
// @ts-ignore | ||
return args.length === 2 ? args : | ||
(typeof args[0] === 'object' ? [undefined, args[0]] : [args[0], undefined]); | ||
} |
declare namespace Chai { | ||
type DiffOptions = import('@open-wc/semantic-dom-diff/get-diffable-html').DiffOptions; | ||
interface Assertion { | ||
@@ -8,2 +8,2 @@ domMatch(re: RegExp, message?: string | DiffOptions): Assertion; | ||
} | ||
} | ||
} |
@@ -5,10 +5,12 @@ /// <reference types="cypress" /> | ||
type DiffOptions = import('@open-wc/semantic-dom-diff/get-diffable-html').DiffOptions; | ||
type Options = Partial<Cypress.Loggable & Cypress.Timeoutable & DiffOptions> | undefined | ||
type Options = | ||
| Partial<Cypress.Loggable & Cypress.Timeoutable & DiffOptions> | ||
| undefined; | ||
interface Chainable<Subject> { | ||
domDiff(re: RegExp, options: Options): Chainable<any> | ||
domDiff(re: RegExp, options: Options): Chainable<any>; | ||
domMatch(re: RegExp, message: string | Options): Chainable<any> | ||
domMatch(re: RegExp, message?: string, options?: Options): Chainable<any> | ||
domMatch(re: RegExp, message: string | Options): Chainable<any>; | ||
domMatch(re: RegExp, message?: string, options?: Options): Chainable<any>; | ||
} | ||
} |
@@ -5,2 +5,2 @@ /// <reference path="./chai-dom.d.ts" /> | ||
export * from '../lib/regexps'; | ||
export * from '../lib/matchers'; | ||
export * from '../lib/matchers'; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17350
4
18
372
82
12
+ Addeddiff@^4.0.2
+ Addedstrip-indent@^3.0.0
+ Addedmin-indent@1.0.1(transitive)
+ Addedstrip-indent@3.0.0(transitive)
- Removeddiff-dom@^4.2.0
- Removeddisparity@^3.1.0
- Removedtypescript@^4.0.3
- Removeddiff-dom@4.2.8(transitive)
- Removeddisparity@3.2.0(transitive)
- Removedtypescript@4.9.5(transitive)