QA Interview Hub · База знань · Тести
Cypress
Retry-ability, очікування, таймаути
звідки береться стабільність
Що таке retry-ability і що саме повторюється?
Cypress ділить команди на три групи, і поводяться вони по-різному.
| Група | Приклади | Поведінка |
|---|
| Queries | get, find, contains, its, invoke, eq, filter, first | Лише читають стан — їх безпечно повторювати |
| Assertions | should, and | Нічого не шукають, але саме вони запускають повтори |
| Actions | click, type, check, select | Змінюють стан — виконуються один раз |
Як працює цикл повторів
Коли за query стоїть асерт, Cypress знаходить елементи, перевіряє асерт, і якщо той не пройшов, через кілька мілісекунд повторює обидва кроки — і пошук, і перевірку. Зупиняється, коли асерт пройшов або минув defaultCommandTimeout — 4 секунди.
cy.get('[data-cy=cart-count]').should('have.text', '3');
// кожну мить: знайти елемент → перевірити текст → не 3? ще раз
З Cypress 12 повторюється весь ланцюжок queries з початку, а не лише остання команда. Раніше Cypress міг тримати старий елемент, який React чи Vue вже перемалював, і падати з помилкою «element is detached from the DOM». Тепер елемент щоразу шукається заново.
cy.get('[data-cy=order-list]')
.find('[data-cy=order]')
.first()
.should('contain', 'Оплачено');
// при кожній спробі заново: get → find → first → should
Кілька асертів у ланцюжку мають пройти одночасно, в одній спробі:
cy.get('[data-cy=submit]')
.should('be.visible')
.and('not.be.disabled')
.and('have.text', 'Оплатити');
Чому дії не повторюються
Якби click на «Оплатити» ретраївся, а сторінка ще не встигла оновитися, Cypress клікнув би вдруге — і замовлення створилося б двічі. Тому дія виконується рівно один раз.
Але перед цим вона чекає actionability, теж до 4 секунд. Елемент має бути в DOM, видимим, не disabled (для type ще й не readonly), не анімованим і не перекритим модалкою чи лоадером. Перед кліком Cypress ще й прокручує елемент у видиму зону. Тобто «один раз» не означає «наосліп».
Пастки, на яких ловлять
1. .then вбиває ретраї. Колбек у .then виконується один раз.
// ❌ якщо список ще вантажиться — впаде одразу
cy.get('li').then(($li) => {
expect($li).to.have.length(3);
});
// ✅ ретраїться
cy.get('li').should('have.length', 3);
// ✅ теж ретраїться — колбек усередині should
cy.get('li').should(($li) => {
expect($li).to.have.length(3);
});
2. Асерт одразу після дії. Після .click() у ланцюжку немає query, яку можна перезапустити, тож .should перевіряє той самий елемент, який віддав клік. Якщо застосунок його перемалював, тест упаде. Надійніше почати новий ланцюжок.
// ⚠️ ризиковано
cy.get('[data-cy=tab-stats]').click().should('have.class', 'active');
// ✅ надійно: новий ланцюжок з query
cy.get('[data-cy=tab-stats]').click();
cy.get('[data-cy=tab-stats]').should('have.class', 'active');
3. .invoke з побічним ефектом. Це query, тож під час ретраїв метод викликається кілька разів. Для читання значення нормально, для функції, що щось змінює, — небезпечно.
4. Що не ретраїться взагалі. cy.visit, cy.request, cy.task, cy.wait, .then мають лише власні таймаути, а не цикл повторів.
На співбесіді: У Cypress ретраяться тільки queries — команди, які читають DOM, як get, find чи contains, — разом із should, що стоїть за ними. З дванадцятої версії Cypress при кожній спробі перезапускає весь ланцюжок queries, тому елемент щоразу шукається заново. Дії на кшталт click чи type виконуються один раз, бо повторний клік міг би, наприклад, двічі створити замовлення. Але перед виконанням вони чекають, поки елемент стане видимим, активним, стабільним і не перекритим. Звідси дві практичні речі: перевірки я пишу через should, а не then, бо then не ретраїться, а після дії починаю новий ланцюжок з cy.get, щоб асерт міг перешукати елемент.
А в Playwright:Механізм інший: чекають і дії, і асерти. Дія перед виконанням перевіряє actionability і щоразу перешукує елемент за локатором, а expect(locator) ретраїться до 5 секунд. Тому в Playwright немає поділу на queries і actions та немає пастки з .then. Але є своя: expect(await locator.isVisible()).toBe(true) перевіряє стан один раз і не ретраїться.
Queries, actions і assertions — у чому різниця?
| Тип | Приклади | Поведінка |
| Queries | get, find, contains, its, invoke, eq, filter | Повторюються, поки наступний асерт не пройде |
| Assertions | should, and | Змушують попередні queries повторюватись |
| Actions | click, type, check, select | Чекають actionability, потім виконуються один раз |
| Інші | visit, request, wait, then | Не ретраяться, мають власні таймаути |
Практичний висновок: асерт став одразу після query, а не після дії. Ланцюжок .click().should(...) перевіряє вже результат кліку без повторного кліку.
На співбесіді: Queries — це запити до DOM: get, find, contains, its. Вони повторюються, поки асерт за ними не пройде. Assertions — should і and — саме вони запускають ці повтори. Actions — click, type, select — чекають, поки елемент стане доступним, але виконуються один раз, бо повторний клік міг би зламати стан застосунку.
А в Playwright:Такого поділу немає. Створення локатора нічого не робить, дія чекає готовності елемента, а асерт expect(locator) повторюється. Не ретраїться лише асерт над уже отриманим значенням: expect(await locator.isVisible()).toBe(true).
Чому cy.wait(3000) — антипатерн і що використовувати замість?
Фіксована пауза завжди вгадана: якщо застосунок відповів за пів секунди, тест витрачає ще дві з половиною даремно; якщо за чотири — тест падає. Тобто тест одночасно повільний і нестабільний.
Замість паузи чекай конкретний стан: асерт на елемент або завершення конкретного запиту через alias.
// ❌
cy.get('[data-cy=save]').click();
cy.wait(3000);
// ✅ чекаємо стан у UI
cy.get('[data-cy=toast]').should('contain', 'Збережено');
// ✅ чекаємо конкретний запит
cy.intercept('POST', '/api/profile').as('save');
cy.get('[data-cy=save]').click();
cy.wait('@save').its('response.statusCode').should('eq', 200);
На співбесіді: Тому що це вгадане число: якщо застосунок відповів швидше, тест чекає даремно, а якщо повільніше — тест падає. Замість цього я чекаю конкретний стан: або асерт на елемент, який сам ретраїться, або завершення конкретного запиту через intercept з alias і cy.wait на цей alias.
А в Playwright:Те саме правило: page.waitForTimeout() — антипатерн. Замість нього web-first assertion або page.waitForResponse().
Які таймаути є в Cypress і як їх перевизначати?
| Таймаут | За замовчуванням | На що діє |
defaultCommandTimeout | 4 с | Queries, асерти, actionability |
requestTimeout | 5 с | Скільки cy.wait('@alias') чекає, поки запит відправиться |
responseTimeout | 30 с | Відповідь на cy.request і на перехоплений запит |
pageLoadTimeout | 60 с | Завантаження сторінки в cy.visit |
taskTimeout | 60 с | cy.task |
Збільшуй таймаут точково, для конкретної команди, а не глобально — інакше ховаєш проблему.
cy.get('[data-cy=report]', { timeout: 15000 }).should('be.visible');
describe('повільні звіти', { defaultCommandTimeout: 10000 }, () => {
it('генерує PDF', () => { /* ... */ });
});
На співбесіді: Основний — defaultCommandTimeout, чотири секунди, на пошук елементів і асерти. Для мережі є requestTimeout, п'ять секунд на те, щоб запит відправився, і responseTimeout, тридцять секунд на відповідь. Ще є pageLoadTimeout на cy.visit і taskTimeout на cy.task. Перевизначаю я їх точково — опцією timeout у конкретній команді або в конфігу describe, а не глобально.
А в Playwright:
| Таймаут | За замовчуванням |
Тест цілком (timeout) | 30 с |
expect | 5 с |
actionTimeout | без окремого ліміту, обмежений тестом |
navigationTimeout | без окремого ліміту, обмежений тестом |
globalTimeout | весь прогін |
Ключова різниця: у Playwright є таймаут на весь тест, у Cypress його за замовчуванням немає.
Тест флакує в CI. Як шукаєш причину в Cypress?
Зібрати докази. Скріншот при падінні робиться автоматично в cypress run. Відео з Cypress 13 вимкнене за замовчуванням — увімкни video: true для CI. У Cypress Cloud є Test Replay з DOM, мережею й консоллю.
Відтворити. Прогнати один спек багато разів, наприклад циклом Cypress._.times або плагіном на кшталт cypress-burn, з тими самими ресурсами, що в CI.
Типові причини: cy.wait(ms) замість очікування стану; дія до того, як прийшла відповідь API; спільні тестові дані між тестами; анімації; залежність від порядку тестів; intercept, зареєстрований після дії.
Retries — retries: { runMode: 2, openMode: 0 } — лише тимчасова пов'язка. Кожен тест, що пройшов з другої спроби, має закінчуватись розслідуванням.
Cypress._.times(20, (i) => {
it('оформлення замовлення #' + i, () => {
// тест, що флакує
});
});
На співбесіді: Спочатку збираю докази: скріншот при падінні, відео — його треба явно увімкнути, бо з тринадцятої версії воно вимкнене, — і Test Replay, якщо є Cloud. Потім відтворюю: проганяю спек десятки разів у циклі. Далі перевіряю типові причини: жорсткі cy.wait, дії до відповіді API, спільні дані, анімації, intercept, поставлений уже після дії. Retries в runMode допомагають дожити до релізу, але не лікують — кожен такий тест я розбираю.
А в Playwright:Головний інструмент — Trace Viewer: trace: 'on-first-retry' дає таймлайн із DOM-знімками, мережею й консоллю без платного сервісу. Відтворення — --repeat-each=20, перевірка конкуренції за дані — --workers=1.
Очікування і retry-ability
Cypress автоматично чекає: query-команди, як cy.get, повторюються, поки елемент не знайдеться або не мине таймаут, за замовчуванням 4 секунди (defaultCommandTimeout). Асершени через .should() теж ретраяться разом із попереднім query. Дії, як click і type, чекають, поки елемент стане видимим, не перекритим і не disabled.
Ретраїться лише останній query перед асершеном, а action-команди, як click, не повторюються. Тому ланцюжок бажано будувати так, щоб query і перевірка йшли разом. Таймаут можна перевизначити для конкретної команди через параметр timeout.
cy.wait(5000) погано, бо це фіксована пауза: на швидкому середовищі марно гальмує прогін, на повільному все одно може не вистачити. Правильно чекати на конкретну умову: видимість елемента через should, або мережевий запит через cy.intercept з аліасом і cy.wait('@alias').