UNPKG

8.63 kBMarkdownView Raw
1---
2permalink: 'testing/semantic-dom-diff.html'
3title: Semantic Dom Diff
4section: guides
5tags:
6 - guides
7---
8
9# Semantic Dom Diff
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> Part of [Open Web Components](https://github.com/open-wc/open-wc/): guides, tools and libraries for modern web development and web components
20
21[![CircleCI](https://circleci.com/gh/open-wc/open-wc.svg?style=shield)](https://circleci.com/gh/open-wc/open-wc)
22[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=M2UrSFVRang2OWNuZXlWSlhVc3FUVlJtTDkxMnp6eGFDb2pNakl4bGxnbz0tLUE5RjhCU0NUT1ZWa0NuQ3MySFFWWnc9PQ==--86f7fac07cdbd01dd2b26ae84dc6c8ca49e45b50)](https://www.browserstack.com/automate/public-build/M2UrSFVRang2OWNuZXlWSlhVc3FUVlJtTDkxMnp6eGFDb2pNakl4bGxnbz0tLUE5RjhCU0NUT1ZWa0NuQ3MySFFWWnc9PQ==--86f7fac07cdbd01dd2b26ae84dc6c8ca49e45b50)
23[![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/)
24
25## Manual Setup
26
27```bash
28npm i -D @open-wc/semantic-dom-diff
29```
30
31## Chai Plugin
32
33While `semantic-dom-diff` can be used standalone (see below), it most commonly used as a Chai plugin.
34
35<details>
36 <summary>Registering the plugin</summary>
37
38> If you are using `@open-wc/testing` this is already done for you.
39
40```javascript
41import 'chai/chai.js';
42import { chaiDomDiff } from '@open-wc/semantic-dom-diff';
43
44window.chai.use(chaiDomDiff);
45```
46
47</details>
48
49### Assertion Styles
50
51The Chai plugin supports both the BDD (`expect`) and TDD (`assert`) APIs.
52
53```javascript
54expect(el).dom.to.equal('<div></div>');
55assert.dom.equal(el, '<div></div>');
56
57expect(el).dom.to.equal('<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
58assert.dom.equal(el, '<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
59
60expect(el).lightDom.to.equal('<div></div>');
61assert.lightDom.equal(el, '<div></div>');
62
63expect(el).shadowDom.to.equal('<div></div>');
64assert.shadowDom.equal(el, '<div></div>');
65```
66
67### Setting up your dom for diffing
68
69You can set up our chai plugin to diff different types of DOM:
70
71```javascript
72class MyElement extends HTMLElement {
73 constructor() {
74 super();
75 this.attachShadow({ mode: 'open' });
76 }
77
78 connectedCallback() {
79 this.shadowRoot.innerHTML = '<p> shadow content </p>';
80 }
81}
82
83customElements.define('my-element', MyElement);
84
85it('my test', async () => {
86 const el = await fixture(`
87 <my-element>
88 <div> light dom content </div>
89 </my-element>
90 `);
91
92 expect(el).dom; // dom is <my-element><div>light dom content</div></my-element>
93 expect(el).lightDom; // dom is <div>light dom content</div>
94 expect(el).shadowDom; // dom is <p>shadow content</p>
95});
96```
97
98### Manual diffing
99
100You can use the chai plugin to manually diff chunks of dom. The dom is diffed semantically: whitespace, newlines, etc. are normalized.
101
102```javascript
103class MyElement extends HTMLElement {
104 constructor() {
105 super();
106 this.attachShadow({ mode: 'open' });
107 }
108
109 connectedCallback() {
110 this.shadowRoot.innerHTML = '<p> shadow content </p>';
111 }
112}
113
114customElements.define('my-element', MyElement);
115
116it('my test', async () => {
117 const el = await fixture(`
118 <my-element>
119 <div> light dom content </div>
120 </my-element>
121 `);
122
123 expect(el).dom.to.equal('<my-element><div>light dom content</div></my-element>');
124 expect(el).lightDom.to.equal('<div>light dom content</div>');
125 expect(el).shadowDom.to.equal('<p>shadow content</p>');
126});
127```
128
129### Snapshot testing
130
131The most powerful feature of `semantic-dom-diff` is the ability to test and manage snapshots of your web components.
132
133> 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).
134
135#### Setting up a snapshot
136
137Snapshots 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:
138
139```js
140import { fixture } from '@open-wc/testing';
141
142describe('my-message', () => {
143 it('renders message foo correctly', async () => {
144 const element = await fixture(`
145 <my-message message="Foo"></my-element>
146 `);
147
148 expect(element).shadowDom.to.equalSnapshot();
149 });
150
151 it('renders message bar correctly', async () => {
152 const element = await fixture(`
153 <my-message message="Bar"></my-element>
154 `);
155
156 expect(element).shadowDom.to.equalSnapshot();
157 });
158
159 it('renders a capitalized message correctly', async () => {
160 const element = await fixture(`
161 <my-message message="Bar" capitalized></my-element>
162 `);
163
164 expect(element).shadowDom.to.equalSnapshot();
165 });
166
167 it('allows rendering a message from a slot', async () => {
168 const element = await fixture(`
169 <my-message capitalized>Bar</my-element>
170 `);
171
172 expect(element).lightDom.to.equalSnapshot();
173 });
174});
175```
176
177Snapshots are stored in the `__snapshots__` folder in your project, using the most top-level `describe` as the name for your snapshots file.
178
179#### Updating a snapshot
180
181> 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.
182
183When 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.
184
185If the difference was an intended change, you can update the snapshots by passing the `--update-snapshots` flag.
186
187#### Cleaning up unused snapshots
188
189After refactoring, there might be unused and leftover snapshot files. You can run karma with the `--prune-snapshots` flag to clean these up.
190
191**Ignoring tags and attributes**
192
193When 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.
194
195```javascript
196it('renders correctly', async () => {
197 const el = await fixture(`
198 <div my-random-attribute="${Math.random()}">
199 Hey
200 </div>
201 `);
202
203 expect(el).dom.to.equal('<div>Hey</div>', {
204 ignoreAttributes: ['my-random-attribute'],
205 });
206
207 expect(el).dom.to.equalSnapshot({
208 ignoreAttributes: ['my-random-attribute'],
209 });
210});
211```
212
213**Ignoring an attribute only for certain tags**
214
215Randomly generated ids are often used, throwing off your diffs. You can ignore attributes on specific tags:
216
217```javascript
218it('renders correctly', async () => {
219 const el = await fixture(`
220 <input id="customInput${Math.random()}">
221 `);
222
223 // ignore id attributes on input elements
224 expect(el).dom.to.equal('<div>Hey</div>', {
225 ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
226 });
227
228 expect(el).dom.to.equalSnapshot({
229 ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
230 });
231});
232```
233
234**Ignoring tags**
235
236You can tell the diff to ignore certain tags entirely:
237
238```javascript
239it('renders correctly', async () => {
240 const el = await fixture(`
241 <div>
242 <my-custom-element></my-custom-element>
243 foo
244 </div>
245 `);
246
247 // ignore id attributes on input elements
248 expect(el).dom.to.equal('<div>Hey</div>', {
249 ignoreTags: ['my-custom-element'],
250 });
251
252 expect(el).dom.to.equalSnapshot({
253 ignoreTags: ['my-custom-element'],
254 });
255});
256```
257
258**Ignoring children**
259
260When 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.
261
262We can ignore just it's light dom:
263
264```javascript
265it('renders correctly', async () => {
266 const el = await fixture(`
267 <div>
268 <my-custom-input id="myInput">
269 <input id="inputRenderedInLightDom">
270 Some text rendered in the light dom
271 </my-custom-input>
272 foo
273 </div>
274 `);
275
276 // ignore id attributes on input elements
277 expect(el).dom.to.equal(
278 `
279 <div>
280 <my-custom-input id="myInput"></my-custom-input>
281 foo
282 </div>
283 `,
284 { ignoreChildren: ['my-custom-input'] },
285 );
286
287 expect(el).dom.to.equalSnapshot({
288 ignoreChildren: ['my-custom-input'],
289 });
290});
291```