E2E Тестирование
jest-puppeteer
- библиотека для тестирования с открытым исходным кодом
Пример конфига для jest
:
// jest.config.js
{
preset: 'jest-puppeteer',
globals: {
URL: 'http://localhost:8080'
},
testMatch: [
'**/test/**/*.test.js'
],
verbose: true
}
Пример конфига для jest-puppeteer
:
// jest-puppeteer.config.js
// Вместо puppeteer.launch()
{
launch: {
headless: process.env.HEADLESS !== 'false',
slowMo: process.env.SLOWMO ? process.env.SLOWMO : 0,
devtools: true
}
}
Пример теста:
const timeout = process.env.SLOWMO ? 30000 : 10000
beforeAll(async () => {
// Перед каждым тестом открываем страницу
await page.goto(URL, { waitUntil: 'domcontentloaded' })
})
describe('Test header and title of the page', () => {
test('Title of the page', async () => {
// Получаем заголовок
const title = await page.title()
// Проверяем заголовок
expect(title).toBe('E2E Puppeteer Testing')
},
// Задаём таймаут теста
timeout,
)
})
Метод evaluate()
позволяет выполнить переданную фукнкцию, как если бы она была выполнена на странице. Первым параметром принимает функцию, которую нужно выполнить, а остальные параметры передаются в качестве аргументов в выполняемую функцию:
test('Header of the page', async () => {
// Получаем элемент
const h1Handle = await page.$('.learn_header')
// Вызываем evaluate
const html = await page.evaluate(
// Передаём функцию, которую нужно выполнить
h1Handle => h1Handle.innerHTML,
// Следующим параметром передаём аргумент
h1Handle,
)
// Проверяем результат
expect(html).toBe('What will you learn')
})
Пример теста для формы регистрации:
test('Submit form with valid data', async () => {
// Переходим на форму регистрации
await page.click('[href="/signin"]')
// Ждём загрузки формы
await page.waitForSelector('form')
// Вводим логин
await page.type('#name', 'Rick')
// Вводим пароль
await page.type('#password', 'szechuanSauce')
// Вводим потверждение пароля
await page.type('#repeat_password', 'szechuanSauce')
// Отправляем форму
await page.click('[type="submit"]')
// Ждем завершения отправки
await page.waitForSelector('.success')
// Получаем содержимое сообщения
const html = await page.$eval('.success', el => el.innerHTML)
// Проверяем сообщение
expect(html).toBe('Successfully signed up!')
})
Пример создания скриншотов и изменение размера страницы:
// screenshots.test.js
test('Take screenshot of home page', async () => {
// Задаем размеры страницы
await page.setViewport({ width: 1920, height: 1080 })
// Создаем скриншот
await page.screenshot({
path: './src/test/screenshots/home.jpg',
fullpage: true,
type: 'jpeg',
})
})
Для проверки разных размеров страницы, удобно использовать test.each
:
// global
const dimentions = [
[600, 1200],
[640, 1200],
[600, 1380],
[640, 1380],
]
// screenshots.test.js
// Проходимся по каждому размеру
test.each(dimentions)('Take screenshot of home page with size %p x %p', async ([height, width]) => {
// Задаем размеры страницы
await page.setViewport({ width, height })
// Сохраняем скриншот
await page.screenshot({
path: `./src/test/screenshots/home-${height}x${width}.jpg`,
fullpage: true,
type: 'jpeg',
})
})
Пример эмуляции другого устройства:
// Подключаем модуль для имитации устройств
import devices from 'puppeteer/DeviceDescriptors'
test('Emulate Mobile Device And take screenshot', async () => {
// Открываем страницу
await page.goto(`${URL}/login`, { waitUntil: 'domcontentloaded' })
const iPhonex = devices['iPhone X']
// Задаем эмуляцию устройства
await page.emulate(iPhonex)
// Задаем настройки устройства
await page.setViewport({ width: 375, height: 812, isMobile: true })
// Создаем скриншот
await page.screenshot({
path: './src/test/screenshots/home-mobile.jpg',
fullpage: true,
type: 'jpeg',
})
})
Пример прерывания запроса:
test('Intercept Request', async () => {
// Активируем перехват запросов
await page.setRequestInterception(true)
page.on('request', (interceptedRequest) => {
// Перехватываем запросы и обрабатываем
if (interceptedRequest.url().endsWith('.png')) {
interceptedRequest.abort()
}
else {
interceptedRequest.continue()
}
})
await page.reload({ waitUntil: 'networkidle0' })
await page.setRequestInterception(false)
})
Puppeteer
Тесты и браузер имеют разную среду. Чтобы запустить код в контексте браузера, нужно использовать evaluate()
:
// Скролл страницы
await page.evaluate((x, y) => window.scrollBy(x, y), x, y);
// Получение переменной
await page.evaluate(() => variable));
Пример извлечения элементов:
const imageUrls = await page.evaluate(() => {
// Внутри evaluate обращаемся напрямую к DOM
const images = document.querySelectorAll('article img')
const urls = Array.from(images).map(({ src }) => ({ src }))
return urls
})
Тоже самое, но с передачей селектора в переменной:
// Сохраняем селектор
const imageSelector = 'article img'
const imageUrls = await page.evaluate((selector) => {
// selector передается через параметр
const images = document.querySelectorAll(selector)
const urls = Array.from(images).map(({ src }) => ({ src }))
return urls
}, imageSelector) // Передаём селектор
Еще один пример:
// Получаем элемент
const bodyHandle = await page.$('body')
// Извлекаем содержимое элемента с помощью evaluate
const html = await page.evaluate(body => body.innerHTML, bodyHandle)
await bodyHandle.dispose()
Playwright также имеет метод evaluate()
:
// Обращаемся к документу с помощью evaluate
const href = await page.evaluate(() => document.location.href)
// Выполнение запроса
const status = await page.evaluate(async () => {
const response = await fetch(location.href)
return response.status
})
// Получаем кнопку
const button = await page.$('button')
// Извлекаем содержимое элемента с помощью evaluate
const buttonText = await page.evaluate(button => button.textContent, button)
Как не надо делать:
const user = { name: 'Ruslan', age: 77 }
const result = await page.evaluate(() => {
// Доступа к user нет, замыкание не работает
window.myApp.use(user)
})
Правильный способ:
const user = { name: 'Ruslan', age: 77 }
const result = await page.evaluate((user) => {
window.myApp.use(user)
}, user) // Передаём user через параметры evaluate
Работа с асинхронностью
Есть несколько событий, которые мы ожидаем:
- загрузка страницы
- изменения на странице (изменения в DOM-дереве)
- запросы
- кастомные ожидания
Ожидание загрузки страницы
Selenium
// Переходим на страницу
driver.get('http://localhost:3000')
driver.wait(function () {
return driver
// Запускаем скрипт
.executeScript('return document.readyState')
.then(function (readyState) {
// Проверяем что все в порядке
return readyState === 'complete'
})
})
Cypress
// Вся работа происходит внутри метода
cy.visit('http://localhost:3000')
Playwright and Puppeteer
// Тоже самое в playwright и puppeteer
await page.goto('http://localhost:3000')
Ожидания изменений на странице
Selenium
// Ожидание элемента
driver.wait(until.elementLocated(By.id('#form-feedback')), 4000)
// Ожидание элемента с определенным контентом
const el = driver.wait(until.elementLocated(By.id('#form-feedback')), 4000)
wait.until(ExpectedConditions.textToBePresentInElement(el, 'Success'))
Cypress
// Ожидание элемента
cy.get('#form-feedback', { timeout: 5000 }) // 4 секунды по умолчанию
// Ожидание элемента с определенным контентом
cy.get('#form-feedback').contains('Success')
Playwright и Puppeteer
// Ожидание элемента
await page.waitForSelector('#form-feedback', { timeout: 5000 }) // 30 секунд по умолчанию
// Ожидание элемента с определенным контентом
await page.waitForFunction((selector) => {
const el = document.querySelector(selector)
return el && el.textContent === 'Success'
},
{},
'#form-feedback',
)
Ожидание запросов
Selenium
// Указываем url, за которым хотим следить
driver.get('https://mail.ru/api/users')
const mydynamicelement = (new webdriverwait(driver, 10))
.until(expectedconditions.presenceofelementlocated(by.id('mydynamicelement')))
Cypress
cy.intercept('https://mail.ru/api/users').as('users')
cy.wait('@users')
.its('response.body')
.then((body) => {
// ...
})
Playwright and Puppeteer
// Указываем url, на который ожидаем запрос
await page.waitForRequest('https://mail.ru/api/users')
// Указываем url, на который ожидаем ответ
const response = await page.waitForResponse(
'https://mail.ru/api/users',
)
const body = response.json()
Ожидание кастомных изменений
Мы хотим дождаться, пока глобальной переменной user
не будет присвоено значение Ivan
Selenium
browser.executeAsyncScript(`
window.setTimeout(function() {
if(window.user === 'Ivan') {
arguments[arguments.length - 1]();
}
}, 300);
`)
Cypress
// Используется плагин cypress-wait-until
cy.waitUntil(() => cy.window().then(win => win.user === 'Ivan'))
Playwright и Puppeteer
await page.waitForFunction('window.user === "Ivan"')
Итог
Ниже плохой пример, в нем используется магическое число и код останавливается на какое-то время. Не факт, что обработка события успеет завершиться к этому времени
test('can logout', async () => {
await page.click('#menu div > a');
sleep 500;
// ...
});
Ниже правильный пример. Указываем какие элементы ожидать:
test('can logout', async () => {
await page.click('[data-testid="userMenuButton"]')
await page.waitForSelector('[data-testid="userMenuOpen"]')
await page.click('[data-testid="logoutLink"]')
await page.waitForSelector('[data-testid="userLoginForm"]')
})
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.