@open-wc/semantic-dom-diff
    TypeScript icon, indicating that this package has built-in type declarations

    0.19.4 • Public • Published

    permalink: 'testing/semantic-dom-diff.html' title: Semantic Dom Diff section: guides tags:

    • guides

    Semantic Dom Diff

    semantic-dom-diff allows diffing chunks of dom or HTML for semantic equality:

    • whitespace and newlines are normalized
    • tags and attributes are printed on individual lines
    • comments are removed
    • style, script and SVG contents are removed
    • tags, attributes or element's light dom can be ignored through configuration

    Part of Open Web Components: guides, tools and libraries for modern web development and web components

    CircleCI BrowserStack Status Renovate enabled

    Manual Setup

    npm i -D @open-wc/semantic-dom-diff

    Chai Plugin

    While semantic-dom-diff can be used standalone (see below), it most commonly used as a Chai plugin.

    Registering the plugin

    If you are using @open-wc/testing this is already done for you.

    import 'chai/chai.js';
    import { chaiDomDiff } from '@open-wc/semantic-dom-diff';
    
    window.chai.use(chaiDomDiff);

    Assertion Styles

    The Chai plugin supports both the BDD (expect) and TDD (assert) APIs.

    expect(el).dom.to.equal('<div></div>');
    assert.dom.equal(el, '<div></div>');
    
    expect(el).dom.to.equal('<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
    assert.dom.equal(el, '<div foo="bar"></div>', { ignoreAttributes: ['foo'] });
    
    expect(el).lightDom.to.equal('<div></div>');
    assert.lightDom.equal(el, '<div></div>');
    
    expect(el).shadowDom.to.equal('<div></div>');
    assert.shadowDom.equal(el, '<div></div>');

    Setting up your dom for diffing

    You can set up our chai plugin to diff different types of DOM:

    class MyElement extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
      }
    
      connectedCallback() {
        this.shadowRoot.innerHTML = '<p> shadow content </p>';
      }
    }
    
    customElements.define('my-element', MyElement);
    
    it('my test', async () => {
      const el = await fixture(`
        <my-element>
          <div> light dom content </div>
        </my-element>
      `);
    
      expect(el).dom; // dom is <my-element><div>light dom content</div></my-element>
      expect(el).lightDom; // dom is <div>light dom content</div>
      expect(el).shadowDom; // dom is <p>shadow content</p>
    });

    Manual diffing

    You can use the chai plugin to manually diff chunks of dom. The dom is diffed semantically: whitespace, newlines, etc. are normalized.

    class MyElement extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
      }
    
      connectedCallback() {
        this.shadowRoot.innerHTML = '<p> shadow content </p>';
      }
    }
    
    customElements.define('my-element', MyElement);
    
    it('my test', async () => {
      const el = await fixture(`
        <my-element>
          <div> light dom content </div>
        </my-element>
      `);
    
      expect(el).dom.to.equal('<my-element><div>light dom content</div></my-element>');
      expect(el).lightDom.to.equal('<div>light dom content</div>');
      expect(el).shadowDom.to.equal('<p>shadow content</p>');
    });

    Snapshot testing

    The most powerful feature of semantic-dom-diff is the ability to test and manage snapshots of your web components.

    If you are not using @open-wc/testing-karma, you need to manually install karma-snapshot and karma-mocha-snapshot.

    Setting up a snapshot

    Snapshots 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:

    import { fixture } from '@open-wc/testing';
    
    describe('my-message', () => {
      it('renders message foo correctly', async () => {
        const element = await fixture(`
          <my-message message="Foo"></my-element>
        `);
    
        expect(element).shadowDom.to.equalSnapshot();
      });
    
      it('renders message bar correctly', async () => {
        const element = await fixture(`
          <my-message message="Bar"></my-element>
        `);
    
        expect(element).shadowDom.to.equalSnapshot();
      });
    
      it('renders a capitalized message correctly', async () => {
        const element = await fixture(`
          <my-message message="Bar" capitalized></my-element>
        `);
    
        expect(element).shadowDom.to.equalSnapshot();
      });
    
      it('allows rendering a message from a slot', async () => {
        const element = await fixture(`
          <my-message capitalized>Bar</my-element>
        `);
    
        expect(element).lightDom.to.equalSnapshot();
      });
    });

    Snapshots are stored in the __snapshots__ folder in your project, using the most top-level describe as the name for your snapshots file.

    Updating a snapshot

    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.

    When 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.

    If the difference was an intended change, you can update the snapshots by passing the --update-snapshots flag.

    Cleaning up unused snapshots

    After refactoring, there might be unused and leftover snapshot files. You can run karma with the --prune-snapshots flag to clean these up.

    Ignoring tags and attributes

    When 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.

    it('renders correctly', async () => {
      const el = await fixture(`
        <div my-random-attribute="${Math.random()}">
          Hey
        </div>
      `);
    
      expect(el).dom.to.equal('<div>Hey</div>', {
        ignoreAttributes: ['my-random-attribute'],
      });
    
      expect(el).dom.to.equalSnapshot({
        ignoreAttributes: ['my-random-attribute'],
      });
    });

    Ignoring an attribute only for certain tags

    Randomly generated ids are often used, throwing off your diffs. You can ignore attributes on specific tags:

    it('renders correctly', async () => {
      const el = await fixture(`
        <input id="customInput${Math.random()}">
      `);
    
      // ignore id attributes on input elements
      expect(el).dom.to.equal('<div>Hey</div>', {
        ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
      });
    
      expect(el).dom.to.equalSnapshot({
        ignoreAttributes: [{ tags: ['input'], attributes: ['id'] }],
      });
    });

    Ignoring tags

    You can tell the diff to ignore certain tags entirely:

    it('renders correctly', async () => {
      const el = await fixture(`
        <div>
          <my-custom-element></my-custom-element>
          foo
        </div>
      `);
    
      // ignore id attributes on input elements
      expect(el).dom.to.equal('<div>Hey</div>', {
        ignoreTags: ['my-custom-element'],
      });
    
      expect(el).dom.to.equalSnapshot({
        ignoreTags: ['my-custom-element'],
      });
    });

    Ignoring children

    When 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.

    We can ignore just it's light dom:

    it('renders correctly', async () => {
      const el = await fixture(`
        <div>
          <my-custom-input id="myInput">
            <input id="inputRenderedInLightDom">
            Some text rendered in the light dom
          </my-custom-input>
          foo
        </div>
      `);
    
      // ignore id attributes on input elements
      expect(el).dom.to.equal(
        `
        <div>
          <my-custom-input id="myInput"></my-custom-input>
          foo
        </div>
      `,
        { ignoreChildren: ['my-custom-input'] },
      );
    
      expect(el).dom.to.equalSnapshot({
        ignoreChildren: ['my-custom-input'],
      });
    });

    Scoped elements

    Let's take this example going forward:

    import { ScopedElementsMixin } from '@open-wc/scoped-elements';
    import { LitElement } from 'lit-element';
    import { MyButton } from '@somewhere/my-button';
    
    class MyElement extends ScopedElementsMixin(LitElement) {
      static get scopedElements() {
        return {
          'my-button': MyButton,
        };
      }
      render() {
        return html`
          <p>Here's my button</p>
          <my-button>Hey!</my-button>
        `;
      }
    }
    
    window.customElements.define('my-element', MyElement);

    Without a proper diffing, scoped elements will produce untestable Shadow DOM snapshots. A different chunk of random numbers is attached to these elements' tags, every time, like so:

    <my-element>
      <p>Here's my button</p>
      <my-button-23443 data-tag-name="my-button"> Hey! </my-button-23443>
    </my-element>

    Whether you want to ignore this tag entirely:

    it('renders correctly', async () => {
      const el = await fixture(`
          <my-element>
          </my-element>
      `);
    
      expect(el).shadowDom.to.equalSnapshot({ ignoreTags: 'my-button' });
    });
    <my-element>
      <p>Here's my button</p>
    </my-element>

    ... or just take a whole Shadow DOM snapshot of it:

    it('renders correctly', async () => {
      const el = await fixture(`
          <my-element>
          </my-element>
      `);
    
      expect(el).shadowDom.to.equalSnapshot();
    });
    <my-element>
      <p>Here's my button</p>
      <my-button data-tag-name="my-button"> Hey! </my-button>
    </my-element>

    We've got you covered! ;)

    Notice how in both examples the diff has worked out the real name and produced a testable snapshot.

    Install

    npm i @open-wc/semantic-dom-diff

    DownloadsWeekly Downloads

    25,788

    Version

    0.19.4

    License

    MIT

    Unpacked Size

    67.1 kB

    Total Files

    13

    Last publish

    Collaborators

    • avatar
    • avatar
    • avatar