QA Interview Hub · База знань · Тести
Cypress
Черга команд і асинхронність
чому немає await
Чому в Cypress не можна писати await cy.get()?
Команди Cypress повертають не Promise, а Chainable-об'єкт. Виклик cy.get() нічого не виконує в цю мить — він лише ставить команду в чергу.
Коли функція тесту завершується, Cypress сам проганяє чергу по порядку: кожна команда чекає на попередню. Тому await тут або нічого не дасть, або зламає порядок виконання. Значення дістають через .then(), а перевіряють через .should().
// ❌ не працює так, як очікуєш
const text = await cy.get('h1').invoke('text');
// ✅ значення — через .then
cy.get('h1').invoke('text').then((text) => {
expect(text).to.contain('Кошик');
});
// ✅ перевірка — через should (з ретраями)
cy.get('h1').should('contain', 'Кошик');
На співбесіді: Тому що команди Cypress повертають не Promise, а chainable-об'єкт. Виклик cy.get лише ставить команду в чергу, а виконує цю чергу сам Cypress, послідовно, після того як тіло тесту відпрацювало. Await тут або нічого не дасть, або зламає порядок. Значення я дістаю через then, а перевіряю через should, бо should ще й ретраїться.
А в Playwright:У Playwright навпаки: кожна дія повертає справжній Promise, і await обов'язковий. Типова помилка там — забутий await: дія не дочекається, тест стане флакі. Її ловить ESLint-правило no-floating-promises.
const text = await page.locator('h1').textContent();
await expect(page.locator('h1')).toContainText('Кошик');
Що таке subject і як команди передають значення по ланцюжку?
Кожна команда «віддає» (yields) результат наступній — це і є subject. cy.get('li') віддає jQuery-колекцію елементів, .its('length') віддає число, .invoke('text') — рядок.
Є команди, які починають новий ланцюжок (cy.get, cy.visit, cy.request), і команди, які працюють із subject попередньої (.find, .click, .should). Дії на кшталт .click() зазвичай віддають той самий елемент далі.
cy.get('.cart-item') // yields: усі елементи кошика
.its('length') // yields: число
.should('eq', 3);
cy.get('[data-cy=email]')
.type('qa@test.com') // yields: той самий input
.should('have.value', 'qa@test.com');
На співбесіді: Subject — це результат, який одна команда передає наступній по ланцюжку. Наприклад, cy.get передає знайдені елементи, its length — число, invoke text — рядок. Одні команди починають новий ланцюжок, як cy.get чи cy.request, інші працюють з тим, що прийшло від попередньої, як find, click або should.
А в Playwright:Ланцюжка subject немає. Локатор — звичайний об'єкт, його можна зберегти у змінну і звужувати (locator.locator()), а значення повертає await.
Як зберегти значення між командами: змінні, .then, aliases?
Запис const el = cy.get('h1') не дає елемента — це Chainable. Є три правильні способи:
- .then() — працюєш зі значенням усередині колбеку.
- Alias через
.as('name') — потім cy.get('@name'). Для DOM і перехоплень alias перешукує актуальне значення.
- this.name — лише у звичайній
function(), не в стрілковій, бо стрілка не має свого this.
Aliases очищаються між тестами, тому створюй їх у beforeEach.
beforeEach(() => {
cy.fixture('user.json').as('user');
});
it('логін', function () { // function, не стрілка
cy.get('[data-cy=email]').type(this.user.email);
});
cy.get('[data-cy=total]').invoke('text').as('total');
cy.get('@total').should('match', /\d+/);
На співбесіді: Звичайна змінна з cy.get — це не елемент, а chainable, тому так не можна. Значення я передаю або через then, або через alias: позначаю результат через as і потім дістаю через cy.get з собачкою. Ще можна через this, але тільки в звичайній function, бо в стрілковій функції this не той. Aliases скидаються між тестами, тому я створюю їх у beforeEach.
А в Playwright:Aliases не потрібні: значення лягає у звичайну змінну.
const total = await page.getByTestId('total').textContent();
const user = JSON.parse(fs.readFileSync('data/user.json', 'utf8'));
Чим .then() відрізняється від .should() з колбеком?
.should(cb) ретраїться: Cypress повторно виконує попередні queries і колбек, поки всі асерти всередині не пройдуть або не мине таймаут. Тому в ньому не можна викликати інші cy-команди і робити побічні дії.
.then(cb) виконується один раз. Він для того, щоб дістати значення і запустити наступні команди.
// ✅ ретраїться, поки умова не стане true
cy.get('.status').should(($el) => {
expect($el.text().trim()).to.eq('Оплачено');
});
// ⚠️ виконається один раз: якщо статус ще не оновився — впаде
cy.get('.status').then(($el) => {
expect($el.text().trim()).to.eq('Оплачено');
});
На співбесіді: Should з колбеком ретраїться: Cypress повторює пошук елемента й перевірку, поки асерт не пройде. Тому всередині should не можна викликати інші cy-команди. Then виконується рівно один раз, він для того, щоб дістати значення і продовжити ланцюжок. Якщо перевіряю стан, який змінюється з часом, я обираю should.
А в Playwright:Аналог .should(cb) з ретраями — expect.poll() для значення або expect(async () => { ... }).toPass() для блоку асертів. Одноразова перевірка — expect(await locator.textContent()).toBe(...), як .then у Cypress.
Як поєднувати Cypress із Promise і звичайним async-кодом?
cy.wrap(promise) додає Promise в чергу: Cypress дочекається resolve і передасть значення далі. Якщо Promise відхилиться, тест упаде.
Для коду, який має виконатися в Node (база даних, файли, генерація токенів), використовують cy.task: він повертає значення або Promise з Node-процесу в тест.
Головне правило — не змішувати: не повертай Promise з тесту і не роби await на cy-командах в одному блоці.
const getToken = () => fetch('/api/token').then((r) => r.json());
cy.wrap(getToken()).its('token').then((token) => {
cy.setCookie('auth', token);
});
cy.task('db:getUser', { email: 'qa@test.com' })
.its('status')
.should('eq', 'active');
На співбесіді: Для Promise є cy.wrap: я загортаю Promise, Cypress чекає на нього в черзі і передає значення далі. А код, який має виконуватися в Node, наприклад запити до бази, я виношу в cy.task — він повертає результат прямо в тест. Головне — не змішувати черги Cypress і await в одному місці, бо порядок виконання стає непередбачуваним.
А в Playwright:Проблеми змішування немає: Playwright — це звичайний async/await, тож працюють Promise.all і будь-які бібліотеки. Аналог cy.task не потрібен, бо тест і так виконується в Node — до бази можна звернутися прямо з тесту.
Асинхронність команд Cypress
Команди Cypress не виконуються одразу: під час виклику вони лише додаються в чергу, а виконуються послідовно пізніше. Вони повертають не проміс, а об'єкт-ланцюжок (chainable), тому await cy.get(...) не спрацює як очікуєш. Змішування async/await з командами ламає порядок черги.
Через це не можна просто присвоїти результат у змінну: const text = cy.get('h1').text() не дасть тексту. Щоб отримати значення, використовують .then(), куди передається результат попередньої команди. Для перевикористання між кроками — аліаси через .as() та cy.get('@alias').
Звичайний JS-код поза командами виконується одразу, до команд у черзі, — це часте джерело багів. Тому умовну логіку й обчислення зі значеннями зі сторінки розміщують усередині .then().