Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

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

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»