Skip to content

How to test cloned HTML templates and generated by JS HTML elements with Jest?

I have the following HTML:

<div class='t-wrapper' id='t-wrapper'>
  <template id="type1">
    <div class='t-container t-container__type1'>
      <a>
        <div class='poster'></div>
        <div class='content'>
          <div class='title'></div>
          <div class='description'></div>
        </div>
      </a>
    </div>
  </template>
</div>

And I have the following plain JS file script.js:

const SETTINGS = {
  // some settings
}

class Manager {
  constructor(params) {
    this.PUBLISHER_ID = params.PUBLISHER_ID
    // ...setting other class properties
    this.init(this.SELECTOR, this.TYPE)
  }

  // private methods
  #generateElement(obj, el) {
    const element = document.createElement(obj.elType || 'div')
    // setting attributes and generating children elements
    return element
  }

  #fillData(data, el) {
    el.querySelector('a').setAttribute('href', data.url)
    el.querySelector('a').setAttribute('target', SETTINGS[data.origin].target)
    // ...setting attributes and text content
    return el
  }

  #render(data, type) {
    if ('content' in document.createElement('template')) {
      // if HTML template is supported by browser
      const template = document.querySelector(`#${type}`)
      let clone = template.content.cloneNode(true)
      clone = this.#fillData(data, clone)
      return clone
    } else {
      // If HTML template not supported, opt to generating elements
      let element = this.#generateElement(SETTINGS.types[type].structure)
      element.classList.add(`t-container__${type}`)
      element = this.#fillData(data, element)
      return element
    }
  }
  // end private methods

  get requestUrl() {
    return 'constructed URL'
  }

  async getData() {
    const data = // fetching data with fetch()
    return data
  }

  init(elementId, type) {
    this.getData().then(
      function (data) {
        if (data.list && data.list.length) {
          const fragment = new DocumentFragment()
          const wrapper = document.querySelector(`#${elementId}`)
          for (const item of data.list) {
            const el = this.#render(item, type)
            fragment.appendChild(el)
          }
          wrapper.appendChild(fragment)
        }
      }.bind(this)
    )
  }
}

// Defining all neccessary constants
const PUBLISHER_ID = 'some-id'
const API_KEY = 'some-key'
const SOURCE_ID = '123456789'
const COUNT = 6
const SELECTOR = 't-wrapper'

new Manager({ PUBLISHER_ID, API_KEY, SOURCE_ID, COUNT, SELECTOR })

module.exports = { Manager }

Basically, once the class Manager is instantiated, it calls the init() method. This method is fetching data from an API, and once fetched, it generates HTML elements for each element of the received data.list array. When an element is being generated, it first checks if a browser supports the HTML template. If it does – the template is being cloned and attributes are being set. If not – the elements are being created with document.createElement(). The generated <a> element will have a "target" attribute which depends on one of the settings – either "_self" or "_blank"

Everything work and the elements are being generated either way. However, now I need to test with Jest that they are in fact being generated, and when clicking on a link it should be opened either in a new window/tab, or in the same window, depending on the setting.

I’m very, very new to Jest and to testing. After some searching I tried to follow this example. So I created this test:

const { Manager } = require('./script')

const PUBLISHER_ID = 'some-id'
const API_KEY = 'some-key'
const SOURCE_ID = '123456789'
const COUNT = 6

describe('Manager Script', () => {

  let createElement
  let querySelector
  let createObjectURL
  beforeAll(() => {
    createElement = document.createElement
    querySelector = document.querySelector
    createObjectURL = window.URL.createObjectURL
  })
  afterAll(() => {
    jest.restoreAllMocks()
    document.createElement = createElement
    document.querySelector = querySelector
    window.URL.createObjectURL = createObjectURL
  })

  // ...other tests

  it('should render elements', () => {
    const divEl = { setAttribute: jest.fn(), innerHTML: '' }
    const linkEl = { setAttribute: jest.fn(), innerHTML: '' }
    const container = { appendChild: jest.fn() }

    document.createElement = jest.fn().mockImplementation(tagName => {
      switch (tagName) {
        case 'div':
          return divEl
        case 'a':
          return linkEl
      }
    })

    document.querySelector = jest.fn().mockRejectedValueOnce(container)
    const manager = new Manager({
      PUBLISHER_ID,
      API_KEY,
      SOURCE_ID,
      COUNT,
    })
    expect(document.createElement).toBeCalledTimes(1)
  })
})

But this test fails with Expected number of calls: 1; Received number of calls: 0

I tried to call manager.init() after the instantiation, tried to set the template support checking to false and go directly to generating with document.createElement() part, tried to use beforeEach/afterEach (as in example)… The test keeps failing.

What am I doing wrong and how can I make it work? How can I test all those things?

Answer

In general perception, I think you should use a mocked data instead of getting data from external API.

  1. It will make your test much more faster.
  2. It will make your text much more stable.
  3. It will focus on what the test is meant to test. (The API / Internet Connection are not relevant for testing the creation of the template)

I believe you test will work fine if it will not have to “wait” for the async response.