UNPKG

7.98 kBJavaScriptView Raw
1/* eslint-disable no-param-reassign */
2
3import { getDiffableHTML, isDiffOptions } from './get-diffable-html.js';
4import { getOuterHtml, getCleanedShadowDom, snapshotPath } from './src/utils.js';
5
6/** @typedef {import('./get-diffable-html.js').DiffOptions} DiffOptions */
7
8function disambiguateArgs(...args) {
9 switch (args.length) {
10 // equal<T>(actual: T, expected: T, message?: string, options?: DiffOptions): void;
11 case 2: {
12 const [message, options] = args;
13 return { message, options };
14 }
15
16 // equal<T>(actual: T, expected: T, message?: string): void;
17 // equal<T>(actual: T, expected: T, options?: DiffOptions): void;
18 case 1: {
19 const [first] = args;
20 return isDiffOptions(first) ? { options: first } : { message: first };
21 }
22
23 default:
24 return {};
25 }
26}
27
28/**
29 * @type {Chai.ChaiPlugin}
30 */
31export const chaiDomDiff = (chai, utils) => {
32 /**
33 * can not be an arrow function as it gets rebound by chai
34 */
35 chai.Assertion.addProperty('lightDom', function lightDom() {
36 new chai.Assertion(this._obj.nodeType).to.equal(1);
37 utils.flag(this, 'lightDom', true);
38 });
39
40 /**
41 * can not be an arrow function as it gets rebound by chai
42 */
43 chai.Assertion.addProperty('shadowDom', function shadowDom() {
44 new chai.Assertion(this._obj.nodeType).to.equal(1);
45 utils.flag(this, 'shadowDom', true);
46 });
47
48 /**
49 * can not be an arrow function as it gets rebound by chai
50 */
51 chai.Assertion.addProperty('dom', function dom() {
52 new chai.Assertion(this._obj.nodeType).to.equal(1);
53 utils.flag(this, 'dom', true);
54 });
55
56 const getDomHtml = el => getOuterHtml(el);
57 const getLightDomHtml = el => el.innerHTML;
58 const getShadowDomHtml = el => getCleanedShadowDom(el);
59
60 /**
61 * Base HTML assertion for `assert` interface.
62 * @param {string | Node} actual
63 * @param {string | Node} expected
64 * @param {boolean} negate
65 * @param {[string]|[DiffOptions]|[string, DiffOptions]} rest
66 */
67 const assertHtmlEquals = (actual, expected, negate, ...rest) => {
68 const { message, options } = disambiguateArgs(...rest);
69 // use chai's built-in string comparison, log the updated snapshot on error
70 const assertion = new chai.Assertion(getDiffableHTML(actual, options), message);
71 const expectedDiffableHTML = getDiffableHTML(expected, options);
72
73 if (negate) {
74 assertion.not.equal(expectedDiffableHTML, message);
75 } else {
76 assertion.equal(expectedDiffableHTML, message);
77 }
78 };
79
80 /** DOM assertion for `should` and `expect` interfaces. */
81 const domEquals = _super =>
82 /**
83 * @this {Chai.AssertionStatic}
84 */
85 function handleDom(value, ...args) {
86 if (
87 utils.flag(this, 'lightDom') ||
88 utils.flag(this, 'shadowDom') ||
89 utils.flag(this, 'dom')
90 ) {
91 let html;
92 if (utils.flag(this, 'lightDom')) {
93 html = getLightDomHtml(this._obj);
94 } else if (utils.flag(this, 'shadowDom')) {
95 html = getShadowDomHtml(this._obj);
96 } else {
97 html = getDomHtml(this._obj);
98 }
99
100 assertHtmlEquals(html, value, utils.flag(this, 'negate'), args[0]);
101 } else {
102 _super.apply(this, [value, ...args]);
103 }
104 };
105
106 chai.Assertion.overwriteMethod('equals', domEquals);
107 chai.Assertion.overwriteMethod('equal', domEquals);
108 chai.Assertion.overwriteMethod('eq', domEquals);
109
110 const context = window.__mocha_context__;
111 const snapshotState = window.__snapshot__;
112
113 /**
114 * Base HTML snapshot assertion for `assert` interface.
115 * @this {Chai.AssertionStatic}
116 * @param {string|Node} actual
117 * @param {boolean} negate
118 * @param {[string]|[DiffOptions]|[string, DiffOptions]} rest
119 */
120 function assertHtmlEqualsSnapshot(actual, negate, ...rest) {
121 const { message, options } = disambiguateArgs(...rest);
122 const { index } = context;
123 context.index += 1;
124 let path;
125
126 const html = getDiffableHTML(actual, options);
127
128 if (context.runnable.type === 'hook') {
129 path = snapshotPath(context.runnable.ctx.currentTest);
130 } else {
131 path = snapshotPath(context.runnable);
132 }
133
134 if (snapshotState.update) {
135 snapshotState.set(path, index, html, 'html');
136 } else {
137 const snapshot = snapshotState.get(path, index);
138
139 if (!snapshot) {
140 snapshotState.set(path, index, html, 'html');
141 } else {
142 const isMatch = snapshotState.match(html, getDiffableHTML(snapshot.code, options));
143 if ((isMatch && negate) || (!isMatch && !negate)) {
144 /* istanbul ignore next */
145 throw new chai.AssertionError(
146 message || `Received value does not match stored snapshot ${index}`,
147 {
148 actual: html,
149 expected: snapshot.code,
150 showDiff: true,
151 },
152 chai.util.flag(this, 'ssfi'),
153 );
154 }
155 }
156 }
157 }
158
159 /**
160 * Snapshot assertion for `should` and `expect` interfaces.
161 * @this {Chai.AssertionStatic}
162 */
163 function equalSnapshot(options) {
164 const el = chai.util.flag(this, 'object');
165 let html;
166 if (utils.flag(this, 'shadowDom')) {
167 html = getShadowDomHtml(el);
168 } else if (utils.flag(this, 'lightDom')) {
169 html = getLightDomHtml(el);
170 } else {
171 html = el;
172 }
173 return assertHtmlEqualsSnapshot.call(this, html, utils.flag(this, 'negate'), options);
174 }
175
176 utils.addMethod(chai.Assertion.prototype, 'equalSnapshot', equalSnapshot);
177 utils.addMethod(chai.Assertion.prototype, 'notEqualSnapshot', equalSnapshot);
178
179 utils.addMethod(chai.assert, 'equalSnapshot', assertHtmlEqualsSnapshot);
180 utils.addMethod(chai.assert, 'notEqualSnapshot', assertHtmlEqualsSnapshot);
181
182 /** @type {Chai.Assert['dom']} */
183 chai.assert.dom = {
184 equal(actualEl, expectedHTML, ...rest) {
185 const negate = false;
186 return assertHtmlEquals.call(this, getDomHtml(actualEl), expectedHTML, negate, ...rest);
187 },
188 notEqual(actualEl, expectedHTML, ...rest) {
189 const negate = true;
190 return assertHtmlEquals.call(this, getDomHtml(actualEl), expectedHTML, negate, ...rest);
191 },
192 equalSnapshot(actualEl, ...rest) {
193 const negate = false;
194 return assertHtmlEqualsSnapshot.call(this, actualEl, negate, ...rest);
195 },
196 notEqualSnapshot(actualEl, ...rest) {
197 const negate = true;
198 return assertHtmlEqualsSnapshot.call(this, actualEl, negate, ...rest);
199 },
200 };
201
202 /** @type {Chai.Assert['lightDom']} */
203 chai.assert.lightDom = {
204 equal(actualEl, expectedHTML, ...rest) {
205 const negate = false;
206 return assertHtmlEquals.call(this, getLightDomHtml(actualEl), expectedHTML, negate, ...rest);
207 },
208 notEqual(actualEl, expectedHTML, ...rest) {
209 const negate = true;
210 return assertHtmlEquals.call(this, getLightDomHtml(actualEl), expectedHTML, negate, ...rest);
211 },
212 equalSnapshot(actualEl, ...rest) {
213 const negate = false;
214 return assertHtmlEqualsSnapshot.call(this, getLightDomHtml(actualEl), negate, ...rest);
215 },
216 notEqualSnapshot(actualEl, ...rest) {
217 const negate = true;
218 return assertHtmlEqualsSnapshot.call(this, getLightDomHtml(actualEl), negate, ...rest);
219 },
220 };
221
222 /** @type {Chai.Assert['shadowDom']} */
223 chai.assert.shadowDom = {
224 equal(actualEl, expectedHTML, ...rest) {
225 const negate = false;
226 return assertHtmlEquals.call(this, getShadowDomHtml(actualEl), expectedHTML, negate, ...rest);
227 },
228 notEqual(actualEl, expectedHTML, ...rest) {
229 const negate = true;
230 return assertHtmlEquals.call(this, getShadowDomHtml(actualEl), expectedHTML, negate, ...rest);
231 },
232 equalSnapshot(actualEl, ...rest) {
233 const negate = false;
234 return assertHtmlEqualsSnapshot.call(this, getShadowDomHtml(actualEl), negate, ...rest);
235 },
236 notEqualSnapshot(actualEl, ...rest) {
237 const negate = true;
238 return assertHtmlEqualsSnapshot.call(this, getShadowDomHtml(actualEl), negate, ...rest);
239 },
240 };
241};