Зарегистрируйтесь, чтобы продолжить обучение

E2E Тестирование Тестирование фронтенда

Видео может быть заблокировано из-за расширений браузера. В статье вы найдете решение этой проблемы.

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"]');
});

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

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

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

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

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