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"]');
});
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.