Зарегистрируйтесь, чтобы продолжить обучение

JSDOM Тестирование фронтенда

Видео может быть заблокировано из-за расширений браузера. В статье вы найдете решение этой проблемы.

theme: gaia class:

  • lead
  • invert paginate: true ---

JSDOM

Hexlet


Проблематика

  • вам нужно окружение, позволяющее запускать браузер
  • тесты медленные

Что есть JSDOM

  • библиотека, которая анализирует и взаимодействует с собранным HTML так же, как браузер
  • jsdom - это чистая JavaScript-реализация многих веб-стандартов, в частности стандартов WHATWG DOM и HTML, для использования с Node.js
  • это не настоящий браузер, НО
  • она реализует веб-стандарты так же, как это делают браузеры

Подключение

// npm i jsdom -D

const jsdom = require('jsdom')
const { JSDOM } = jsdom

Инициализация

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`)
console.log(dom.window.document.querySelector('p').textContent)

Параметры

const dom = new JSDOM(``, {
  url: 'https://mail.ru/', // по умолчанию about:blank
  referrer: 'https://mail.ru/',
  contentType: 'text/html', // значения по умолчанию
  includeNodeLocations: true, // найти, где находится DOM-узел в исходном документе
  storageQuota: 10000000, // объем в кодовых единицах (https://developer.mozilla.org/en-US/docs/Glossary/Code_unit)
})

Скрипты в html

// Не работает
const dom = new JSDOM(`<body>
  <script>document.body.appendChild(document.createElement("hr"));</script>
</body>`)

Запуск скриптов

// Работает, НО
// но появляется уязвимость
const dom = new JSDOM(`<body>
  <script>document.body.appendChild(document.createElement("hr"));</script>
</body>`, { runScripts: 'dangerously' })

  • resources: 'usable' - можно ли загружать ресурсы вроде js, css, изображения и пр.
  • runScripts: 'dangerously' - можно ли запускать скрипты

Флоу


Установка

import { JSDOM } from 'jsdom'
import { repaintButton } from '../helpers'

describe('button styles', () => {
  test('repaint', () => {

  })
})

Экземпляр JSDOM

describe('button styles', () => {
  beforeEach(() => {
    const dom = new JSDOM()
  })

  test('repaint', () => {

  })
})

Установка параметров

describe('button styles', () => {
  beforeEach(() => {
    const dom = new JSDOM(
      '<button class="button" aria-expanded="true">Im A Button</button>',
      { url: 'https://localhost:3000' },
    )
  })

  test('repaint', () => {

  })
})

Установка window и document

describe('button', () => {
  beforeEach(() => {
    const dom = new JSDOM('some html', { url: 'https://localhost:3000' })

    global.window = dom.window
    global.document = dom.window.document
  })

  test('repaint', () => {

  })
})

Пишем тесты


describe('button', () => {
  beforeEach(() => {
    const dom = new JSDOM(
      '<button class="button" aria-expanded="true">Im A Button</button>',
      { url: 'https://localhost:3000' },
    )

    global.window = dom.window
    global.document = dom.window.document
  })

  test('repaint', () => {
    // Выбираем
    const button = document.querySelector('.button')

    // Действуем
    repaintButton(button)

    // Проверяем
    expect(button.style.color).toBe('red')
  })
})

А можно и так

// Никаких импортов
describe('button', () => {
  beforeEach(() => {
    document.body.innerHTML = '<button class="button" aria-expanded="true">Im A Button</button>'
  })

  test('repaint', () => {
    // Выбираем
    const button = document.querySelector('.button')

    // Действуем
    repaintButton(button)

    // Проверяем
    expect(button.style.color).toBe('red')
  })
})


fs.writeFile(
  './dist/preview.html',
  dom.window.document.querySelector("html").innerHTML,
  (err) => err && throw err
);


Загрузка по URL

const response = await axios.get('https://mail.ru/')
const dom = new JSDOM(response.data)
dom.window.document.querySelectorAll('a').forEach((link) => {
  console.log(link.href)
})

Альтернатива

const dom = await JSDOM.fromURL('https://mail.ru/')

Визуальная составляющая

  • jsdom не умеет рендерить визуальное содержание
  • не отображает верстку
    • вы не можете перетащить ползунок и проверить, что что-то изменилось
    • то же касается бесконечной прокрутки
    • или :hover, :active
    • и прочего

Виртуальные консоли

  • Сколько всего есть консолей?
    • 3
    • console.log из Node.js
    • Консоль страницы
    • Консоль jsdom

const virtualConsole = new jsdom.VirtualConsole()
const dom = new JSDOM(``, { virtualConsole })
  • По умолчанию конструктор JSDOM вернет экземпляр с виртуальной консолью, который пересылает все свои данные в консоль Node.js
  • virtualConsole.sendTo(console);


const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.on("error", () => { ... });
const dom = new JSDOM(``, { virtualConsole });


  • особое событие jsdomError отображает:
    • Ошибки загрузки или парсинга ресурсов (скрипты, стили, фреймы, и i-фреймы)
    • Ошибки при выполнении скрипта, которые не видит обработчик событий window onerror, который возвращает true или вызывает event.preventDefault()
    • Не реализованные ошибки, возникающие в результате вызовов методов вроде window.alert, которого нет в jsdom, но который установлен для веб-совместимости
  • virtualConsole.sendTo(console, { omitJSDOMErrors: true });

Изменения перед парсингом

Это особенно полезно, если вы хотите каким-либо образом изменить среду, например, добавив адаптеры для API веб-платформы, которые jsdom не поддерживает

const dom = new JSDOM(`<p>Hello</p>`, {
  beforeParse(window) {
    window.document.childNodes.length === 0
    window.someCoolAPI = () => { /* ... */ }
  },
})

  • JSDOM.fromURL
  • JSDOM.fromFile
  • JSDOM.fragment
  • Поддержка canvas с помощью npm-пакета node-canvas
  • window.close() убивает window.setTimeout()
  • Запускайте в браузере, НО
  • используйте прокси

Подводные камни

  • Фундаментальное ограничение для асинхронной загрузки скриптов
  • Нет навигации
  • Нет верстки: getBoundingClientRects, offsetTop

Глобальные свойства

  • Мы используем navigator.userAgent.indexOf('Chrome') > -1 вместо window.navigator.userAgent.indexOf('Chrome') > -1

  • ReferenceError: navigator is not defined


function propagateToGlobal(window) {
  for (let key in window) {
    if (!window.hasOwnProperty(key)) continue
    if (key in global) continue

    global[key] = window[key]
  }
}

jest-dom

  • Убирает дублирующийся код, добавляет абстракцию
    • аттрибуты
    • текст
    • css классы
    • и т.д.
  • Расширяет матчеры jest
  • Декларативность

Testing Library data-testid

<button data-testid="button">Click me</button>
const domElement = getByTestId('button')

expect(getByTestId('button')).toBeDisabled()

expect(getByTestId('empty')).toBeEmptyDOMElement()

expect(getByTestId('valid-form')).not.toBeInvalid()

expect(getByTestId('wrapper')).not.toBeVisible()

expect(getByTestId('delete-button')).toHaveClass('btn-danger extra btn', { exact: true })

expect(getByTestId('button')).not.toHaveStyle({
  backgroundColor: 'blue',
  display: 'none',
})

expect(getByTestId('greeting')).toHaveTextContent(/^Hello Username$/)

eslint-plugin-jest-dom

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff