- JSDOM
- Проблематика
- Что есть JSDOM
- Подключение
- Инициализация
- Скрипты в html
- Запуск скриптов
- Флоу
- Установка
- Экземпляр JSDOM
- Установка параметров
- Установка window и document
- Пишем тесты
- Загрузка по URL
- Альтернатива
- Визуальная составляющая
- Виртуальные консоли
- Изменения перед парсингом
- Подводные камни
- Глобальные свойства
- jest-dom
- Testing Library data-testid
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
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.