Як забезпечити стабільність і продуктивність e2e-тестів на TypeScript з Puppeteer
Привіт усім!
Мене звуть Ярослав. Я працюю в компанії WiX в ролі QA Automation / SDET, і сьогодні я хочу поділитися з вами нашим досвідом використання Puppeteer та стабілізації e2e тестів на TypeScript.
Я працюю на проєкті EditorX. EditorX — це платформа для створення адаптивних сайтів за технологією drag & drop. За кілька років на нашому проєкті ми написали значну кількість e2e-тестів (2000+) на TypeScript, але постійно стикалися з проблемами нестабільності в e2e через те, що більшість логіки взаємодії юзера з сайт білдером виконується на фронтенді, наслідки дій можуть відбутися через деякий час після того, як дія була зроблена.
Puppeteer, зазвичай, значно швидше виконується, аніж система реагує на команди.
Спочатку ми використовували базові методи Puppeteer та нативні Jest порівняння, але з часом зрозуміли, що нам потрібно щось більше для досягнення стабільності білда.
Що ми вирішили змінювати
В нас на проєкті імплементовано процес Continuous Integration. Повний тест сьют раниться на готовий пул-реквест, і код може потрапити до master гілки тільки після проходження всіх тестів.
Вимоги до успішності (SR) не менше 99,6% (за виключенням падіння з причин реальних багів або інфри). Якщо тест не відповідає умовам стабільності, він скіпається.
Щоб вирішити проблеми з SR та покращити стабільність наших e2e-тестів, ми написали власні утіліти всередині нашого проєкту та згодом побачили, що інші команди мають такі ж проблеми зі стабільністю, які ми мали у минулому.
Тому ми вирішили їх винести в опен-сорс бібліотеку bubanai.
Bubanai з івріту перекладається як ляльковод, який маніпулює нитками ляльок.
Можливості, які надає Bubanai
Ця опен-сорс бібліотека надає такі можливості:
- Спрощена робота з методами Puppeteer шляхом додавання очікувань всередині загальних методів, таких як click і hover, etc.
- Швидкий доступ до атрибутів/ властивостей елементів.
- Очікування для елементів, наприклад, очікування, коли елемент буде мати певний атрибут або його частину; очікування, коли позиція елемента не буде змінюватись тощо.
- Очікування виконання функцій згідно з заданими умовами.
- Методи роботи з контекстами.
- Методи роботи з колекціями елементів.
- Спрощений drag & drop.
- Логування для вискорівневих методів в тесті
- Cкрол-утиліти.
- Робота з клавіатурою.
- Console & Network listeners.
- Утиліти для вкладених елементів, які вже ініціалізовані (корисно для ранніх версій Puppeteer).
- Утиліти для дропдаунів.
- etc.
Типові проблеми, які вирішує ця бібліотека:
Приклад 1
Припустимо, у вас є компонент, якому ви змінюєте колір через деякий час після кліка.
Ви точно знаєте колір, який ви йому задаєте, але після зміни колір не одразу набуває заданого стану.
import { getElementBackgroundColor, waitForObjectsToBeEqual } from 'bubanai-ng'; ... const expectedColor = {r: 200, g: 200, b: 200}; await colorPickerDriver.setColor(expectedColor); await waitForObjectsToBeEqual(() => getBackgroundColor(context, element), expectedColor); ...
Завдяки цьому шматку коду ми точно дочекаємось, коли умова виконується за мінімально можливий проміжок часу (retry за замовчанням 0.5s між викликами функції).
Приклад 2
У вас є панель, з якої ви додаєте елементи в builder. Ви робите це завдяки drag & drop. Коли ви тягнете елемент з панелі, тa в процесі драгу натискаєте Enter, вона повинна закриватись.
Але якщо ви тягнете елемент всередині області панелі, вона закриватися не буде.
Вам потрібно помістити елемент під областю панелі (тобто, виконати проміжковий драг степ і екшн).
import { getCenter, dragElementToPoint } from 'bubanai-ng'; ... const panelCenter = getCenter(panel); const stageCenter = getCenter(stageBounding); await dragElementToPoint(context, element, panelCenter, { tempSteps: { point: stageCenter, action: () => pressEnter(),} }); ...
Приклад 3
Вам потрібно написати тест на використання hotkey.
У вас є ефект ласо, котрий змінює логіку drag & drop в процесі при затисканні комбінації на клавіатурі.
import { getBoundingBox, dragElementBy, holdAndExecute } from 'bubanai-ng'; ... const elementBounding = await getBoundingBox(element); const action = async () => dragElementBy(context, element, 100, 10); await holdAndExecute(['Shift', MetaKeys.ControlOrCommand], action); const boundingAfter = await getBoundingBox(element); expect(boundingAfter.x).toBe(elementBounding.x + 100); expect(boundingAfter.y).toBe(elementBounding.y); ...
Приклад 4
В логах на CI Typescript транслюється на JavaScript, і рядки падіння в тесті відрізняються від тайпскриптових (або взагалі заперечення кидається десь глибоко на нижньому рівні).
Вам потрібно розуміти, які функції викликались на хай левелі та з якими аргументами в самому тест-файлі.
import { log } from 'bubanai-ng'; export class ColorModalDriver { @log async setColor(color) { } @log async openModal() { } @log async closeModal() { } } ...
Test file :
... await colorModal.openModal(); await colorModal.setColor({r: 20, g: 20, b: 20}); await colorModal.closeModal(); ...
Logs:
22/05/23 13:01:43 Calling ColorModalDriver.openModal(); 22/05/23 13:01:53 Calling ColorModalDriver.setColor({"r": "20", "g": "20", "b": "20"}) 22/05/23 13:02:15 Calling ColorModalDriver.closeModal();
Приклад 5
У вас є структура, яка може бути як всередині айрейма, так і просто знаходитись в загальному контексті сторінки.
Бібліотека дозволяє маніпулювати контекстами без зайвого болю.
export class VideoPlayerDriver extends ElementBaseDriver { ... }
Test file:
... const playerFrame = await getFrameByUrl(page, 'video-player'); let videoPlayer = new VideoPlayerDriver(page, playerFrame); // play is executed in frame context await videoPlayer.play(); await goToPreview(); videoPlayer = new VideoPlayerDriver(page); // play is executed on page context await videoPlayer.play(); ...
Щоб почати використовувати Bubanai у вашому проєкті, достатньо додати залежність у package.json:
"bubanai-ng": "^2.0.25"
Для використання бібліотеки просто імпортуйте потрібні методи:
import { click, getText } from 'bubanai-ng';
Більшість методів працюють з селекторами CSS або об’єктами ElementHandle.
const { type, getText, getFrameByName } = require('bubanai-ng'); const puppeteer = require('puppeteer'); const assert = require('assert'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); const newTextValue = 'Additional content. '; const areaSelector = 'body'; const frameSelector = 'mce_0_ifr'; await page.goto('http://the-internet.herokuapp.com/tinymce'); const frame = await getFrameByName(page, frameSelector); const currentText = await getText(frame, areaSelector); await type( newTextValue, frame, areaSelector, {}, { clearInput: false }, page, ); const newText = await getText(frame, areaSelector); assert.equal(newText, newTextValue + currentText); await browser.close(); })();
Bubanai також надає розширені можливості, які дозволяють налаштовувати пошук елементів та виконувати складніші дії в Puppeteer-based фреймворках. Більше прикладів ви можете знайти в тестах бібліотеки. Документація до ліби на GitHub.
Bubanai може бути корисною як для початківців, так і для досвідчених автоматизаторів, які працюють з Puppeteer. Використовуючи цю бібліотеку, ви зможете спростити свій процес написання тестів та забезпечити стабільність білда.
Ми також активно розвиваємо бібліотеку і відкриті до співпраці. Якщо у вас є ідеї, пропозиції або доповнення, будь ласка, долучайтеся контриб’юторами до нашого проєкту.
12 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів