So far in our series about testing JavaScript, we've looked at how to write tests for code that's mostly logic-based.
Today, we'll look at how to write tests for a DOM manipulation library or Web Component. Let's dig in!
The Library
Today, we'll be testing my <show-hide>
Web Component, which let's you create some content that you can show or hide whenever a <button>
is clicked.
(Yes, I know about <details>
and <summary>
! This is a good, simple example for teaching.)
To use it, wrap a <button>
and some content to selectively show or hide in the <show-hide>
component.
Add the [trigger]
and [hidden]
attributes to the toggle <button>
. Add the [content]
attribute to the content to show or hide.
<show-hide> <button trigger hidden>Show Content</button> <div content> <p>Now you see me, now you don't!</p> </div> </show-hide>
When the Web Component loads, it will display the button, hide the content, add required ARIA attributes, and add event listeners to selectively show or hide the [content]
when the [trigger]
is clicked.
It also adds the [aria-expanded]
attribute to the [trigger]
, which tells screen readers what the <button>
behavior is and its current state.
/** * Instantiate the Web Component */ constructor () { // Get parent class properties super(); // Get the elements this.trigger = this.querySelector('[trigger]'); this.content = this.querySelector('[content]'); if (!this.trigger || !this.content) return; // Setup default UI this.trigger.removeAttribute('hidden'); this.trigger.setAttribute('aria-expanded', false); this.content.setAttribute('hidden', ''); // Listen for click events if (!this.trigger || !this.content) return; this.trigger.addEventListener('click', this); }
When the [trigger]
is clicked, the [hidden]
attribute is added or removed to the [content]
, and the value of the [aria-expanded]
attribute is updated.
/** * Handle events * @param {Event} event The event object */ handleEvent (event) { // Don't let the button trigger other actions event.preventDefault(); // If the content is expanded, hide it // Otherwise, show it if (this.trigger.getAttribute('aria-expanded') === 'true') { this.trigger.setAttribute('aria-expanded', false); this.content.setAttribute('hidden', ''); } else { this.trigger.setAttribute('aria-expanded', true); this.content.removeAttribute('hidden'); } }
You can download the Web Component JavaScript and the rest of the code for today's article on GitHub.
What we're going to test
Thinking about this from a TDD approach (this isn't really TDD because the code is already written), the external behaviors we need are…
- On load, the
[trigger]
should be visible and the [content]
should be hidden. - On load, the required ARIA attributes should be added.
- When the
[trigger]
is clicked, the content should be shown or hidden, depending on its current state. - When the
[trigger]
is clicked, the [aria-expanded]
attribute value should change to reflect the current state.
We can start by stumping out our tests…
const {expect} = chai; describe('The <show-hide> Web Component', function () { it('Should hide content and make button visible on load', function () { // ... }); it('Should add required ARIA attributes on load', function () { // ... }); it('Should show and hide content on click events', function () { // ... }); it('Should update ARIA on click events', function () { // ... }); });
Because we're testing DOM manipulation, there are a few things we need to do that we don't with plain old logic tests.
Testing the DOM
In command line (CLI) testing libraries, you typically need to install a JS-based DOM simulator that replicates the browser engine.
Because we're going buildless and opening up a real browser, we don't have to worry about that! We have full access to all of the native browser methods.
What we don't have is the ability to actually scroll, click, and press keys in the UI. For those, we'll have to simulate events to run our tests.
I've put together a tests.dom-helpers.js
file that contains functions to simulate click events, keyboard events, and other generic events.
// Get our target element let btn = document.querySelector('button'); // Simulate a click on the button simulateClick(btn) // Simulate a return key keyboard event on the button simulateKeyboard('Return', btn); // Simulate any other type of event (in this case a mouseenter event) simulateEvent(elem, 'mouseenter');
This file is available in the source code, and over at the Lean Web Club.
Test setup
While you could just hard-code the HTML for the test into the test HTML file, it's probably a good idea to setup a fresh component before each test, and tear it down after each one to ensure code from a previous test doesn't mess up the current one.
Most testing frameworks offer a way to do that.
In Mocha, you can run before()
, after()
, beforeEach()
, and afterEach()
methods, which run before all tests, after all tests, before each test, and after each test respectively.
For our purposes, we'll create a new app
element and inject it into the document.body
.
Before each test, we'll inject a fresh copy of our <show-hide>
element. And after all the tests are done, we'll remove the app
element entirely.
// Create a DOM node to inject content into let app = document.createElement('div'); document.body.append(app); // Inject fresh Web Component beforeEach(function () { app.innerHTML = `<show-hide> <button trigger hidden>Show Content</button> <div content> <p>Now you see me, now you don't!</p> </div> </show-hide>`; }); // Clean up UI after(function () { app.remove(); });
Because we'll need to keep grabbing our elements over-and-over again, I also added a getElements()
method that searches for our elements in the DOM, and returns an object ({}
) with with references to each of our elements.
// Get the Web Component elements from the DOM function getElements () { return { wc: app.querySelector('show-hide'), trigger: app.querySelector('[trigger]'), content: app.querySelector('[content]'), }; }
Writing the tests
Our on load tests are the easiest to write.
We'll use our getElements()
method and object destructuring to get the elements we want to test.
Then, we'll use the Element.hasAttribute()
and Element.getAttribute()
methods to ensure the require attributes are there, aren't there, or have the correct value, respectively.
it('Should hide content and make button visible on load', function () { let {wc, trigger, content} = getElements(); expect(trigger.hasAttribute('hidden')).to.equal(false); expect(content.hasAttribute('hidden')).to.equal(true); }); it('Should add required ARIA attributes on load', function () { let {trigger} = getElements(); expect(trigger.getAttribute('aria-expanded')).to.equal('false'); });
To test click behavior, we'll again get our elements.
Then, we'll run the simulateClick()
method on the trigger
, and check that the [hidden]
attribute has been removed from the content
. We'll run simulateClick()
again, and make sure the [hidden]
element was added back.
it('Should show and hide content on click events', function () { // Get elements let {trigger, content} = getElements(); // Show content on click simulateClick(trigger); expect(content.hasAttribute('hidden')).to.equal(false); // Hide content if clicked again simulateClick(trigger); expect(content.hasAttribute('hidden')).to.equal(true); });
We'll run a similar test for the [aria-expanded]
attribute on the trigger
, this time checking its value, not just that it exists.
it('Should update ARIA on click events', function () { // Get elements let {trigger} = getElements(); // Show content on click simulateClick(trigger); expect(trigger.getAttribute('aria-expanded')).to.equal('true'); // Hide content if clicked again simulateClick(trigger); expect(trigger.getAttribute('aria-expanded')).to.equal('false'); });
Other tests you might run
It's probably a good idea to also test edge cases and error behaviors.
What happens, for example, if the user doesn't add required attributes like [trigger]
and [content]
. If you want to play around with this yourself, try writing tests for those behaviors.
Download the complete source code on GitHub.
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] Testing DOM manipulation and Web Components"