PatternFly Elements - Home PatternFly Elements

Write your tests

Let's write tests for pfe-cool-element.

We rely on a few tools to ensure our element is reliable in production:

  1. Web Test Runner, built and maintained by the Modern Web team, makes testing easy. All we have to do is create a new test file, provide example markup and write some tests.
  2. We'll use the Chai Assertion Library to make sure our tests pass since Mocha and Chai are both included in Web Test Runner. Additional Web Component specific test helpers are also available.

Web Test Runner

If you followed the Prerequisites in Setup, your setup should already be done.

Test Setup

In the root of the element, there's a /test directory with an pfe-cool-element.spec.js file. This file will be where we add all of our tests.

Let's add four stubs for the functionality we need to test in our test file:

// Import testing helpers. For more information check out:
// https://open-wc.org/docs/testing/helpers/
import { expect, elementUpdated } from '@open-wc/testing/index-no-side-effects.js';

// Import our custom fixture wrapper. This allows us to run tests
// in React and Vue as well as a normal fixture.
import { createFixture } from '../../../test/utils/create-fixture.js';

// Import the element we're testing.
import '../dist/pfe-cool-element';

// One element, defined here, is used
// in multiple tests. It's torn down and recreated each time.
const element =
  `<pfe-cool-element photo-url="https://avatars2.githubusercontent.com/u/330256?s=400&u=de56919e816dc9f821469c2f86174f29141a896e&v=4">
    Kyle Buchanan
   </pfe-cool-element>
   `;

describe("<pfe-cool-element>", () => {

  it("it should upgrade", async () => {
    const el = await createFixture(element);

    expect(el).to.be.an.instanceOf(
      customElements.get("pfe-cool-element"),
      'pfe-cool-element should be an instance of PfeCoolElement'
    );
  });

  it('should set a username from the light DOM', async () => {

  });

  it('should allow a user to follow a profile', async () => {

  });

  it('should set the state to follow if the following attribute is present', async () => {

  });

  it('should set a profile pic from the photo-url attribute', async () => {

  });

});

Note that we using import '../dist/pfe-cool-element'; to load our element definition to make sure we're testing the true source of our element instead of the transpiled version.

You'll also notice the createFixture(element) function. A test fixture renders a piece of HTML and injects into the DOM so that you can test the behavior of your component. It returns the first dom element from the template so that you can interact with it if needed. For example you can call functions, look up DOM nodes or inspect the rendered HTML.

Test fixtures are async to ensure rendering is properly completed.

By using createFixture(), separate instances of tests that use a React or Vue wrapper are automatically created at run time. This allows us to easy test our elements within a React or Vue context.

Now that our setup is complete, we can start building our tests.

Test Cases

Let's build out the 'pfe-cool-element' tests. We'll use fixtures and querySelector to grab our element and include DOM API methods to interact with what we're testing.

Here is the full JavaScript code:

// Import testing helpers. For more information check out:
// https://open-wc.org/docs/testing/helpers/
import { expect, elementUpdated } from '@open-wc/testing/index-no-side-effects.js';

// Import our custom fixture wrapper. This allows us to run tests
// in React and Vue as well as a normal fixture.
import { createFixture } from '../../../test/utils/create-fixture.js';

// Import the element we're testing.
import '../dist/pfe-cool-element';

// One element, defined here, is used
// in multiple tests. It's torn down and recreated each time.
const element =
  `<pfe-cool-element photo-url="https://avatars2.githubusercontent.com/u/330256?s=400&u=de56919e816dc9f821469c2f86174f29141a896e&v=4">
    Kyle Buchanan
   </pfe-cool-element>
   `;

describe("<pfe-cool-element>", () => {

    it("it should upgrade", async () => {
      const el = await createFixture(element);

      expect(el).to.be.an.instanceOf(
        customElements.get("pfe-cool-element"),
        'pfe-cool-element should be an instance of PfeCoolElement'
      );
    });

    it('should set a username from the light DOM', async () => {
      const el = await createFixture(element);

      // Grab the text content from the light DOM and trim off any extra white space.
      const elementLightDOMContent = el.textContent.trim();
      const shadowRoot = el.shadowRoot;

      // Grab the text content from the slot and trim off any extra white space.
      const slotContent = shadowRoot.querySelector('slot').assignedNodes()[0].textContent.trim();

      // Make sure the slot text matches the light DOM text.
      expect(slotContent).to.equal(elementLightDOMContent);
    });

    it('should allow a user to follow a profile', async () => {
      const el = await createFixture(element);
      // Click the button.
      el.button.click();

      // Make sure the follow attribute is now updated.
      expect(el.hasAttribute("follow")).to.be.true;
      // Make sure the text is updated to reflect the "unfollowed" state.
      expect(el.button.textContent).to.equal("Unfollow");

      // Click the button again.
      el.button.click();

      // Make sure the follow attribute is updated.
      expect(el.hasAttribute("follow")).to.be.false;
      // Make sure the text is updated to reflect the "follow" state.
      expect(el.button.textContent).to.equal("Follow");

    });

    it('should set the state to follow if the following attribute is present', async () => {
      const el = await createFixture(element);

      // Manually add the follow attribute.
      el.setAttribute("follow", "");

      // Wait for the element to be done updating.
      // This isn't really necessary in this example but it's helpful to know about.
      await elementUpdated(el);

      // Make sure the follow attribute is now updated.
      expect(el.hasAttribute("follow")).to.be.true;
      // Make sure the text is updated to reflect the "unfollowed" state.
      expect(el.button.textContent).to.equal("Unfollow");

      // Manually remove the follow attribute.
      el.removeAttribute("follow");

      // Wait for the element to be done updating.
      await elementUpdated(el);

      // Make sure the follow attribute is updated.
      expect(el.hasAttribute("follow")).to.be.false;
      // Make sure the text is updated to reflect the "follow" state.
      expect(el.button.textContent).to.equal("Follow");
    });

    it('should set a profile pic from the photo-url attribute', async () => {
      const el = await createFixture(element);

      // Grab the `photo-url` attribute.
      const photoUrlAttribute = el.getAttribute('photo-url');

      // Find the background image in the shadow root css.
      const shadowRoot = el.shadowRoot;
      const profilePicContainer = shadowRoot.querySelector('#profile-pic');
      const backgroundImageUrl = profilePicContainer.style.backgroundImage.slice(5, -2);

      // Make sure the photo url attribute matches the background image url.
      expect(photoUrlAttribute).to.equal(backgroundImageUrl);
    });
});

You may notice we're accessing the shadowRoot here, available to our element by extending PFElement in the definition of our element. You can also access content in the <slot></slot> of your element by using the assignedNodes() method.

We use a slot for the username in pfe-cool-element, making it available to us in the array returned by assignedNodes().

shadowRoot.querySelector('slot').assignedNodes()[0].textContent.trim();

Run the Test

Lastly, we can run the test command below to see how we did. You can focus on this specific test so you're only running the tests for pfe-cool-element.

npm run test:watch --element="pfe-cool-element"

This command starts up web test runner and allows us to debug our test in the browser.

npm test command

Debugging the test in the browser should give you the following:

Browser test debugging

Note

Unexpected reserved word error

If you run into an SyntaxError: Unexpected reserved word error while writing tests, chances are it's because you're using an await function without an async parent.

Will error out

it('should set a username from the light DOM', () => {
  const el = await createFixture(element);
});

Fixed by adding the async keyword

it('should set a username from the light DOM', async () => {
  const el = await createFixture(element);
});

Testing against Vue and React

Now that our vanilla JavaScript tests are passing, let's use Vue and React wrappers to run the same tests.

Run the same tests within React by using:

npm run test:watch --group="with-react"

Run the same tests within Vue with:

npm run test:watch --group="with-vue"

This works exactly the same as the normal npm run test:watch command, the only difference is the fixture will be wrapped with React or Vue.

Finally we can run the test "ci" command which will run the following:

  1. All tests.
  2. Those same tests with React wrappers.
  3. Those same tests with Vue wrappers.
npm run test:ci

Final test output

Nice! All tests are working in Chrome and with React and Vue wrappers. 🎉

A quick note about the framework testing—the Vue and React tests are meant to be an initial first pass in those frameworks just to make sure that the functionality is working and that the component renders properly.

That's it for testing! Now that we've created our pfe-cool-element and all of our code passes, the final step is to submit a pull request to get this merged.

Next up: Open a pull request