Playwright як інструмент для автоматизації тестування доступності
Усім привіт! Мене звати Віктор і я займаюсь автоматизацією тестування за допомогою Playwright, використовуючи мову програмування TypeScript. Нещодавно на проєкті, де я працюю, виникла потреба автоматизувати процес тестування доступності. Це цікава тема, яку я вирішив дослідити та зрештою впровадити процес на практиці.
В Levi9 регулярно проводяться мітапи для розробників різних напрямів, як-от Python, Java, QA тощо. На одному з них я якраз ділився своїм досвідом з автоматизації тестування, щоб допомогти колегам полегшити роботу та знайти нові ідеї. Готуючи презентацію, я оцінив ситуацію з іншої сторони і в результаті покращив попередні рішення.
В цій статті хочу поділитися набутим досвідом. А оскільки в роботі я використовую TypeSctipt, то всі представлені в статті фрагменти коду будуть написані на ньому.
1. Тестування доступності
Загалом тестування доступності (Accessibility Testing) — це велика та важлива тема для компаній і проєктів, які піклуються про своїх клієнтів, тому якщо ви зовсім не знайомі з тестуванням доступності, то раджу ознайомитись із цією темою.
Варто зазначити, що на сайті Playwright представлена детальна документація щодо його використання для таких тестів, тож я не дублюватиму усе тут, а зосереджусь на вузьких місцях і можливих шляхах їх вирішення.
2. Базові налаштування
Щоб тестувати доступність за допомогою Playwright, потрібно додатково встановити бібліотеку @axe-core/playwright за допомогою якої і відбуватиметься сканування.
npm i @axe-core/playwright
Після того як бібліотеку встановлено, можна написати перший тест.
//test-file.spec.ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('Test name', async ({ page }, testInfo) => { await page.goto('https://your-site.com/'); const scanResults = await new AxeBuilder({ page }).analyze(); expect(scanResults.violations).toEqual([]); });
Як бачимо, в тесті все відбувається в 3 кроки:
1) перехід на потрібну сторінку;
2) сканування за допомогою AxeBuilder();
3) порівняння отриманих та очікуваних результатів.
Важливо розуміти, що сканування відбувається за принципом фотографії: якщо на сторінці є модальні вікна чи списки, що випадають, їх потрібно попередньо відкрити, щоб протестувати.
Крім того, варто враховувати, що результати сканування містять не лише порушення, тому для коректного порівняння в expect() потрібно передавати violations.
Якщо є порушення, то після тесту ми отримаємо звіт з детальною інформацією про порушені правила та місця, де це відбулося, а також інформацію щодо можливих варіантів вирішення.
[ Object { "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds", "help": "Elements must have sufficient color contrast", "helpUrl": "https://dequeuniversity.com/rules/axe/4.6/color-contrast?application=playwright", "id": "color-contrast", "impact": "serious", "nodes": Array[ Object { "all": Array[], "any": Array[ Object { "data": Object { "bgColor": "#ebedf0", "contrastRatio": 2.27, "expectedContrastRatio": "4.5:1", "fgColor": "#969faf", "fontSize": "12.0pt (16px)", "fontWeight": "normal", "messageKey": null, "shadowColor": undefined, }, "id": "color-contrast", "impact": "serious", "message": "Element has insufficient color contrast of 2.27 (foreground color: #969faf, background color: #ebedf0, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1", "relatedNodes": Array[ Object { "html": "<button type=\"button\" class=\"DocSearch DocSearch-Button\" aria-label=\"Search\">", "target": Array[ ".DocSearch", ], }, ], }, ], "failureSummary": "Fix any of the following: Element has insufficient color contrast of 2.27(foreground color: #969faf, background color: #ebedf0, font size: 12.0pt(16px), font weight: normal).Expected contrast ratio of 4.5: 1", "html": "<span class=\"DocSearch-Button-Placeholder\">Search</span>", "impact": "serious", "none": Array[], "target": Array[ ".DocSearch-Button-Placeholder", ], }, ], "tags": Array[ "cat.color", "wcag2aa", "wcag143", "ACT", ], }, ]
Загалом інформації у звіті достатньо, щоб локалізувати проблему та зрозуміти можливі шляхи її розв’язання.
3. Конфігурування аналізатора
Щоб отримати оптимальний ефект від тестування, аналіз можна досить гнучко конфігурувати. Цей ефект досягається використанням методів, які дозволяють встановлювати рівень рекомендацій чи набір правил, корегуючи їх шляхом виключення з перевірки неактуальних правил.
Зручно також виключити з перевірки певну частину сторінки і тестувати лише ту, що необхідна. Для себе я обрав наступну конфігурацію, де в include() залежно від конкретного тесту буде передано необхідний селектор або body за замовчуванням.
//test-file.spec.ts test('Test name', async ({ page }) => { await page.goto('https://your-site.com/); const scanResults = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) // набір правил .disableRules(['color-contrast']) // виключення з перевірки певного правила .include('selector') // обмеження частини сторінки для перевірки .analyze(); expect(scanResults.violations).toEqual([]); });
4. Оптимізація використання
Для оптимізації використання аналізатора в тестах розробники Playwright пропонують використовувати фікстури. Проте, експериментуючи, я знайшов зручніший варіант.
Оскільки Playwright дозволяє розширити expect, то я вирішив зробити метод toBeAccessible(). В результаті це дає зручніший і лаконічніший синтаксис для написання тестів, а саме:
//test-file.spec.ts test('Test name', async ({ page }, testInfo) => { await page.goto('https://your-site.com/); await expect(page).toBeAccessible(testInfo, 'selector'); });
Для цього потрібно змінити 3 файли. Перший — playwright.config.ts.
// playwright.config.ts import { expect, PlaywrightTestConfig } from '@playwright/test'; expect.extend({ async toBeAccessible(page: Page, testInfo: TestInfo, include?: string) { const scanResults = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .disableRules(['color-contrast']) .include(include || 'body') .analyze(); if (scanResults.violations.length == 0) { return { message: () => 'pass', pass: true, }; } else { await testInfo.attach('accessibility-scan-results', { body: JSON.stringify(scanResults.violations, null, 2), contentType: 'application/json', }); return { message: () => `${scanResults.violations.length} violated rules were found.`, pass: false, }; } }, });
Як і при використанні фікстур, така конструкція дозволяє змінити конфігурацію аналізатора під конкретний тест. Додатково «під капотом» вона приховує крок зі збереженням звіту про порушення у вигляді застосунку до основного звіту.
Це значно полегшує роботу зі звітами, якщо порушень більше одного, адже їхній розмір може бути й на сотні рядків.
І оскільки я використовую TypeScript, то для коректної типізації додатково потрібно змінити ще й файл global.d.ts
//global.d.ts import { TestInfo } from '@playwright/test'; declare global { namespace PlaywrightTest { interface Matchers<R> { toBeAccessible(testInfo: TestInfo, include?: string): Promise<R>; } } }
А також tsconfig.json, щоб IDE розпізнавала ці типи:
//tsconfig.json { "compilerOptions": { "typeRoots": [ "global.d.ts" ] } }
5. Використання тестів
Тести по перевірці доступності можна використовувати окремо чи міксувати з основними. Я вирішив виокремити їх в блок, використовуючи тег @a11y, щоб простіше маніпулювати наборами тестів для різних потреб.
Доцільним для таких тестів є також використання кроків разом з expect.soft(). Таке рішення дозволяє покроково сканувати сторінку без втрати порушення всередині кроків, а також суттєво економить час. Прикладом такого використання є:
test('Test name @a11y', async ({ page, somePage }, testInfo) => { await page.goto('https://your-site.com/); await test.step('Scan whole page', async () => { await expect.soft(page).toBeAccessible(testInfo); }); await test.step('Scan modal window', async () => { await somePage.modalWindow.open(); await expect.soft(page).toBeAccessible(testInfo, 'modal-window-selector'); }); });
На цьому прикладі також показано перевагу використання обмеженого сканування частини сторінки. І якщо буде знайдено порушення для
6. Складнощі
При написанні тестів я виявив проблему, яку вже частково описували розробники Playwright: для коректного сканування певної частини сторінки потрібно дочекатися поки вона стане видимою.
Для цього вони пропонують використовувати waitFor().
await page.locator('selector').waitFor();
Все ж іноді така конструкція незручна. Наприклад, коли треба протестувати модальне вікно, я можу вказати селектор його контейнера, щоб дочекатись видимості перед скануванням.
Проте якщо елементи цього модального вікна підвантажуються асинхронно, то система перевірить їх некоректно чи взагалі просканує пустий контейнер. В обох випадках результати тестування будуть хибні.
Висновки
Playwright в поєднанні з бібліотекою @axe-core — досить гнучкий і зручний інструмент для автоматизованого тестування доступності, хоча й не покриває всі його потреби. І це варто враховувати, якщо ваш продукт має такі зобов’язання.
Проте варто зважити, що ефективність використання будь-якого інструменту здебільшого залежить від того, хто його використовує, тому завжди потрібно думати про можливі способи покращення.
27 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів