Тестирование React. Часть 2: Playwright

Всем привет. Я - Петр Цой. Ищу первую работу на React. В качестве моего резюме выступает сайт petrtcoi.com. Ссылка на GitHub.

В предыдущей статье я рассмотрел unit-тесты моего приложения с помощью библиотеки @testing-library. Здесь опишу для чего и как применяю end-to-end тесты. Использую библиотеку Playwright.

Зачем

Фреймворк Playwright эмулирует работу браузеров, используя их же движки. Если testing-library готовит упрощенную версию HTML документа, то Playwright отрисовывает документ также, как он будет отрисовываться у посетителей сайта (причем может это сделать для разных устройств).

Если в случае с @testing-library ряд функций, завязанных на CSS, мы могли проверить только по косвенным признакам. То теперь мы можем протестировать все напрямую.

Настройка

Для настройки я следовал стандартным инструкциям, размещенных на сайте фреймворка. После чего немного настроил его под себя.

Во-первых, поменял локации стандартных папок в файле playwright.config.ts.

// /playwright.config.ts const config: PlaywrightTestConfig = { testDir: './src/__e2e__', ... reporter: [ ['html', { outputFolder: './src/__e2e__/report' }] ], ... outputDir: './src/__e2e__/test-results/', ... }

Таким образом, тесты переместились в папку src. В этом же файле убрал комментарии для нескольким мобильных устройств. Чтобы работа сайта проверялась и на них: Pixel 5 и iPhone 12.

Во-вторых, прописываем скрипты в package.json

.... "scripts":{ ... "e2e": "npx playwright test", "e2e-update": "npx playwright test --update-snapshots", ... } ...

Первая команда запускает тест. Вторая запускает тест с обновлением сохраненных скриншотов.

В-третьих, на случай роста проекта и появления в нем новых страниц, я сразу добавил объект для хранения адресов страниц. Это упростит читаемость и поддержку кода.

// /src/__e2e__/helpers/pages.ts const baseUrl = 'http://localhost:3000/' export const pages = { home: baseUrl }

Также добавил файл с константами

// /src/__e2e__/helpers/variables.ts export const changeThemeDuatationMs = 500

Проверка работы бокового меню

Видимость и анимация бокового меню реализованы полностью через CSS: на самом компоненте меню установлен атрибут data-state-popupmenu-status={ props.menuStatus }, при переключении которого боковое меню или "выскакивает" или "скрывается".

Код CSS выглядит следующим образом:

/* /src/components/Layout/Popupmenu/popupmenu.module.css .popupmenu { position: fixed; top: 0; right: 0; margin-right: -350px; width: 300px; height: 100%; border-left: 1px solid var(--color-grey); background-color: var(--color-background); transition: margin var(--basic-duration), opacity var(--basic-duration), background-color var(--basic-duration); } .popupmenu[data-state-popupmenu-status="open"] { margin-right: 0px; transition: margin var(--basic-duration), background-color var(--basic-duration); }

При тестировании данного компонента с помощью @testing-libraryвсе, что мы могли сделать, это проверить значение атрибута data-state-popupmenu-status и положиться на то, что CSS отработает смену его значения как требуется.

Благодаря Playwright мы получаем возможность как проверить, находится ли меню в зоне видимости. Для этого создадим вспомогательную функцию:

// /src/__e2e__/utils/isInScreenAxisX.ts import { Locator, Page } from "@playwright/test" type Props = { element: Locator, page: Page } export const isInScreenAxisX = async (props: Props): Promise<boolean> => { let isIn = false const rect = await props.element.boundingBox() const viewportSizes = props.page.viewportSize() if (rect === null) throw new Error("element's boundingBox is null") if (viewportSizes === null) throw new Error("page's viewportSize is null") if (rect.x < viewportSizes.width && (rect.x + rect.width) > 0) { isIn = true } return isIn }

Функция берет значения размеров экрана props.page.viewportSize()и координаты объекта pops.element.boundingBox(), затем просто проверяет, входит ли элемент в границы экрана или нет.

Здесь я проверяю только горизонтальную ось. Поэтому и функция называется так замысловато.

Файл с тестом этой функции выглядит следующим образом. Сначала загружаем страницу и проверяем, что все требуемые элементы на ней удалось найти.

// /src/__e2e__/PopupMenu.spec.ts test.describe('Работа кнопки открытия и закрытия меню', () => { let page: Page let burgerIcon: Locator let popupMenu: Locator let closeMenuIcon: Locator test.beforeEach(async ({ browser }) => { page = await browser.newPage() await page.goto(pages.home) burgerIcon = page.getByRole('button', { name: /открыть меню/i }) popupMenu = page.getByRole('navigation', { name: /popup меню/i }) closeMenuIcon = page.getByRole('button', { name: /закрыть меню/i }) }) test("Все элементы на месте", async () => { await expect(burgerIcon).toBeVisible() await expect(popupMenu).toBeVisible() await expect(closeMenuIcon).toBeVisible() }) ... })

После этого добавляем, собственно проверку, где находится наше меню: за экраном или на нем. Из-за того, что появление и исчезновение меню происходит не моментально, я добавил небольшие паузы: await page.waitForTimeout(changeThemeDuatationMs).

// /src/__e2e__/PopupMenu.spec.ts .... test("Popup menu расположено за размерами экрана", async () => { const isIn = await isInScreenAxisX({ element: popupMenu, page }) expect(isIn).toBe(false) }) test("Popup menu находится в видимости экрана после клика по Burger", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) const isIn = await isInScreenAxisX({ element: popupMenu, page }) expect(isIn).toBe(true) }) test("Меню закрывается после клика по ClodeButton", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) await closeMenuIcon.click() await page.waitForTimeout(changeThemeDuatationMs * 2) const isIn = await isInScreenAxisX({ element: popupMenu, page }) expect(isIn).toBe(false) }) ....

Проверка смены темы сайта и snapshot-тесты

В прошлой статье, при составлении unit-тестов, мы проверили лишь сам вызов функции, меняющей тему, и убедились, что в нее передаются верные аргументы. Но, собственно, смену требуемого атрибута в теге и, тем более, изменение цветовой схемы на сайте, мы проверить не могли.

Как работает смена темы

Цвета на сайте задаются через CSS-переменные и выглядят они так:

/* /src/assets/styles/variables.css */ :root { --color-main: #e6e5e5; --color-grey: #868687; ... } [data-theme="light"] { --color-main: #000000; --color-background: #FFFFFF; }

Смена темы происходит через вспомогательную функцию:

// /src/assets/utls/setTheme.ts import { ThemeColorSchema } from "../types/ui.type" const setUiTheme = (theme: ThemeColorSchema) => { document.documentElement.setAttribute("data-theme", theme) } export { setUiTheme }

Как проверяем

Первый шаг, как обычно, загружаем страницу и убеждаемся, что на ней ей все, что должно быть:

// /src/__e2e___/Theme.spec.ts import { test, expect, type Page, Locator } from '@playwright/test' import { ThemeColorSchema } from '../assets/types/ui.type' import { pages } from './utils/pages' import { changeThemeDuatationMs } from './utils/variables' test.describe('Theme swicthing', () => { let page: Page let themeSwitcher: Locator let burgerIcon: Locator let htmlTag: Locator test.beforeEach(async ({ browser }) => { page = await browser.newPage() await page.goto(pages.home) themeSwitcher = page.getByRole('switch', { name: /переключение темы/i }) burgerIcon = page.getByRole('button', { name: /открыть меню/i }) htmlTag = page.locator('html') }) test("Все элементы на месте", async () => { await expect(themeSwitcher).toBeVisible() await expect(burgerIcon).toBeVisible() await expect(htmlTag).toBeVisible() }) ... })

Далее собственно тесты. Первая группа проверяет, что в тег ставится нужный атрибут.

... test("При загрузке страницы у тега <html> атрибут data-theme НЕ имеет занчение light", async () => { const theme = await htmlTag.getAttribute('data-theme') expect(theme).not.toBe(ThemeColorSchema.light) }) test("Проверка screenshot dark_theme при начальной загрузке", async () => { await htmlTag.getAttribute('data-theme') await expect(page).toHaveScreenshot('dark_theme.png') }) test("Клик по ThemeSwitcher переключает для <html> атрибут data-theme в значение light", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) await themeSwitcher.click() const theme = await htmlTag.getAttribute('data-theme') expect(theme).toBe(ThemeColorSchema.light) }) test("Клик по ThemeSwitcher второй раз вернет тему обратно на DARK", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) await themeSwitcher.click() await themeSwitcher.click() const theme = await htmlTag.getAttribute('data-theme') expect(theme).toBe(ThemeColorSchema.dark) }) ...

Вторая группа тестов делает снимки экранов и, при следующем запуске тестов, проверяет, произошли ли какие-либо изменения. Если да, то генерирует отдельные изображения, где подсвечивает изменения. Тесты выглядят так:

test("Проверка screenshot dark_theme при начальной загрузке", async () => { await htmlTag.getAttribute('data-theme') await expect(page).toHaveScreenshot('dark_theme.png') }) test("Проверка screenshot light_theme после смены темы", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) await themeSwitcher.click() await htmlTag.getAttribute('data-theme') await expect(page).toHaveScreenshot('light_theme.png') }) test("Проверка screenshot daek_theme_2 после повторного переключения темы", async () => { await burgerIcon.click() await page.waitForTimeout(changeThemeDuatationMs) await themeSwitcher.click() await themeSwitcher.click() await expect(page).toHaveScreenshot('dark_theme_2.png') })

Здесь я напрямую указал названия изображений, чтобы было проще ориентироваться. Playwright генерирует изображения и включает название в свои названия:

Тестирование React. Часть 2: Playwright

Теперь можно просто пройтись по ним и посмотреть, выглядит ли сайт, как ожидалось. Если все в порядке, то можно в будущем полагаться, на этот тест, что все работает верно.

Побочным эффектом данного теста является еще и проверка работы бокового меню.

Тестирование React. Часть 2: Playwright

Также, наличие snapshot-теста, в данном случае ставит под вопрос, насколько нужен был предыдущий тест, который проверял тег : он проверял внутренности кода не давая, при этом, никакой информации о конечном результате.

Но, так как он уже написан и смена механизма смены темы не предвидится, то решил его оставить. Возможно, в будущем он окажется полезным.

Заключение

Здесь я рассмотрел работу с фреймворком Playwright. Написание тестов на нем ненамного сложнее, чем на @testing-library. При этом сами тесты выходят более надежными, так как проверки осуществляются в условиях максимально приближенным к реальным.

Snapshot-тесты настраиваются очень просто но, при этом, дают возможность быстро оценить работу сайта "в целом": сменилась ли тема, не накосячили ли мы с CSS, отработала ли логика и т.д. В следующей статье я опишу проверку сайта с использованием Storybook, но возможности Playwright в этой части мне показались более подходящими.

В то же время нужно отметить, что e2e тесты работают заметно медленнее простых unit-тестов и написание кода в стиле TDD мне показалось на них затруднительным. Их область применения - это проверка работа сайта на общем уровне и, как в случае с , дополнительное тестирование там, где проверка unit-тестов может вызывать сомнения в надежности.

11
Начать дискуссию