UNPKG

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