UNPKG

8.66 kBMarkdownView Raw
1# Semantic Dom Diff
2
3[//]: # 'AUTO INSERT HEADER PREPUBLISH'
4
5## Manual Setup
6
7```bash
8npm i -D @open-wc/semantic-dom-diff
9```
10
11`semantic-dom-diff` allows diffing chunks of dom or HTML for semantic equality:
12
13- whitespace and newlines are normalized
14- tags and attributes are printed on individual lines
15- comments are removed
16- style, script and SVG contents are removed
17- tags, attributes or element's light dom can be ignored through configuration
18
19## Chai Plugin
20
21While `semantic-dom-diff` can be used standalone (see below), it most commonly used as a Chai plugin.
22
23<details>
24 <summary>Registering the plugin</summary>
25
26> If you are using `@open-wc/testing` this is already done for you.
27
28```javascript
29import 'chai/chai.js';
30import { chaiDomDiff } from '@open-wc/semantic-dom-diff';
31
32window.chai.use(chaiDomDiff);
33```
34
35</details>
36
37### Assertion Styles
38
39The Chai plugin supports both the BDD (`expect`) and TDD (`assert`) APIs.
40
41```javascript
42expect(el).dom.to.equal('<div></div>');
43assert.dom.equal(el, '<div></div>');
44
45expect(el).dom.to.equal('<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
46assert.dom.equal(el, '<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
47
48expect(el).lightDom.to.equal('<div></div>');
49assert.lightDom.equal(el, '<div></div>');
50
51expect(el).shadowDom.to.equal('<div></div>');
52assert.shadowDom.equal(el, '<div></div>');
53```
54
55### Setting up your dom for diffing
56
57You can set up our chai plugin to diff different types of DOM:
58
59```javascript
60class MyElement extends HTMLElement {
61 constructor() {
62 super();
63 this.attachShadow({ mode: 'open' });
64 }
65
66 connectedCallback() {
67 this.shadowRoot.innerHTML = '<p> shadow content </p>';
68 }
69}
70
71customElements.define('my-element', MyElement);
72
73it('my test', async () => {
74 const el = await fixture(`
75 <my-element>
76 <div> light dom content </div>
77 </my-element>
78 `);
79
80 expect(el).dom; // dom is <my-element><div>light dom content</div></my-element>
81 expect(el).lightDom; // dom is <div>light dom content</div>
82 expect(el).shadowDom; // dom is <p>shadow content</p>
83});
84```
85
86### Manual diffing
87
88You can use the chai plugin to manually diff chunks of dom. The dom is diffed semantically: whitespace, newlines, etc. are normalized.
89
90```javascript
91class MyElement extends HTMLElement {
92 constructor() {
93 super();
94 this.attachShadow({ mode: 'open' });
95 }
96
97 connectedCallback() {
98 this.shadowRoot.innerHTML = '<p> shadow content </p>';
99 }
100}
101
102customElements.define('my-element', MyElement);
103
104it('my test', async () => {
105 const el = await fixture(`
106 <my-element>
107 <div> light dom content </div>
108 </my-element>
109 `);
110
111 expect(el).dom.to.equal('<my-element><div>light dom content</div></my-element>');
112 expect(el).lightDom.to.equal('<div>light dom content</div>');
113 expect(el).shadowDom.to.equal('<p>shadow content</p>');
114});
115```
116
117### Snapshot testing
118
119The most powerful feature of `semantic-dom-diff` is the ability to test and manage snapshots of your web components.
120
121> If you are not using `@open-wc/testing-karma`, you need to manually install [karma-snapshot](https://www.npmjs.com/package/karma-snapshot) and [karma-mocha-snapshot](https://www.npmjs.com/package/karma-mocha-snapshot).
122
123#### Setting up a snapshot
124
125Snapshots are created by setting up your component in a specific state, and then calling `.to.equalSnapshot()`. You can use `.dom`, `.lightDom` or `.shadowDom` to set up the dom of your element:
126
127```js
128import { fixture } from '@open-wc/testing';
129
130describe('my-message', () => {
131 it('renders message foo correctly', () => {
132 const element = await fixture(`
133 <my-message message="Foo"></my-element>
134 `);
135
136 expect(element).shadowDom.to.equalSnapshot();
137 });
138
139 it('renders message bar correctly', () => {
140 const element = await fixture(`
141 <my-message message="Bar"></my-element>
142 `);
143
144 expect(element).shadowDom.to.equalSnapshot();
145 });
146
147 it('renders a capitalized message correctly', () => {
148 const element = await fixture(`
149 <my-message message="Bar" capitalized></my-element>
150 `);
151
152 expect(element).shadowDom.to.equalSnapshot();
153 });
154
155 it('allows rendering a message from a slot', () => {
156 const element = await fixture(`
157 <my-message capitalized>Bar</my-element>
158 `);
159
160 expect(element).lightDom.to.equalSnapshot();
161 });
162});
163```
164
165Snapshots are stored in the `__snapshots__` folder in your project, using the most top-level `describe` as the name for your snapshots file.
166
167#### Updating a snapshot
168
169> If you are not using the standard `@open-wc/testing-karma` configuration, see the documentation of `karma-snapshot` how to pass the update/prune flags.
170
171When your tests run for the first time the snapshot files are generated. On subsequent test runs your element is compared with the stored snapshots. If the element and the snapshots differ the test fails.
172
173If the difference was an intended change, you can update the snapshots by passing the `--update-snapshots` flag.
174
175#### Cleaning up unused snapshots
176
177After refactoring, there might be unused and leftover snapshot files. You can run karma with the `--prune-snapshots` flag to clean these up.
178
179**Ignoring tags and attributes**
180
181When working with libraries or custom elements there might be parts of the rendered dom which is random or otherwise outside of your control. In those cases, you might want to ignore certain attributes or tags entirely. This is possible by passing an options object.
182
183```javascript
184it('renders correctly', async () => {
185 const el = await fixture(`
186 <div my-random-attribute="${Math.random()}">
187 Hey
188 </div>
189 `);
190
191 expect(el).dom.to.equal('<div>Hey</div>', {
192 ignoreAttributes: ['my-random-attribute'],
193 });
194
195 expect(el).dom.to.equalSnapshot({
196 ignoreAttributes: ['my-random-attribute'],
197 });
198});
199```
200
201**Ignoring an attribute only for certain tags**
202
203Randomly generated ids are often used, throwing off your diffs. You can ignore attributes on specific tags:
204
205```javascript
206it('renders correctly', async () => {
207 const el = await fixture(`
208 <input id="customInput${Math.random()}">
209 `);
210
211 // ignore id attributes on input elements
212 expect(el).dom.to.equal('<div>Hey</div>', {
213 ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
214 });
215
216 expect(el).dom.to.equalSnapshot({
217 ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
218 });
219});
220```
221
222**Ignoring tags**
223
224You can tell the diff to ignore certain tags entirely:
225
226```javascript
227it('renders correctly', async () => {
228 const el = await fixture(`
229 <div>
230 <my-custom-element></my-custom-element>
231 foo
232 </div>
233 `);
234
235 // ignore id attributes on input elements
236 expect(el).dom.to.equal('<div>Hey</div>', {
237 ignoreTags: ['my-custom-element'],
238 });
239
240 expect(el).dom.to.equalSnapshot({
241 ignoreTags: ['my-custom-element'],
242 });
243});
244```
245
246**Ignoring children**
247
248When working with web components you may find that they sometimes render to their light dom, for example, to meet some accessibility requirements. We don't want to ignore the tag completely, as we would then not be able to test if we did render the tag.
249
250We can ignore just it's light dom:
251
252```javascript
253it('renders correctly', async () => {
254 const el = await fixture(`
255 <div>
256 <my-custom-input id="myInput">
257 <input id="inputRenderedInLightDom">
258 Some text rendered in the light dom
259 </my-custom-input>
260 foo
261 </div>
262 `);
263
264 // ignore id attributes on input elements
265 expect(el).dom.to.equal(
266 `
267 <div>
268 <my-custom-input id="myInput"></my-custom-input>
269 foo
270 </div>
271 `,
272 { ignoreChildren: ['my-custom-element'] },
273 );
274
275 expect(el).dom.to.equalSnapshot({
276 ignoreChildren: ['my-custom-element'],
277 });
278});
279```
280
281**TypeScript**
282
283When working with typescript you may notice that the types are not correct for
284
285```js
286expect(el).dom.to.equal('<div>Hey</div>', {
287 ignoreTags: ['my-custom-element'],
288});
289```
290
291e.g. the 2nd parameter is expected to be a string. Unfortunately, we currently can not change this.
292For now you will need to ignore types if you want to provide extra options.
293
294```js
295// @ts-ignore
296expect(el).dom.to.equal('<div>Hey</div>', {
297 ignoreTags: ['my-custom-element'],
298});
299```
300
301We plan to change and include it in the next [breaking testing release](https://github.com/open-wc/open-wc/projects/1).
302
303<script>
304 export default {
305 mounted() {
306 const editLink = document.querySelector('.edit-link a');
307 if (editLink) {
308 const url = editLink.href;
309 editLink.href = url.substr(0, url.indexOf('/master/')) + '/master/packages/semantic-dom-diff/README.md';
310 }
311 }
312 }
313</script>