I have moved away from creating fixture elements using CSS selectors. While CSS can be very concise, it can be hard to recognize the created elements, and how they are nested within each other.
A more visual alternative is to embed a string of HTML into your test. My specs often have a function htmlFixtures()
that parses the HTML and returns a reference to all created elements:
let [list, item1, item2] = htmlFixtures(`
<ul type="square">
<li>item 1</li>
<li>item 2</li>
</ul>
`)
Because the effective HTML is visible in your test, reader will imediately recognize elements and how they relate to each other.
Referencing created elements
The function returns an array-like value with all created elements, in the same order they appear in the HTML string. Text nodes (like whitespace between elements) are also created, but are not part of the returned array:
let [flash, link] = htmlFixtures(`
<div class="flash">User created</div>
<a href="/users">Go back</a>
`)
If you are not interested in all elements, you can also use object destructuring to access elements by their [id]
attribute:
const { alert, login } = htmlFixtures(`
<div id="alert">Invalid credentials</div>
<form>
<label for="login">User name</label>
<input name="login" id="login">
</form>
`)
Changes
- Jasmine specs for the frontend often need some DOM elements to work with. Because creating them is such a common task, we should have an efficient way to do it.
- Let's say I need this HTML structure:
- ```html
- <ul type="square">
- <li>item 1</li>
- <li>item 2</li>
- </ul>
- ```
- This card compares various approaches to fabricating DOM elements for testing.
- ## Constructing individual elements
- While you can use standard DOM functions to individually create and append elements, this is extremely verbose:
- ```js
- let list = document.createElement('ul')
- list.type = 'square'
- jasmine.fixtures.appendChild(ul)
- let item1 = document.createElement('li')
- item1.innerText = 'item 1'
- list.appendChild(item1)
- let item2 = document.createElement('li')
- list.appendChild(item2)
- ```
- For a reader it is hard to follow which DOM structure is being created.
-## Setting `{ innerHTML }`- +## Parsing a HTML string
-An alternative is to set the `{ innerHTML }` property of a container element. This approach makes it very clear which DOM structure is being created:- +A very visual method is to embedded a string of HTML into your test. My specs often have a function `htmlFixtures()` that parses the HTML and returns a reference to all created elements:
- ```js
-jasmine.fixtures.innerHTML = `-<ul type="square">- +let [list, item1, item2] = htmlFixtures(`
- + <ul type="square">
- <li>item 1</li>
- <li>item 2</li>
- </ul>
-`- +`)
- ```
-> [note]-> I'm using a global [`jasmine.fixtures`](https://makandracards.com/makandra/521479-jasmine-cleaning-up-the-dom-after-each-test) container, which is cleared automatically after each test.- +Because the effective HTML is visible in your test, reader will imediately recognize elements and how they relate to each other.
-One drawback is that you don't get variable references for the individual elements. For this you need to rediscover elements after setting `{ innerHTML }`:- +
- +### Referencing created elements
- +
- +The function returns an array-like value with all created elements, in the same order they appear in the HTML string. Text nodes (like whitespace between elements) are also created, but are not part of the returned array:
- ```js
-jasmine.fixtures.innerHTML = ...-let list = jasmine.fixtures.children[0]-let [item0, item1] = list.querySelectorAll('li')-- +let [flash, link] = htmlFixtures(`
- + <div class="flash">User created</div>
- + <a href="/users">Go back</a>
- +`)
- +```
- +
- +If you are not interested in all elements, you can also use object destructuring to access elements by their `[id]` attribute:
- +
- +```js
- +const { alert, login } = htmlFixtures(`
- + <div id="alert">Invalid credentials</div>
- +
- + <form>
- + <label for="login">User name</label>
- + <input name="login" id="login">
- + </form>
- +`)
- +```
- +
- +
- +
- +### Elements are appended automatically
- +
- +Parsed elements are appended to the [`jasmine.fixtures`](https://makandracards.com/makandra/521479-jasmine-cleaning-up-the-dom-after-each-test) container, which is cleared automatically after each test.
- +
- +
- +
- +### Example implementation
- +
- +Here is is `htmlFixtures()` function to copy into your test suite. Feel free to tweak it to your liking!
- +
- +```js
- +function htmlFixtures(html) {
- + // Parse the given HTML string into an array of Node objects.
- + const parser = document.createRange()
- + parser.setStart(document.body, 0)
- + const fragment = parser.createContextualFragment(html)
- + const parsedNodes = [...fragment.childNodes]
- + const parsedElements = parsedNodes.filter((node) => node instanceof Element)
- +
- + // Traverse the new subtrees in pre-order and build an array of created elements
- + const collectElements = (root) => [root, [...root.children].map(collectElements)]
- + const allElements = parsedElements.map((element) => {
- + // jasmine.fixtures.append(element)
- + return collectElements(element)
- + }).flat(9999)
- +
- + // Map elements by their [id] attribute
- + const idMap = {}
- + for (let element of allElements) {
- + if (element.id) idMap[element.id] = element
- + }
- +
- + // Return a magic value that works for both object destructuring (by ID)
- + // and array structuring (elements in pre-order).
- + return {
- + ...idMap,
- + [Symbol.iterator]: allElements[Symbol.iterator].bind(allElements)
- + }
- +}
- +
- ```
- +
- +
- +
- ## Creating elements from CSS selectors
- A quick way to fabricate DOM elements is to have library create them from a CSS selector. There are several libraries that let you pass a selector like `"span.foo"` and turn it into a `<span class="foo">` element for you:
- - [up.element](https://unpoly.com) (from [Unpoly](https://unpoly.com/))
- - [jasmine-fixture](https://github.com/searls/jasmine-fixture) (requires jQuery)
- - [dom-create-element-query-selector](https://github.com/hekigan/dom-create-element-query-selector)
- Here is the example above using Unpoly's [`up.element.affix()`](https://unpoly.com/up.element.affix) function:
- ```js
- let list = up.element.affix(jasmine.fixtures, 'ul[type=square]')
- up.element.affix(list, 'li', text: 'item 1')
- up.element.affix(list, 'li', text: 'item 2')
- ```
- Note how every call to `up.element.affix()` returns a reference to the newly created element. I do not need to re-discover the individual elements after creation.
-### One more refinement- +One disadvantage of this method is that a reader will not immediately recognize the nesting of the created DOM elements.
- +
- +
- +## Setting `{ innerHTML }`
-I often define a function `jasmine.fixture()` that takes a CSS selector and appends a matching element to my [`jasmine.fixtures`](https://makandracards.com/makandra/521479-jasmine-cleaning-up-the-dom-after-each-test) container:- +An low-tech alternative to constructing individual elements is to set the `{ innerHTML }` property of a container element. This approach makes it very clear which DOM structure is being created:
- ```js
-jasmine.fixture = function(...args) {- let root = args[0] instanceof Element ? args.shift() : jasmine.fixtures- return up.element.affix(root, ...args) -}- +jasmine.fixtures.innerHTML = `
- + <ul type="square">
- + <li>item 1</li>
- + <li>item 2</li>
- + </ul>
- +`
- ```
-This lets me shorten the example some more:- +I'm using a global [`jasmine.fixtures`](https://makandracards.com/makandra/521479-jasmine-cleaning-up-the-dom-after-each-test) container, which is cleared automatically after each test.
- +
- +One drawback is that you don't get variable references for the individual elements. For this you need to rediscover elements after setting `{ innerHTML }`:
- ```js
-let list = jasmine.fixture('ul[type=square]')-jasmine.fixture(list, 'li', text: 'item 1')-jasmine.fixture(list, 'li', text: 'item 2')- +jasmine.fixtures.innerHTML = ...
- +let list = jasmine.fixtures.children[0]
- +let [item0, item1] = list.querySelectorAll('li')
- ```