Як забезпечити стабільність і продуктивність e2e-тестів на TypeScript з Puppeteer

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Привіт усім!

Мене звуть Ярослав. Я працюю в компанії 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. Використовуючи цю бібліотеку, ви зможете спростити свій процес написання тестів та забезпечити стабільність білда.

Ми також активно розвиваємо бібліотеку і відкриті до співпраці. Якщо у вас є ідеї, пропозиції або доповнення, будь ласка, долучайтеся контриб’юторами до нашого проєкту.

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Тобто ви просто допилили купу своїх костилів до папетіра замість того щоб взяти плейврайт

Не завжди можливо просто взяти і прикрутити ПВ. Рішення про Puppeteer було прийнято ще в 2019 році — тоді ПВ ще не існувало в природі. З того часу накопичувались і костилі, і тести В теперішній момент майже всі команди та інфра заточена під Puppeteer, і та ліба, яка параллелить тести на AWS також заточена на Puppeteer. Тестів на різних вертикалях десятки тисяч (може, навіть сотні). Багато ефорту займе перехід всіх, та і розробники не в захваті — вони вже звикли до теперішнього фрейму і не полюбляють зміни.

До речі, зараз в мене на проекті ПВ, і все одно частина цих «костилів» із ліби чи їх аналогів стає в пригоді.

То я ще вчасно зіскочив з папетіра на плейрайт, зараз би такою ж хріню займався)

Якби не 2000 тестів можна було б переїхати на Playwright і не видумувати таких костилів 🙃

Не завжди це реально) Кор лiба, яка виконує параллелізацію на AWS, теж з самого початку була заточена під Puppeteer, тоді вже прийшлось би всі проекти переводити на Playwright, а це дуже багато ресурсів (до того ж, нема впевненості, що це суттєво покращить SR)

Тоді й справді є сенс у таких бібліотеках якщо SR.

Бо

waitForObjectsToBeEqual

виглядає як щось із селеніуму, коли треба було чекати щось перед ассертом.

Так і є. Це більш стабільна заміна assert. Я більше це порівнюю з Selenide, тільки більш універсальне (та частково костильне) рішення, коли об’єктом порівняння може бути не тільки елемент.
І Puppetter-based фреймворки швидше, ніж Selenium-based, а для систем, де складний фронт, швидкість виконання не завжди грає в плюс. :)

Краще б ви цю кор лібу заопенсорсили

Вже «вони», бо я вже більше року як не в Віксі. Навряд. Було би гарно, але, наскільки я знаю, там не достатньо абстракцій щодо приватної інфри у Віксі + тіму, що розробляла ту лібу, розформували. Ну і вона строго заточена під Пюпетір, який зараз рідко де зустрінеш.

Підписатись на коментарі