- 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
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.