Pytania rekrutacyjne Cypress
40 pytań — 20 odpowiedzi dostępnych od razu
Podstawy Cypress
Czym jest Cypress i czym różni się od Selenium?
Odpowiedź w 30 sekund:
Cypress to nowoczesny framework do testowania end-to-end aplikacji webowych, który działa bezpośrednio w przeglądarce obok testowanego kodu. W przeciwieństwie do Selenium, Cypress wykonuje testy w tym samym pętli wykonawczej co aplikacja, co zapewnia szybsze działanie, lepszą kontrolę nad testami i automatyczne oczekiwanie na elementy DOM bez potrzeby jawnych opóźnień.
Odpowiedź w 2 minuty:
Cypress jest open-source'owym narzędziem do testowania end-to-end, które zostało stworzone z myślą o nowoczesnych aplikacjach webowych. Główna różnica w porównaniu do Selenium polega na architekturze – Cypress działa bezpośrednio wewnątrz przeglądarki, w tej samej pętli wykonawczej co testowana aplikacja, podczas gdy Selenium wykonuje polecenia zdalnie przez WebDriver API.
Ta fundamentalna różnica architektoniczna daje Cypress wiele przewag: automatyczne oczekiwanie na elementy i żądania sieciowe (bez potrzeby stosowania explicit/implicit waits), dostęp do wszystkich obiektów aplikacji (window, document, localStorage), natychmiastowe powiadomienia o zmianach w DOM oraz możliwość kontrolowania żądań sieciowych i odpowiedzi serwera. Cypress oferuje również doskonałe narzędzia deweloperskie, w tym Time Travel Debugging, który pozwala przeglądać snapshoty DOM z każdego kroku testu.
Selenium ma jednak swoje zalety w postaci szerszego wsparcia dla różnych przeglądarek (Cypress ma ograniczone wsparcie) oraz możliwości testowania aplikacji wielojęzycznych z różnymi lokalizacjami. Cypress natomiast jest znacznie łatwiejszy w konfiguracji, szybszy w wykonaniu i oferuje lepsze debugging experience, co czyni go idealnym wyborem dla zespołów pracujących z JavaScript/TypeScript.
Warto zauważyć, że Cypress najlepiej sprawdza się w testowaniu nowoczesnych Single Page Applications (SPA) opartych na frameworkach takich jak React, Vue czy Angular, gdzie może w pełni wykorzystać swoje możliwości dostępu do stanu aplikacji.
Przykład kodu:
// Przykład testu Cypress - nowoczesne podejście
describe('Panel logowania', () => {
beforeEach(() => {
// Cypress automatycznie czeka na załadowanie strony
cy.visit('https://example.com/login');
});
it('powinien zalogować użytkownika z poprawnymi danymi', () => {
// Automatyczne czekanie na elementy - nie potrzeba explicit waits
cy.get('[data-testid="email-input"]')
.type('user@example.com');
cy.get('[data-testid="password-input"]')
.type('haslo123');
cy.get('[data-testid="login-button"]')
.click();
// Cypress automatycznie czeka aż URL się zmieni
cy.url().should('include', '/dashboard');
// Asercja na elemencie - Cypress czeka aż element się pojawi
cy.get('[data-testid="welcome-message"]')
.should('contain', 'Witaj');
});
it('powinien wyświetlić błąd przy niepoprawnych danych', () => {
cy.get('[data-testid="email-input"]').type('zly@email.com');
cy.get('[data-testid="password-input"]').type('zlehaslo');
cy.get('[data-testid="login-button"]').click();
// Cypress automatycznie retry'uje asercję aż do timeout
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', 'Nieprawidłowe dane logowania');
});
});
// Ten sam test w Selenium wymagałby explicit waits:
// WebDriverWait wait = new WebDriverWait(driver, 10);
// wait.until(ExpectedConditions.elementToBeClickable(loginButton));
// Cypress - zaawansowane możliwości niedostępne w Selenium
describe('Kontrola żądań sieciowych', () => {
it('powinien przechwycić i zmodyfikować odpowiedź API', () => {
// Przechwycenie żądania sieciowego
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: {
users: [
{ id: 1, name: 'Jan Kowalski' },
{ id: 2, name: 'Anna Nowak' }
]
}
}).as('getUsers');
cy.visit('/users');
// Czekanie na żądanie
cy.wait('@getUsers');
// Weryfikacja wyświetlonych danych
cy.get('[data-testid="user-list"]')
.should('contain', 'Jan Kowalski');
});
it('powinien uzyskać dostęp do obiektu window', () => {
cy.visit('/app');
// Dostęp do obiektów aplikacji - niemożliwe w Selenium
cy.window().then((win) => {
expect(win.localStorage.getItem('token')).to.exist;
});
// Bezpośrednia modyfikacja stanu aplikacji
cy.window().its('store').invoke('dispatch', {
type: 'SET_USER',
payload: { name: 'Test User' }
});
});
});
Diagram:
graph TB
subgraph "Architektura Selenium"
A1[Test Code] -->|WebDriver Protocol| B1[Selenium Server]
B1 -->|Browser Driver| C1[Przeglądarka]
C1 -->|Remote Commands| D1[Aplikacja Web]
style A1 fill:#ff9999
style D1 fill:#99ccff
end
subgraph "Architektura Cypress"
A2[Test Code + Cypress] -->|Ta sama pętla wykonawcza| B2[Przeglądarka]
B2 -->|Bezpośredni dostęp| C2[Aplikacja Web]
A2 -.->|Proxy kontrola| D2[Żądania sieciowe]
style A2 fill:#99ff99
style C2 fill:#99ccff
style D2 fill:#ffcc99
end
subgraph "Kluczowe różnice"
E1[Selenium: Zdalne polecenia<br/>Wymaga explicit waits<br/>Brak dostępu do app internals]
E2[Cypress: Bezpośrednie wykonanie<br/>Automatyczne czekanie<br/>Pełen dostęp do app state]
style E1 fill:#ffcccc
style E2 fill:#ccffcc
end
Materiały:
- Cypress Documentation - Key Differences - Oficjalna dokumentacja opisująca różnice między Cypress a innymi narzędziami
- Cypress vs Selenium: A Comprehensive Comparison - Szczegółowe porównanie obu narzędzi
- Why Cypress? - Uzasadnienie wyboru Cypress i jego architektury
Jak zainstalować i skonfigurować Cypress w projekcie?
Odpowiedź w 30 sekund:
Instalacja Cypress odbywa się przez npm/yarn poleceniem npm install cypress --save-dev. Po instalacji należy uruchomić npx cypress open, co utworzy strukturę katalogów z przykładowymi testami. Następnie konfigurujemy projekt w pliku cypress.config.js, ustawiając baseUrl, timeouty i inne opcje według potrzeb projektu.
Odpowiedź w 2 minuty:
Proces instalacji i konfiguracji Cypress jest prosty i intuicyjny. Najpierw należy dodać Cypress jako dependency deweloperską do projektu używając npm, yarn lub pnpm. Po instalacji pakietu, pierwsze uruchomienie Cypress (przez npx cypress open) automatycznie tworzy pełną strukturę katalogów wraz z plikiem konfiguracyjnym i przykładowymi testami, które pomagają zrozumieć podstawy frameworka.
Głównym plikiem konfiguracyjnym jest cypress.config.js (lub .ts dla TypeScript), w którym definiujemy opcje globalne dla wszystkich testów. Najważniejsze ustawienia to baseUrl (bazowy adres URL aplikacji), viewportWidth i viewportHeight (wymiary okna przeglądarki), oraz różne timeout'y (defaultCommandTimeout, pageLoadTimeout). Można także skonfigurować lokalizacje katalogów z testami, fixtures i screenshots.
Dla projektów używających TypeScript, warto dodać plik tsconfig.json w katalogu cypress z odpowiednimi ustawieniami. Cypress automatycznie rozpoznaje i kompiluje pliki TypeScript bez potrzeby dodatkowej konfiguracji bundlera. Można również skonfigurować zmienne środowiskowe w pliku cypress.env.json lub przez CLI, co jest przydatne dla poufnych danych takich jak klucze API czy dane dostępowe.
Dobrą praktyką jest również dodanie skryptów npm w package.json do uruchamiania testów w różnych trybach (interactive, headless, dla różnych przeglądarek). Po podstawowej konfiguracji projekt jest gotowy do pisania testów end-to-end.
Przykład kodu:
# Instalacja Cypress w projekcie
npm init -y # Jeśli nie masz jeszcze package.json
# Instalacja Cypress jako dev dependency
npm install --save-dev cypress
# Alternatywnie z yarn
yarn add --dev cypress
# Alternatywnie z pnpm
pnpm add -D cypress
# Pierwsze uruchomienie - tworzy strukturę katalogów
npx cypress open
# Uruchomienie w trybie headless (CI/CD)
npx cypress run
# Uruchomienie konkretnego pliku testowego
npx cypress run --spec "cypress/e2e/login.cy.js"
# Uruchomienie w konkretnej przeglądarce
npx cypress run --browser chrome
npx cypress run --browser firefox
npx cypress run --browser edge
// cypress.config.js - podstawowa konfiguracja
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
// Bazowy URL aplikacji - używany w cy.visit()
baseUrl: 'http://localhost:3000',
// Wymiary viewport (okna przeglądarki)
viewportWidth: 1280,
viewportHeight: 720,
// Timeout dla poleceń Cypress (w milisekundach)
defaultCommandTimeout: 10000,
// Timeout dla ładowania strony
pageLoadTimeout: 30000,
// Timeout dla żądań sieciowych
requestTimeout: 10000,
// Timeout dla całego testu
testIsolation: true,
// Katalog z testami e2e
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
// Włączenie nagrywania video
video: true,
// Zapisywanie screenshotów przy błędach
screenshotOnRunFailure: true,
// Setup hooks - wykonywane przed testami
setupNodeEvents(on, config) {
// Tutaj można dodać wtyczki i event listeners
// Przykład: zmiana konfiguracji na podstawie środowiska
if (config.env.environment === 'staging') {
config.baseUrl = 'https://staging.example.com';
}
if (config.env.environment === 'production') {
config.baseUrl = 'https://example.com';
}
return config;
},
},
// Konfiguracja dla testów komponentów (opcjonalnie)
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
},
// Zmienne środowiskowe
env: {
apiUrl: 'http://localhost:4000/api',
environment: 'development',
},
// Retry testów, które się nie powiodły
retries: {
runMode: 2, // Podczas cypress run
openMode: 0, // Podczas cypress open
},
});
// package.json - dodanie skryptów npm
{
"name": "moj-projekt",
"version": "1.0.0",
"scripts": {
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:chrome": "cypress run --browser chrome",
"cy:run:firefox": "cypress run --browser firefox",
"cy:run:headless": "cypress run --headless",
"cy:run:spec": "cypress run --spec",
"test:e2e": "start-server-and-test start http://localhost:3000 cy:run",
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 cy:open"
},
"devDependencies": {
"cypress": "^13.6.0",
"start-server-and-test": "^2.0.3"
}
}
// cypress/tsconfig.json - konfiguracja TypeScript dla Cypress
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"],
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"**/*.ts",
"**/*.tsx"
]
}
// cypress.env.json - zmienne środowiskowe (nie commitować do repo!)
{
"adminUsername": "admin@example.com",
"adminPassword": "TajneHaslo123!",
"apiKey": "sk_test_1234567890",
"testUserId": "user_test_123"
}
// cypress/support/e2e.js - plik commands i custom commands
// Import poleceń Cypress
import './commands';
// Globalna konfiguracja przed każdym testem
beforeEach(() => {
// Przykład: czyszczenie cookies przed każdym testem
cy.clearCookies();
cy.clearLocalStorage();
});
// Obsługa wyjątków - zapobiega failowaniu testów przy błędach JS
Cypress.on('uncaught:exception', (err, runnable) => {
// Zwracamy false aby zapobiec failowaniu testu
// Można dodać warunki dla specyficznych błędów
if (err.message.includes('ResizeObserver')) {
return false;
}
return true;
});
// cypress/support/commands.js - własne polecenia
// Dodanie custom command dla logowania
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password);
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
});
// Custom command z użyciem API (szybsze niż UI)
Cypress.Commands.add('loginViaAPI', (email, password) => {
cy.request('POST', `${Cypress.env('apiUrl')}/auth/login`, {
email,
password,
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
});
Diagram:
graph TD
A[Nowy Projekt] -->|npm install cypress --save-dev| B[Instalacja Cypress]
B -->|npx cypress open| C[Pierwsze uruchomienie]
C --> D[Automatyczne utworzenie struktury]
D --> E[cypress/]
E --> E1[e2e/ - testy e2e]
E --> E2[fixtures/ - dane testowe]
E --> E3[support/ - komendy i konfiguracja]
E --> E4[downloads/ - pobrane pliki]
D --> F[cypress.config.js]
F --> F1[baseUrl]
F --> F2[viewportWidth/Height]
F --> F3[timeouts]
F --> F4[setupNodeEvents]
D --> G[Opcjonalne pliki]
G --> G1[cypress.env.json<br/>zmienne środowiskowe]
G --> G2[cypress/tsconfig.json<br/>TypeScript config]
C --> H[Gotowy do pisania testów]
style B fill:#99ff99
style C fill:#99ccff
style E fill:#ffcc99
style F fill:#ffcc99
style H fill:#99ff99
Materiały:
- Installing Cypress - Official Guide - Oficjalny przewodnik instalacji
- Configuration - Cypress Docs - Pełna dokumentacja opcji konfiguracyjnych
- TypeScript Support in Cypress - Konfiguracja TypeScript w Cypress
Jaka jest struktura projektu Cypress?
Odpowiedź w 30 sekund:
Projekt Cypress składa się z głównego katalogu cypress/ zawierającego podkatalogi: e2e/ (testy end-to-end), fixtures/ (dane testowe JSON), support/ (custom commands i konfiguracja globalna), oraz opcjonalnie downloads/, screenshots/ i videos/. Główny plik konfiguracyjny to cypress.config.js w katalogu głównym projektu.
Odpowiedź w 2 minuty:
Struktura projektu Cypress jest dobrze zorganizowana i intuicyjna. Po pierwszym uruchomieniu npx cypress open, framework automatycznie tworzy kompletną strukturę katalogów, która jest zaprojektowana według najlepszych praktyk testowania.
Katalog cypress/e2e/ to miejsce na wszystkie testy end-to-end, gdzie każdy plik testowy powinien mieć rozszerzenie .cy.js (lub .cy.ts dla TypeScript). Katalog cypress/fixtures/ przechowuje statyczne dane testowe w formacie JSON, które można łatwo importować w testach używając cy.fixture(). Katalog cypress/support/ zawiera dwa kluczowe pliki: e2e.js (lub e2e.ts), który ładuje się przed każdym testem i służy do globalnej konfiguracji, oraz commands.js, gdzie definiujemy własne polecenia Cypress dostępne we wszystkich testach.
Dodatkowo Cypress automatycznie tworzy katalogi dla artefaktów testowych: cypress/downloads/ dla pobranych plików, cypress/screenshots/ dla zrzutów ekranu (automatycznych przy błędach lub ręcznych), oraz cypress/videos/ dla nagrań video z wykonania testów. Te katalogi są szczególnie przydatne w środowiskach CI/CD, gdzie potrzebujemy dokumentacji wizualnej failujących testów.
Plik cypress.config.js w katalogu głównym projektu jest centralnym miejscem konfiguracji, gdzie definiujemy wszystkie globalne ustawienia, ścieżki do katalogów, timeouty, setupNodeEvents dla wtyczek i wiele innych opcji. Dla projektów z wrażliwymi danymi, można utworzyć plik cypress.env.json do przechowywania zmiennych środowiskowych (który powinien być dodany do .gitignore).
Przykład kodu:
moj-projekt/
│
├── cypress/
│ ├── e2e/ # Testy end-to-end
│ │ ├── authentication/ # Organizacja testów w podfoldery
│ │ │ ├── login.cy.js
│ │ │ ├── register.cy.js
│ │ │ └── forgot-password.cy.js
│ │ ├── dashboard/
│ │ │ ├── dashboard-view.cy.js
│ │ │ └── user-settings.cy.js
│ │ └── e-commerce/
│ │ ├── product-list.cy.js
│ │ ├── shopping-cart.cy.js
│ │ └── checkout.cy.js
│ │
│ ├── fixtures/ # Dane testowe (JSON)
│ │ ├── users.json
│ │ ├── products.json
│ │ └── api-responses/
│ │ ├── user-profile.json
│ │ └── orders.json
│ │
│ ├── support/ # Konfiguracja i custom commands
│ │ ├── commands.js # Własne polecenia Cypress
│ │ ├── e2e.js # Setup przed każdym testem
│ │ └── helpers/ # Funkcje pomocnicze
│ │ ├── auth-helpers.js
│ │ └── data-generators.js
│ │
│ ├── downloads/ # Pobrane pliki podczas testów
│ ├── screenshots/ # Zrzuty ekranu (auto przy błędach)
│ └── videos/ # Nagrania video testów
│
├── cypress.config.js # Główny plik konfiguracyjny
├── cypress.env.json # Zmienne środowiskowe (nie commitować!)
├── .gitignore # Ignorowanie plików Cypress
├── package.json
└── tsconfig.json # Jeśli używasz TypeScript
// cypress/e2e/authentication/login.cy.js - przykład testu
describe('Panel logowania', () => {
beforeEach(() => {
// Załadowanie danych z fixtures
cy.fixture('users').as('usersData');
cy.visit('/login');
});
it('powinien zalogować użytkownika z fixtures', function() {
// Użycie danych z fixture
const user = this.usersData.validUser;
cy.get('[data-testid="email"]').type(user.email);
cy.get('[data-testid="password"]').type(user.password);
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
});
});
// cypress/fixtures/users.json - dane testowe
{
"validUser": {
"email": "jan.kowalski@example.com",
"password": "TajneHaslo123!",
"firstName": "Jan",
"lastName": "Kowalski"
},
"adminUser": {
"email": "admin@example.com",
"password": "AdminPass123!",
"role": "admin"
},
"invalidUser": {
"email": "niepoprawny@email.com",
"password": "zlehaslo"
}
}
// cypress/fixtures/products.json
{
"electronics": [
{
"id": "prod_001",
"name": "Laptop Dell XPS 15",
"price": 5999.99,
"category": "Laptopy"
},
{
"id": "prod_002",
"name": "iPhone 15 Pro",
"price": 4999.99,
"category": "Smartfony"
}
],
"books": [
{
"id": "book_001",
"title": "JavaScript: The Good Parts",
"author": "Douglas Crockford",
"price": 89.99
}
]
}
// cypress/support/commands.js - własne polecenia
// Custom command do logowania
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password);
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
});
});
// Custom command do dodania produktu do koszyka
Cypress.Commands.add('addToCart', (productId) => {
cy.get(`[data-product-id="${productId}"]`)
.find('[data-testid="add-to-cart"]')
.click();
cy.get('[data-testid="cart-count"]')
.should('be.visible');
});
// Custom command do wczytania fixture i aliasowania
Cypress.Commands.add('loadFixture', (fixtureName, aliasName) => {
cy.fixture(fixtureName).as(aliasName || fixtureName);
});
// cypress/support/e2e.js - globalna konfiguracja
// Import custom commands
import './commands';
// Uruchamiane przed każdym testem
beforeEach(() => {
// Czyszczenie stanu przeglądarki
cy.clearCookies();
cy.clearLocalStorage();
// Mockowanie zewnętrznych serwisów (np. analytics)
cy.intercept('POST', '**/analytics/**', { statusCode: 200 });
});
// Obsługa wyjątków JavaScript
Cypress.on('uncaught:exception', (err, runnable) => {
// Ignorowanie znanych błędów, które nie wpływają na testy
if (err.message.includes('ResizeObserver loop')) {
return false;
}
if (err.message.includes('Script error')) {
return false;
}
// Pozwól na fail dla innych błędów
return true;
});
// Konfiguracja dla retry przy failurze
Cypress.on('fail', (error, runnable) => {
// Logowanie dodatkowych informacji przy błędzie
console.error('Test failed:', runnable.title);
console.error('Error:', error.message);
throw error; // Ponowne rzucenie błędu
});
// cypress/support/helpers/auth-helpers.js - funkcje pomocnicze
/**
* Logowanie przez API (szybsze niż przez UI)
*/
export function loginViaAPI(email, password) {
return cy.request({
method: 'POST',
url: `${Cypress.env('apiUrl')}/auth/login`,
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token);
window.localStorage.setItem('userId', response.body.userId);
return response.body;
});
}
/**
* Utworzenie testowego użytkownika
*/
export function createTestUser(userData) {
return cy.request({
method: 'POST',
url: `${Cypress.env('apiUrl')}/users`,
body: userData,
headers: {
'Authorization': `Bearer ${Cypress.env('adminApiKey')}`
}
});
}
/**
* Usunięcie testowego użytkownika (cleanup)
*/
export function deleteTestUser(userId) {
return cy.request({
method: 'DELETE',
url: `${Cypress.env('apiUrl')}/users/${userId}`,
headers: {
'Authorization': `Bearer ${Cypress.env('adminApiKey')}`
}
});
}
// Użycie helpers w teście
import { loginViaAPI, createTestUser, deleteTestUser } from '../support/helpers/auth-helpers';
describe('Zarządzanie użytkownikami', () => {
let testUserId;
before(() => {
// Utworzenie użytkownika przed testami
const userData = {
email: 'test@example.com',
password: 'Test123!',
firstName: 'Test',
lastName: 'User'
};
createTestUser(userData).then((response) => {
testUserId = response.body.id;
});
});
after(() => {
// Cleanup po testach
if (testUserId) {
deleteTestUser(testUserId);
}
});
it('powinien edytować profil użytkownika', () => {
loginViaAPI('test@example.com', 'Test123!');
cy.visit('/profile');
// Test edycji profilu...
});
});
# .gitignore - pliki Cypress do ignorowania
cypress/videos/
cypress/screenshots/
cypress/downloads/
cypress.env.json
cypress/results/
Diagram:
graph TB
subgraph "Projekt"
ROOT[Katalog główny projektu]
end
subgraph "Konfiguracja"
CONFIG[cypress.config.js<br/>Główna konfiguracja]
ENV[cypress.env.json<br/>Zmienne środowiskowe]
end
subgraph "cypress/"
E2E[e2e/<br/>Testy E2E<br/>*.cy.js]
FIX[fixtures/<br/>Dane testowe<br/>*.json]
SUP[support/<br/>Commands & Setup]
DOWN[downloads/<br/>Pobrane pliki]
SCREEN[screenshots/<br/>Zrzuty ekranu]
VID[videos/<br/>Nagrania testów]
end
subgraph "support/"
CMD[commands.js<br/>Custom commands]
E2EJS[e2e.js<br/>Setup globalny]
HELP[helpers/<br/>Funkcje pomocnicze]
end
ROOT --> CONFIG
ROOT --> ENV
ROOT --> E2E
ROOT --> FIX
ROOT --> SUP
ROOT --> DOWN
ROOT --> SCREEN
ROOT --> VID
SUP --> CMD
SUP --> E2EJS
SUP --> HELP
E2E -.używa.-> FIX
E2E -.używa.-> CMD
E2EJS -.ładuje się przed.-> E2E
style CONFIG fill:#99ff99
style E2E fill:#99ccff
style FIX fill:#ffcc99
style SUP fill:#ff99cc
style CMD fill:#ffcc99
Materiały:
- Folder Structure - Cypress Docs - Oficjalna dokumentacja struktury projektu
- Best Practices - Organizing Tests - Najlepsze praktyki organizacji testów
- Fixtures - Loading Data - Dokumentacja używania fixtures
Czym jest Cypress Test Runner i jakie ma funkcje?
Odpowiedź w 30 sekund:
Cypress Test Runner to interaktywne GUI uruchamiane poleceniem npx cypress open, które pozwala na wybór i uruchomienie testów w trybie wizualnym. Oferuje funkcje takie jak Time Travel (przeglądanie stanu DOM z każdego kroku), Command Log (historia wszystkich poleceń), preview przed i po każdej akcji, możliwość debugowania testów oraz hot-reload przy zmianach w kodzie.
Odpowiedź w 2 minuty:
Cypress Test Runner to zaawansowane, interaktywne środowisko deweloperskie do uruchamiania i debugowania testów. Jest to graficzny interfejs użytkownika, który otwiera się po wykonaniu polecenia npx cypress open i stanowi jedno z najważniejszych narzędzi w ekosystemie Cypress. Test Runner działa w trybie "watch mode", automatycznie przeładowując testy przy każdej zmianie w kodzie, co znacznie przyspiesza cykl development-testowanie.
Kluczową funkcją Test Runnera jest Command Log - panel pokazujący wszystkie wykonane polecenia Cypress w chronologicznej kolejności. Każde polecenie jest klikalnym elementem, który po najechaniu myszką pokazuje snapshot DOM dokładnie z tego momentu wykonania (Time Travel). To pozwala na precyzyjne debugowanie - możesz zobaczyć dokładnie jak wyglądała strona w momencie kliknięcia przycisku, wpisania tekstu czy wykonania asercji.
Test Runner wyświetla również aplikację testową w prawej części okna w rzeczywistej przeglądarce (Chrome, Firefox, Edge, Electron), gdzie możesz obserwować wykonywanie testów w czasie rzeczywistym. Każde polecenie jest wizualnie zaznaczane na stronie, a Test Runner automatycznie robi snapshoty "before" i "after" dla każdej akcji. Panel DevTools jest w pełni dostępny, co pozwala na inspekcję elementów, analizę network requests i standardowe debugowanie JavaScript.
Dodatkowe funkcje to: wskaźniki czasu wykonania każdego polecenia, możliwość "pinowania" snapshots, selector playground do generowania selektorów CSS, oraz szczegółowe informacje o błędach z dokładnym wskazaniem miejsca w kodzie i przyczyny niepowodzenia testu.
Przykład kodu:
// cypress/e2e/test-runner-demo.cy.js - demonstracja funkcji Test Runnera
describe('Demonstracja Cypress Test Runner', () => {
beforeEach(() => {
cy.visit('https://example.com/login');
});
it('pokazuje Time Travel i Command Log', () => {
// Każde polecenie pojawia się w Command Log
cy.get('[data-testid="email"]')
.type('user@example.com'); // 1. Zobaczysz "TYPE" w Command Log
cy.get('[data-testid="password"]')
.type('haslo123'); // 2. Kolejne polecenie "TYPE"
// Najechanie na polecenie w Command Log pokaże snapshot DOM
cy.get('[data-testid="login-button"]')
.click(); // 3. Polecenie "CLICK"
// Asercje również są widoczne w Command Log
cy.url()
.should('include', '/dashboard'); // 4. "SHOULD" assertion
cy.get('[data-testid="welcome-message"]')
.should('be.visible') // 5. Kolejna asercja
.and('contain', 'Witaj'); // 6. Łańcuchowa asercja
});
it('pokazuje pinowanie snapshots (użyj .debug())', () => {
cy.get('[data-testid="email"]').type('user@example.com');
// .debug() zatrzyma test i otworzy DevTools
// Możesz teraz inspekcować stan w konsoli
cy.get('[data-testid="password"]').debug().type('haslo123');
// .pause() zatrzyma wykonanie - możesz krokować przez test
cy.pause();
cy.get('[data-testid="login-button"]').click();
});
it('pokazuje różne typy poleceń w Command Log', () => {
// REQUEST - żądania HTTP
cy.request('GET', 'https://api.example.com/status')
.its('status')
.should('eq', 200);
// INTERCEPT - przechwytywanie żądań
cy.intercept('POST', '/api/login').as('loginRequest');
// Działania na DOM
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('haslo123');
cy.get('[data-testid="login-button"]').click();
// WAIT - czekanie na przechwycone żądanie
cy.wait('@loginRequest').then((interception) => {
// W Command Log zobaczysz szczegóły żądania i odpowiedzi
expect(interception.response.statusCode).to.equal(200);
});
// WINDOW - dostęp do obiektu window
cy.window().then((win) => {
expect(win.localStorage.getItem('token')).to.exist;
});
// SCREENSHOT - zrzut ekranu (pojawi się w Command Log)
cy.screenshot('po-zalogowaniu');
});
it('pokazuje różne stany elementów w Test Runnerze', () => {
// Test Runner podświetli element na czerwono jeśli nie istnieje
cy.get('[data-testid="email"]', { timeout: 1000 })
.should('be.visible');
// Podświetli na zielono gdy znajdzie
cy.get('[data-testid="email"]')
.should('have.attr', 'type', 'email');
// Pokaże retry mechanism w Command Log
cy.get('[data-testid="dynamic-content"]', { timeout: 10000 })
.should('contain', 'Załadowano');
});
});
// Korzystanie z Selector Playground w Test Runnerze
describe('Użycie Selector Playground', () => {
it('pomaga znaleźć optymalne selektory', () => {
cy.visit('https://example.com/products');
// W Test Runnerze kliknij ikonę "Selector Playground" (celownik)
// Następnie kliknij element na stronie
// Test Runner wygeneruje optymalny selektor
// Przykład: zamiast długiego selektora:
// cy.get('div.container > div.row > div.col-md-6 > button.btn-primary')
// Selector Playground może zasugerować:
cy.get('[data-cy="add-to-cart"]'); // Jeśli element ma data-cy
// lub
cy.contains('button', 'Dodaj do koszyka'); // Prostszy selektor
});
});
// Demonstracja debugowania w Test Runnerze
describe('Debugowanie testów', () => {
it('używa różnych metod debugowania', () => {
cy.visit('/dashboard');
// 1. Użycie cy.log() - wyświetla wiadomość w Command Log
cy.log('Rozpoczynam test dashboardu');
// 2. Użycie cy.debug() - otwiera DevTools i pokazuje obiekt
cy.get('[data-testid="user-profile"]')
.debug(); // DevTools otworzy się z tym elementem
// 3. Użycie cy.pause() - zatrzymuje test
// Możesz kliknąć "Resume" w Test Runnerze aby kontynuować
cy.pause();
cy.get('[data-testid="settings"]').click();
// 4. Użycie .then() z debuggerem
cy.get('[data-testid="form"]').then(($form) => {
// Ustaw breakpoint w DevTools tutaj
debugger; // Test zatrzyma się na tym breakpoincie
console.log('Form element:', $form);
});
// 5. Command Log pokazuje czas wykonania każdego polecenia
// Długie czasy mogą wskazywać na problemy z performance
cy.wait(2000); // To polecenie pokaże "WAIT 2000ms"
});
it('pokazuje szczegóły błędów w Test Runnerze', () => {
cy.visit('/login');
// Celowy błąd - element nie istnieje
// Test Runner pokaże:
// - Gdzie w kodzie wystąpił błąd
// - Dokładny komunikat błędu
// - Snapshot DOM w momencie błędu
// - Stack trace
cy.get('[data-testid="nieistniejacy-element"]', { timeout: 5000 })
.should('be.visible');
// Test Runner automatycznie zrobi screenshot przy błędzie
});
});
// Konfiguracja Test Runnera w cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// Event listener dla logowania w terminalu
on('task', {
log(message) {
console.log(message);
return null;
}
});
return config;
},
// Konfiguracja wpływająca na Test Runner
watchForFileChanges: true, // Hot reload przy zmianach
experimentalStudio: true, // Włącz Cypress Studio (nagrywanie testów)
numTestsKeptInMemory: 50, // Ile testów trzymać w pamięci
// Viewport widoczny w Test Runnerze
viewportWidth: 1280,
viewportHeight: 720,
},
});
// Użycie Cypress Studio w Test Runnerze
describe('Nagrywanie testów z Cypress Studio', () => {
it('nagrywa interakcje użytkownika', () => {
cy.visit('/login');
// W Test Runnerze kliknij "Add Commands to Test"
// Następnie wykonuj akcje na stronie:
// - Klikaj elementy
// - Wpisuj tekst
// - Wykonuj asercje
// Cypress Studio automatycznie wygeneruje kod testu
// Wygenerowany kod może wyglądać tak:
/*
cy.get('[data-testid="email"]').clear();
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').clear();
cy.get('[data-testid="password"]').type('haslo123');
cy.get('[data-testid="login-button"]').click();
cy.get('[data-testid="welcome"]').should('have.text', 'Witaj, User!');
*/
});
});
Diagram:
graph TB
subgraph "Cypress Test Runner - Interfejs"
TR[Test Runner UI]
subgraph "Lewa strona - Kontrola"
SEL[Selektor testów<br/>Wybór przeglądarki]
SPEC[Lista specyfikacji<br/>testowych]
STATS[Statystyki:<br/>✓ Passed<br/>✗ Failed<br/>⊙ Pending]
end
subgraph "Środek - Command Log"
CL[Command Log]
CMD1[VISIT /login]
CMD2[GET input]
CMD3[TYPE email]
CMD4[CLICK button]
CMD5[ASSERT url]
CL --> CMD1
CL --> CMD2
CL --> CMD3
CL --> CMD4
CL --> CMD5
end
subgraph "Prawa strona - Aplikacja"
APP[Testowana aplikacja<br/>w przeglądarce]
SNAP[Snapshots DOM<br/>Time Travel]
DEVTOOLS[DevTools<br/>Console, Network]
end
end
subgraph "Funkcje Test Runnera"
TT[Time Travel<br/>Hover na Command Log]
SP[Selector Playground<br/>Generowanie selektorów]
DEBUG[Debug Mode<br/>cy.debug, cy.pause]
HR[Hot Reload<br/>Auto-refresh testów]
SS[Screenshots<br/>Auto przy błędach]
end
TR --> SEL
TR --> SPEC
TR --> STATS
TR --> CL
TR --> APP
CMD1 -.snapshot.-> SNAP
CMD2 -.snapshot.-> SNAP
CMD3 -.snapshot.-> SNAP
APP --> DEVTOOLS
CL -.-> TT
APP -.-> SP
CL -.-> DEBUG
SPEC -.-> HR
APP -.-> SS
style TR fill:#99ff99
style CL fill:#99ccff
style APP fill:#ffcc99
style TT fill:#ff99cc
Materiały:
- Test Runner - Cypress Docs - Oficjalna dokumentacja Test Runnera
- Debugging Cypress Tests - Przewodnik po debugowaniu
- Selector Playground - Dokumentacja narzędzia do selektorów
Jak uruchomić testy Cypress w trybie headless?
Odpowiedź w 30 sekund:
Testy w trybie headless uruchamiamy poleceniem npx cypress run, które wykonuje testy w tle bez GUI, zapisując wyniki, screenshoty i video. Można określić konkretną przeglądarkę (--browser chrome), pojedynczy plik testowy (--spec), lub konfigurację środowiska. Tryb headless jest idealny dla CI/CD pipelines i automatyzacji.
Odpowiedź w 2 minuty:
Tryb headless w Cypress pozwala na uruchamianie testów bez graficznego interfejsu użytkownika, co jest kluczowe dla procesów CI/CD i automatyzacji. Podstawowe polecenie npx cypress run uruchamia wszystkie testy w domyślnej przeglądarce (Electron) bez wyświetlania okna przeglądarki. W tym trybie Cypress automatycznie wykonuje wszystkie testy, generuje raporty, zapisuje screenshoty dla failujących testów oraz nagrywa video całego przebiegu testów.
Tryb headless oferuje szereg opcji konfiguracyjnych. Można uruchomić testy w konkretnej przeglądarce używając flagi --browser (chrome, firefox, edge), określić pojedynczy plik lub wzorzec plików przez --spec, wybrać środowisko konfiguracyjne, ustawić zmienne środowiskowe czy określić ilość równoległych instancji testowych. Każde uruchomienie generuje szczegółowe logi w konsoli pokazujące progress testów, liczbę passed/failed przypadków oraz czas wykonania.
Wyniki testów headless są zapisywane w określonych katalogach: video w cypress/videos/, screenshoty w cypress/screenshots/, a dodatkowo można skonfigurować generowanie raportów w formatach JUnit, JSON czy Mochawesome dla integracji z systemami CI/CD. Cypress oferuje również możliwość uruchomienia testów na Cypress Cloud (dawniej Dashboard), który agreguje wyniki z wielu uruchomień i oferuje zaawansowaną analitykę.
W środowiskach CI/CD tryb headless umożliwia też równoległe uruchamianie testów na wielu maszynach (test parallelization), co znacząco skraca czas całego test suite. Konfiguracja może być dostosowana przez zmienne środowiskowe, parametry CLI lub przez plik cypress.config.js, co daje pełną elastyczność w różnych środowiskach deploymentu.
Przykład kodu:
# Podstawowe uruchomienie headless - wszystkie testy
npx cypress run
# Uruchomienie w konkretnej przeglądarce
npx cypress run --browser chrome
npx cypress run --browser firefox
npx cypress run --browser edge
npx cypress run --browser electron # Domyślna przeglądarka Cypress
# Uruchomienie konkretnego pliku testowego
npx cypress run --spec "cypress/e2e/login.cy.js"
# Uruchomienie wielu plików pasujących do wzorca
npx cypress run --spec "cypress/e2e/authentication/*.cy.js"
npx cypress run --spec "cypress/e2e/**/*login*.cy.js"
# Uruchomienie z konkretną konfiguracją
npx cypress run --config baseUrl=https://staging.example.com
npx cypress run --config viewportWidth=1920,viewportHeight=1080
# Uruchomienie ze zmiennymi środowiskowymi
npx cypress run --env environment=staging,apiKey=abc123
npx cypress run --env-file cypress.staging.json
# Uruchomienie bez nagrywania video (szybsze)
npx cypress run --config video=false
# Uruchomienie bez zapisywania screenshotów
npx cypress run --config screenshotOnRunFailure=false
# Uruchomienie z konkretnym config file
npx cypress run --config-file cypress.production.config.js
# Wyświetlenie nagłówków HTTP w logach
npx cypress run --headed # Pokazuje przeglądarkę mimo trybu "run"
# Uruchomienie w trybie cichym (mniej logów)
npx cypress run --quiet
# Zapisanie wyników do pliku
npx cypress run > test-results.log 2>&1
// package.json - skrypty npm dla różnych środowisk
{
"name": "moj-projekt-cypress",
"scripts": {
// Podstawowe skrypty
"test": "cypress run",
"test:headed": "cypress run --headed",
"test:chrome": "cypress run --browser chrome",
"test:firefox": "cypress run --browser firefox",
// Skrypty dla różnych środowisk
"test:dev": "cypress run --env environment=development",
"test:staging": "cypress run --config baseUrl=https://staging.example.com --env environment=staging",
"test:prod": "cypress run --config baseUrl=https://example.com --env environment=production",
// Skrypty dla konkretnych testów
"test:login": "cypress run --spec 'cypress/e2e/authentication/login.cy.js'",
"test:smoke": "cypress run --spec 'cypress/e2e/smoke/**/*.cy.js'",
"test:regression": "cypress run --spec 'cypress/e2e/**/*.cy.js'",
// Skrypty optymalizowane dla CI/CD
"test:ci": "cypress run --browser chrome --headless --config video=false",
"test:ci:parallel": "cypress run --record --parallel --group 'UI-Chrome'",
// Skrypt z uruchomieniem serwera (start-server-and-test)
"test:e2e": "start-server-and-test start http://localhost:3000 test",
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 'cypress run'"
},
"devDependencies": {
"cypress": "^13.6.0",
"start-server-and-test": "^2.0.3"
}
}
// cypress.config.js - konfiguracja dla headless mode
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// Konfiguracja video i screenshotów
video: true, // Nagrywanie video testów
videoCompression: 32, // Kompresja video (0-51, niższe = lepsza jakość)
videosFolder: 'cypress/videos', // Katalog dla video
screenshotOnRunFailure: true, // Screenshot przy błędzie
screenshotsFolder: 'cypress/screenshots',
// Konfiguracja dla CI/CD
trashAssetsBeforeRuns: true, // Czyszczenie video/screenshots przed uruchomieniem
// Retry testów w headless mode
retries: {
runMode: 2, // 2 retry w trybie headless
openMode: 0, // 0 retry w trybie interaktywnym
},
// Timeout'y dla headless
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
requestTimeout: 10000,
setupNodeEvents(on, config) {
// Task do logowania w konsoli (widoczne w headless output)
on('task', {
log(message) {
console.log(`\n📝 ${message}\n`);
return null;
},
// Task do zapisu wyników do pliku
saveResults(results) {
const fs = require('fs');
fs.writeFileSync(
'test-results.json',
JSON.stringify(results, null, 2)
);
return null;
}
});
// Event listener dla logowania szczegółów testów
on('after:spec', (spec, results) => {
console.log('\n=================================');
console.log(`Plik: ${spec.name}`);
console.log(`Passed: ${results.stats.passes}`);
console.log(`Failed: ${results.stats.failures}`);
console.log(`Duration: ${results.stats.duration}ms`);
console.log('=================================\n');
});
return config;
},
},
});
# .github/workflows/cypress.yml - GitHub Actions CI/CD
name: Cypress Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Cypress tests
run: npm run test:ci
env:
CYPRESS_BROWSER: ${{ matrix.browser }}
- name: Upload screenshots on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
- name: Generate test report
if: always()
run: |
npm run test:report
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.browser }}
path: cypress/results
# .gitlab-ci.yml - GitLab CI/CD
stages:
- test
cypress-e2e:
stage: test
image: cypress/browsers:node18.12.0-chrome106-ff106
script:
# Instalacja zależności
- npm ci
# Uruchomienie testów headless
- npx cypress run --browser chrome --headless
artifacts:
when: always
paths:
- cypress/screenshots/
- cypress/videos/
- cypress/results/
expire_in: 1 week
only:
- main
- develop
- merge_requests
# Równoległe uruchamianie testów
cypress-parallel:
stage: test
image: cypress/browsers:node18.12.0-chrome106-ff106
parallel: 3 # 3 równoległe instancje
script:
- npm ci
- npx cypress run --record --parallel --group "GitLab CI"
only:
- main
// Generowanie raportów z testów headless
// cypress.config.js z mochawesome reporter
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// Instalacja: npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
// Konfiguracja reportera
on('after:run', (results) => {
// Możesz tutaj przetwarzać wyniki
console.log('Total tests:', results.totalTests);
console.log('Passed:', results.totalPassed);
console.log('Failed:', results.totalFailed);
console.log('Duration:', results.totalDuration);
});
return config;
},
// Reporter dla headless mode
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/results',
overwrite: false,
html: true,
json: true,
charts: true,
reportPageTitle: 'Cypress Test Report',
embeddedScreenshots: true,
inlineAssets: true,
},
},
});
# Skrypt do uruchomienia testów i generowania raportu
#!/bin/bash
# run-tests.sh
echo "🚀 Rozpoczynam testy Cypress..."
# Uruchomienie testów
npx cypress run --browser chrome
# Sprawdzenie exit code
if [ $? -eq 0 ]; then
echo "✅ Wszystkie testy przeszły pomyślnie!"
exit 0
else
echo "❌ Niektóre testy nie powiodły się."
echo "📸 Sprawdź screenshoty w: cypress/screenshots/"
echo "🎥 Sprawdź video w: cypress/videos/"
exit 1
fi
// Użycie cy.task() do logowania w headless mode
describe('Testy z logowaniem w headless', () => {
it('loguje informacje w konsoli CI/CD', () => {
cy.visit('/login');
// Logowanie widoczne w output konsoli
cy.task('log', 'Rozpoczynam test logowania');
cy.get('[data-testid="email"]').type('user@example.com');
cy.task('log', 'Wpisano email');
cy.get('[data-testid="password"]').type('haslo123');
cy.task('log', 'Wpisano hasło');
cy.get('[data-testid="login-button"]').click();
cy.task('log', 'Kliknięto przycisk logowania');
cy.url().should('include', '/dashboard');
cy.task('log', '✅ Test zakończony sukcesem');
});
});
Diagram:
graph TB
START[npx cypress run] --> INIT[Inicjalizacja Cypress]
INIT --> LOAD[Ładowanie konfiguracji<br/>cypress.config.js]
LOAD --> ENV[Ustawienie zmiennych<br/>środowiskowych]
ENV --> BROWSER{Wybór przeglądarki}
BROWSER -->|--browser chrome| CHROME[Chrome Headless]
BROWSER -->|--browser firefox| FIREFOX[Firefox Headless]
BROWSER -->|domyślnie| ELECTRON[Electron]
CHROME --> EXEC[Wykonanie testów]
FIREFOX --> EXEC
ELECTRON --> EXEC
EXEC --> TEST1[Test 1]
EXEC --> TEST2[Test 2]
EXEC --> TEST3[Test N...]
TEST1 --> CHECK1{Passed?}
TEST2 --> CHECK2{Passed?}
TEST3 --> CHECK3{Passed?}
CHECK1 -->|Tak| PASS1[✓ Passed]
CHECK1 -->|Nie| FAIL1[✗ Failed]
CHECK2 -->|Tak| PASS2[✓ Passed]
CHECK2 -->|Nie| FAIL2[✗ Failed]
CHECK3 -->|Tak| PASS3[✓ Passed]
CHECK3 -->|Nie| FAIL3[✗ Failed]
FAIL1 --> SCREEN1[Screenshot]
FAIL2 --> SCREEN2[Screenshot]
FAIL3 --> SCREEN3[Screenshot]
EXEC --> VIDEO[Nagranie video<br/>cypress/videos/]
SCREEN1 --> ARTIFACTS[Artefakty testowe]
SCREEN2 --> ARTIFACTS
SCREEN3 --> ARTIFACTS
VIDEO --> ARTIFACTS
PASS1 --> RESULTS[Agregacja wyników]
PASS2 --> RESULTS
PASS3 --> RESULTS
FAIL1 --> RESULTS
FAIL2 --> RESULTS
FAIL3 --> RESULTS
RESULTS --> REPORT[Generowanie raportu]
ARTIFACTS --> REPORT
REPORT --> OUTPUT[Output w konsoli:<br/>X passed, Y failed<br/>Total duration]
OUTPUT --> EXIT{Exit code}
EXIT -->|Wszystkie passed| EXIT0[Exit 0]
EXIT -->|Jakieś failed| EXIT1[Exit 1]
style START fill:#99ff99
style EXEC fill:#99ccff
style PASS1 fill:#99ff99
style PASS2 fill:#99ff99
style PASS3 fill:#99ff99
style FAIL1 fill:#ff9999
style FAIL2 fill:#ff9999
style FAIL3 fill:#ff9999
style REPORT fill:#ffcc99
style EXIT0 fill:#99ff99
style EXIT1 fill:#ff9999
Materiały:
- Command Line - Cypress Docs - Pełna dokumentacja opcji CLI
- Continuous Integration - Cypress Docs - Przewodnik po CI/CD
- Parallelization - Równoległe uruchamianie testów
Co to jest cypress.config.js i jakie opcje konfiguracyjne zawiera?
Odpowiedź w 30 sekund:
cypress.config.js to główny plik konfiguracyjny Cypress, który definiuje globalne ustawienia dla wszystkich testów. Zawiera opcje takie jak baseUrl (bazowy URL aplikacji), timeouty (defaultCommandTimeout, pageLoadTimeout), wymiary viewport, ścieżki do katalogów z testami i artefaktami, konfigurację przeglądarek, zmienne środowiskowe oraz funkcję setupNodeEvents do rejestracji wtyczek i event listeners.
Odpowiedź w 2 minuty:
Plik cypress.config.js (lub cypress.config.ts dla TypeScript) to centralne miejsce konfiguracji całego projektu Cypress, które zastąpiło starszy format cypress.json od wersji 10.0. Jest to plik JavaScript/TypeScript używający funkcji defineConfig(), co pozwala na dynamiczną konfigurację i dodawanie logiki programistycznej do setupu testów.
Struktura pliku zawiera głównie dwie sekcje: e2e dla testów end-to-end oraz opcjonalnie component dla testów komponentów. W sekcji e2e definiujemy najważniejsze opcje jak baseUrl (bazowy adres aplikacji, używany przez cy.visit()), wymiary viewport (viewportWidth, viewportHeight), różne timeout'y dla poleceń, ładowania stron i żądań sieciowych, oraz wzorce plików testowych (specPattern).
Kluczowa jest funkcja setupNodeEvents(on, config), która uruchamia się w kontekście Node.js i pozwala na rejestrację event listeners, dodawanie custom tasks (dla operacji wykraczających poza przeglądarkę), modyfikację konfiguracji w czasie runtime oraz integrację z wtyczkami Cypress. Można tu np. dynamicznie zmieniać baseUrl w zależności od środowiska, konfigurować preprocessory dla TypeScript, lub dodawać obsługę plików (czytanie/zapis).
Plik pozwala również na konfigurację zaawansowanych opcji jak retry logic (ile razy powtórzyć failujące testy), zachowanie przeglądarki (czy trzymać ją otwartą między testami), izolację testów (testIsolation), ustawienia video i screenshotów, oraz zmienne środowiskowe dostępne w testach przez Cypress.env(). Można też definiować różne konfiguracje dla różnych przeglądarek lub środowisk, co czyni setup bardzo elastycznym.
Przykład kodu:
// cypress.config.js - pełna konfiguracja z komentarzami
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// ============================================
// KONFIGURACJA TESTÓW E2E
// ============================================
e2e: {
// --- URL i Routing ---
baseUrl: 'http://localhost:3000', // Bazowy URL używany w cy.visit()
// --- Wymiary okna przeglądarki ---
viewportWidth: 1280, // Szerokość viewport
viewportHeight: 720, // Wysokość viewport
// --- Timeout'y (w milisekundach) ---
defaultCommandTimeout: 10000, // Timeout dla poleceń Cypress (cy.get, cy.click, etc.)
pageLoadTimeout: 60000, // Timeout dla cy.visit() i ładowania strony
requestTimeout: 10000, // Timeout dla cy.request(), cy.wait()
responseTimeout: 30000, // Timeout dla odpowiedzi serwera
execTimeout: 60000, // Timeout dla cy.exec()
taskTimeout: 60000, // Timeout dla cy.task()
// --- Ścieżki do plików i katalogów ---
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', // Wzorzec plików testowych
supportFile: 'cypress/support/e2e.{js,jsx,ts,tsx}', // Plik ładowany przed testami
fixturesFolder: 'cypress/fixtures', // Katalog z danymi testowymi
downloadsFolder: 'cypress/downloads', // Katalog dla pobranych plików
screenshotsFolder: 'cypress/screenshots', // Katalog dla screenshotów
videosFolder: 'cypress/videos', // Katalog dla video
// --- Video i Screenshots ---
video: true, // Czy nagrywać video testów
videoCompression: 32, // Kompresja video (0-51, niższe = lepsza jakość)
videoUploadOnPasses: false, // Czy uploadować video dla passed testów
screenshotOnRunFailure: true, // Czy robić screenshot przy błędzie
trashAssetsBeforeRuns: true, // Czyszczenie video/screenshots przed uruchomieniem
// --- Zachowanie testów ---
testIsolation: true, // Czy resetować stan między testami
watchForFileChanges: true, // Hot reload w cypress open
chromeWebSecurity: false, // Wyłączenie same-origin policy (ostrożnie!)
// --- Retry Logic ---
retries: {
runMode: 2, // Ile razy retry w cypress run
openMode: 0, // Ile razy retry w cypress open
},
// --- Zmienne środowiskowe ---
env: {
apiUrl: 'http://localhost:4000/api',
environment: 'development',
adminUsername: 'admin@example.com',
// Wrażliwe dane lepiej w cypress.env.json
},
// --- Konfiguracja przeglądarek ---
experimentalStudio: true, // Włączenie Cypress Studio (nagrywanie testów)
experimentalMemoryManagement: true, // Optymalizacja pamięci
numTestsKeptInMemory: 50, // Ile testów trzymać w pamięci
// --- Setup Node Events (wtyczki, tasks, listeners) ---
setupNodeEvents(on, config) {
// ====================================
// CUSTOM TASKS - operacje Node.js
// ====================================
on('task', {
// Logowanie w konsoli Node.js
log(message) {
console.log(`📝 ${message}`);
return null; // Task musi coś zwrócić
},
// Czytanie pliku
readFile(filename) {
const fs = require('fs');
return fs.readFileSync(filename, 'utf8');
},
// Zapis do pliku
writeFile({ filename, content }) {
const fs = require('fs');
fs.writeFileSync(filename, content);
return null;
},
// Operacje na bazie danych
'db:seed'() {
// Przykład: zasilenie bazy testowymi danymi
console.log('Seeding database...');
// tutaj kod zasilania bazy
return null;
},
'db:reset'() {
// Reset bazy danych
console.log('Resetting database...');
return null;
},
// Zwracanie aktualnego czasu
now() {
return new Date().toISOString();
},
});
// ====================================
// EVENT LISTENERS
// ====================================
// Przed uruchomieniem testów
on('before:run', (details) => {
console.log('\n🚀 Rozpoczynam testy Cypress');
console.log(`Specyfikacje: ${details.specs.length}`);
});
// Po zakończeniu testów
on('after:run', (results) => {
console.log('\n✅ Zakończono testy');
console.log(`Passed: ${results.totalPassed}`);
console.log(`Failed: ${results.totalFailed}`);
console.log(`Duration: ${results.totalDuration}ms`);
});
// Po każdym pliku spec
on('after:spec', (spec, results) => {
console.log(`\n📄 ${spec.name}`);
console.log(` Passed: ${results.stats.passes}`);
console.log(` Failed: ${results.stats.failures}`);
});
// Przed każdym testem
on('before:browser:launch', (browser, launchOptions) => {
console.log(`🌐 Uruchamiam przeglądarkę: ${browser.name}`);
// Konfiguracja specyficzna dla przeglądarki
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage');
launchOptions.args.push('--no-sandbox');
}
return launchOptions;
});
// ====================================
// DYNAMICZNA KONFIGURACJA
// ====================================
// Zmiana konfiguracji na podstawie zmiennych środowiskowych
const environment = config.env.environment || 'development';
if (environment === 'staging') {
config.baseUrl = 'https://staging.example.com';
config.env.apiUrl = 'https://api-staging.example.com';
} else if (environment === 'production') {
config.baseUrl = 'https://example.com';
config.env.apiUrl = 'https://api.example.com';
config.video = false; // Wyłączenie video na produkcji
}
// Konfiguracja dla CI/CD
if (process.env.CI) {
config.video = true;
config.screenshotOnRunFailure = true;
config.retries.runMode = 3; // Więcej retry w CI
}
// ====================================
// INTEGRACJA Z WTYCZKAMI
// ====================================
// Przykład: cypress-mochawesome-reporter
// const reporter = require('cypress-mochawesome-reporter/plugin');
// reporter(on);
// Przykład: @cypress/code-coverage
// require('@cypress/code-coverage/task')(on, config);
// Zwróć zmodyfikowaną konfigurację
return config;
},
},
// ============================================
// KONFIGURACJA TESTÓW KOMPONENTÓW (opcjonalnie)
// ============================================
component: {
devServer: {
framework: 'react', // react, vue, angular
bundler: 'vite', // vite, webpack
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/component.{js,jsx,ts,tsx}',
indexHtmlFile: 'cypress/support/component-index.html',
// Podobne opcje jak w e2e
viewportWidth: 800,
viewportHeight: 600,
},
// ============================================
// OPCJE GLOBALNE (dla e2e i component)
// ============================================
projectId: 'abc123', // ID projektu w Cypress Cloud
// Konfiguracja Cypress Cloud (dawniej Dashboard)
cloudEnabled: false,
// Eksperimentalne feature'y
experimentalWebKitSupport: false, // Wsparcie dla WebKit/Safari
});
// Przykład: Różne config files dla różnych środowisk
// cypress.staging.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'https://staging.example.com',
env: {
apiUrl: 'https://api-staging.example.com',
environment: 'staging',
},
video: true,
retries: {
runMode: 3,
openMode: 0,
},
setupNodeEvents(on, config) {
// Setup specyficzny dla staging
return config;
},
},
});
// Uruchomienie: npx cypress run --config-file cypress.staging.config.js
// Przykład: TypeScript config
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
// TypeScript zapewnia type safety
on('task', {
log(message: string): null {
console.log(message);
return null;
},
});
return config;
},
},
});
// Użycie konfiguracji w testach
describe('Dostęp do konfiguracji', () => {
it('odczytuje zmienne z config', () => {
// Dostęp do baseUrl
cy.log(`Base URL: ${Cypress.config('baseUrl')}`);
// Dostęp do zmiennych środowiskowych
const apiUrl = Cypress.env('apiUrl');
const environment = Cypress.env('environment');
cy.log(`API URL: ${apiUrl}`);
cy.log(`Environment: ${environment}`);
// Dostęp do innych opcji config
const timeout = Cypress.config('defaultCommandTimeout');
cy.log(`Default timeout: ${timeout}ms`);
});
it('używa custom task z config', () => {
// Wywołanie custom task zdefiniowanego w setupNodeEvents
cy.task('log', 'To jest wiadomość z testu');
cy.task('now').then((timestamp) => {
cy.log(`Aktualny czas: ${timestamp}`);
});
});
it('używa różnych konfiguracji dla różnych środowisk', () => {
const env = Cypress.env('environment');
if (env === 'production') {
// Testy specyficzne dla produkcji
cy.visit('/');
cy.get('[data-testid="prod-banner"]').should('not.exist');
} else {
// Testy dla dev/staging
cy.visit('/');
cy.get('[data-testid="dev-banner"]').should('be.visible');
}
});
});
# Nadpisywanie konfiguracji przez CLI
# Nadpisanie pojedynczej opcji
npx cypress run --config baseUrl=https://example.com
# Nadpisanie wielu opcji
npx cypress run --config baseUrl=https://example.com,viewportWidth=1920
# Nadpisanie zmiennych środowiskowych
npx cypress run --env environment=staging,apiUrl=https://api.example.com
# Użycie innego pliku config
npx cypress run --config-file cypress.production.config.js
# Kombinacja
npx cypress run \
--config-file cypress.staging.config.js \
--config video=false \
--env apiKey=abc123
// cypress.env.json - wrażliwe zmienne środowiskowe (NIE COMMITOWAĆ!)
{
"adminUsername": "admin@example.com",
"adminPassword": "TajneHaslo123!",
"apiKey": "sk_live_1234567890abcdef",
"databaseUrl": "postgresql://user:pass@localhost:5432/testdb",
"auth0ClientId": "abc123def456",
"auth0ClientSecret": "secret789"
}
Diagram:
graph TB
CONFIG[cypress.config.js]
subgraph "Główne sekcje"
E2E[e2e: {}<br/>Konfiguracja testów E2E]
COMP[component: {}<br/>Konfiguracja testów komponentów]
GLOBAL[Opcje globalne<br/>projectId, cloud, etc.]
end
subgraph "e2e - Podstawowe opcje"
URL[baseUrl<br/>Bazowy URL aplikacji]
VIEW[viewportWidth/Height<br/>Wymiary okna]
TIME[Timeout'y<br/>command, page, request]
PATHS[Ścieżki<br/>specPattern, fixtures, etc.]
end
subgraph "e2e - Zaawansowane"
VIDEO[Video & Screenshots<br/>video, videoCompression]
RETRY[Retry Logic<br/>runMode, openMode]
ENV[env: {}<br/>Zmienne środowiskowe]
SETUP[setupNodeEvents()<br/>Tasks, Listeners, Wtyczki]
end
subgraph "setupNodeEvents"
TASKS[on('task', {})<br/>Custom tasks Node.js]
EVENTS[on('before:run')<br/>Event listeners]
PLUGINS[Integracja wtyczek<br/>coverage, reporters]
DYNAMIC[Dynamiczna config<br/>env-based setup]
end
CONFIG --> E2E
CONFIG --> COMP
CONFIG --> GLOBAL
E2E --> URL
E2E --> VIEW
E2E --> TIME
E2E --> PATHS
E2E --> VIDEO
E2E --> RETRY
E2E --> ENV
E2E --> SETUP
SETUP --> TASKS
SETUP --> EVENTS
SETUP --> PLUGINS
SETUP --> DYNAMIC
ENV -.może być w.-> ENVFILE[cypress.env.json]
CONFIG -.może być.-> CONFIGTS[cypress.config.ts<br/>TypeScript]
CONFIG -.różne wersje.-> CONFIGENV[cypress.staging.config.js<br/>środowiskowe config]
style CONFIG fill:#99ff99
style E2E fill:#99ccff
style SETUP fill:#ffcc99
style ENV fill:#ff99cc
Materiały:
- Configuration - Cypress Docs - Pełna dokumentacja wszystkich opcji konfiguracyjnych
- Environment Variables - Przewodnik po zmiennych środowiskowych
- Plugins - setupNodeEvents - Dokumentacja pisania wtyczek i używania setupNodeEvents
Selektory i Interakcje
Jak Cypress znajduje elementy na stronie (selektory)?
Odpowiedź w 30 sekund:
Cypress używa komend takich jak cy.get(), cy.contains() i cy.find() do lokalizowania elementów. Obsługuje selektory CSS, atrybuty data, teksty oraz oferuje specjalne metody jak .parent(), .children(), czy .siblings(). Najpopularniejsze podejście to używanie atrybutów data-cy lub data-test dla stabilności testów.
Odpowiedź w 2 minuty:
Cypress oferuje bogaty zestaw metod do znajdowania elementów na stronie. Podstawową komendą jest cy.get(), która przyjmuje selektory CSS, identyfikatory, klasy czy atrybuty. Komenda cy.contains() pozwala znaleźć element po zawartym w nim tekście, co jest szczególnie przydatne przy testowaniu interfejsów użytkownika.
Do nawigacji po drzewie DOM Cypress udostępnia metody traversal takie jak .find() (szukanie potomków), .parent() (rodzic elementu), .children() (bezpośrednie dzieci), .siblings() (rodzeństwo), .first(), .last(), .eq() (wybór po indeksie) oraz .filter() i .not() do filtrowania wyników.
Zaleca się używanie dedykowanych atrybutów testowych jak data-cy, data-test czy data-testid, ponieważ są one niezależne od stylu i struktury aplikacji. Unikanie selektorów bazujących na klasach CSS czy strukturze DOM czyni testy bardziej odpornymi na zmiany w kodzie produkcyjnym.
Cypress automatycznie powtarza zapytania (retry) do momentu znalezienia elementu lub przekroczenia timeoutu, co eliminuje potrzebę dodawania explicite waitów w większości przypadków. Wszystkie komendy są automatycznie kolejkowane i wykonywane asynchronicznie, co upraszcza pisanie testów.
Przykład kodu:
describe('Znajdowanie elementów w Cypress', () => {
beforeEach(() => {
cy.visit('https://example.com/login');
});
it('używa różnych selektorów do znajdowania elementów', () => {
// Selektor CSS po ID
cy.get('#username').type('jan.kowalski');
// Selektor po klasie
cy.get('.btn-primary').click();
// Selektor po atrybucie data-cy (zalecane)
cy.get('[data-cy="password-input"]').type('haslo123');
// Selektor po typie i atrybucie
cy.get('input[name="email"]').type('jan@example.com');
// Znajdowanie po tekście
cy.contains('Zaloguj się').click();
cy.contains('button', 'Wyślij').click(); // Precyzyjniejsze
// Kombinacja get i contains
cy.get('.navbar').contains('Profil').click();
});
it('używa metod traversal do nawigacji DOM', () => {
// Znajdowanie potomków
cy.get('.user-list').find('li').should('have.length', 5);
// Wybór pierwszego i ostatniego elementu
cy.get('.items').children().first().should('have.text', 'Pierwszy');
cy.get('.items').children().last().should('have.text', 'Ostatni');
// Wybór elementu po indeksie (0-based)
cy.get('ul li').eq(2).should('contain', 'Trzeci element');
// Nawigacja do rodzica
cy.get('[data-cy="child-element"]').parent().should('have.class', 'parent-container');
// Znajdowanie rodzeństwa
cy.get('.active-item').siblings().should('have.length', 4);
// Filtrowanie elementów
cy.get('li').filter('.completed').should('have.length', 3);
cy.get('li').not('.disabled').click({ multiple: true });
});
it('używa aliasów dla często używanych elementów', () => {
// Utworzenie aliasu
cy.get('[data-cy="search-input"]').as('searchBox');
cy.get('.results-list').as('results');
// Użycie aliasu
cy.get('@searchBox').type('Cypress');
cy.get('@results').should('be.visible');
cy.get('@searchBox').clear().type('Testing');
});
it('łączy selektory dla precyzyjnego targetowania', () => {
// Selektor wielopoziomowy
cy.get('form#login-form input[type="text"]').first().type('user');
// Kombinacja atrybutów
cy.get('[data-cy="submit"][type="submit"]').click();
// Nested selektory
cy.get('.modal').within(() => {
cy.get('[data-cy="modal-title"]').should('contain', 'Potwierdź');
cy.get('[data-cy="confirm-btn"]').click();
});
});
it('obsługuje dynamiczne selektory', () => {
const userId = '12345';
// Interpolacja w selektorze
cy.get(`[data-user-id="${userId}"]`).click();
cy.get(`#user-${userId}`).should('be.visible');
// Dynamiczne wyszukiwanie po tekście
const buttonText = 'Zapisz zmiany';
cy.contains('button', buttonText).click();
});
});
Diagram:
graph TD
A[Cypress Selektory] --> B[Podstawowe Komendy]
A --> C[Metody Traversal]
A --> D[Best Practices]
B --> B1[cy.get - selektory CSS]
B --> B2[cy.contains - tekst]
B --> B3[cy.find - potomkowie]
C --> C1[.parent - rodzic]
C --> C2[.children - dzieci]
C --> C3[.siblings - rodzeństwo]
C --> C4[.first/.last - pierwszy/ostatni]
C --> C5[.eq - po indeksie]
C --> C6[.filter/.not - filtrowanie]
D --> D1[Używaj data-cy]
D --> D2[Unikaj selektorów CSS]
D --> D3[Stosuj aliasy]
D --> D4[Within dla kontekstu]
style A fill:#f9f,stroke:#333,stroke-width:4px
style D1 fill:#bfb,stroke:#333,stroke-width:2px
Materiały:
Czym jest atrybut data-cy i dlaczego jest zalecany?
Odpowiedź w 30 sekund:
data-cy to niestandardowy atrybut HTML używany wyłącznie do celów testowych w Cypress. Jest zalecany, ponieważ oddziela logikę testową od implementacji UI - zmiany w klasach CSS, stylach czy strukturze DOM nie psują testów. Zapewnia stabilne i czytelne selektory niezależne od warstwy prezentacji.
Odpowiedź w 2 minuty:
Atrybut data-cy (lub alternatywnie data-test, data-testid) to specjalny atrybut HTML wprowadzony jako best practice w testowaniu aplikacji z Cypress. Jego głównym celem jest wyraźne oddzielenie warstwy testowej od implementacji interfejsu użytkownika. W przeciwieństwie do klas CSS czy identyfikatorów, które mogą się zmieniać podczas refaktoryzacji stylów lub logiki biznesowej, data-cy służy wyłącznie testom i pozostaje stabilny.
Stosowanie data-cy przynosi kilka kluczowych korzyści. Po pierwsze, czyni testy odpornymi na zmiany w kodzie produkcyjnym - zmiana nazwy klasy CSS nie wymaga aktualizacji testów. Po drugie, poprawia czytelność testów - cy.get('[data-cy="submit-button"]') jest bardziej zrozumiałe niż cy.get('.btn.btn-primary.mt-3'). Po trzecie, stanowi jasny kontrakt między deweloperami a testerami - każdy element z data-cy jest świadomie oznaczony jako testowany.
W praktyce zaleca się dodawanie atrybutów data-cy do wszystkich interaktywnych elementów (przyciski, pola formularzy, linki) oraz kluczowych elementów strukturalnych (nagłówki sekcji, kontenery modali). Wartości atrybutów powinny być opisowe i stabilne, preferując kebab-case dla spójności. W środowisku produkcyjnym można je usuwać podczas budowania aplikacji, aby zmniejszyć rozmiar HTML.
Warto zauważyć, że konwencja nazewnictwa może się różnić między projektami - niektóre zespoły używają data-test, data-testid czy data-qa. Najważniejsze jest zachowanie konsekwencji w całym projekcie i skonfigurowanie odpowiednich zasad w ESLint czy innych narzędziach do code review.
Przykład kodu:
// ========================================
// Przykład komponentu React z data-cy
// ========================================
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// Logika logowania
};
return (
<form onSubmit={handleSubmit} data-cy="login-form">
<h2 data-cy="form-title">Zaloguj się</h2>
{error && (
<div className="alert alert-danger" data-cy="error-message">
{error}
</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
data-cy="email-input"
placeholder="Wpisz email"
/>
</div>
<div className="form-group">
<label htmlFor="password">Hasło</label>
<input
id="password"
type="password"
className="form-control"
value={password}
onChange={(e) => setPassword(e.target.value)}
data-cy="password-input"
placeholder="Wpisz hasło"
/>
</div>
<button
type="submit"
className="btn btn-primary"
data-cy="submit-button"
>
Zaloguj
</button>
<a href="/forgot-password" data-cy="forgot-password-link">
Zapomniałeś hasła?
</a>
</form>
);
}
// ========================================
// Test Cypress używający data-cy
// ========================================
describe('Formularz logowania', () => {
beforeEach(() => {
cy.visit('/login');
});
it('wyświetla wszystkie elementy formularza', () => {
// Sprawdzenie widoczności kluczowych elementów
cy.get('[data-cy="login-form"]').should('be.visible');
cy.get('[data-cy="form-title"]').should('contain', 'Zaloguj się');
cy.get('[data-cy="email-input"]').should('be.visible');
cy.get('[data-cy="password-input"]').should('be.visible');
cy.get('[data-cy="submit-button"]').should('be.visible');
cy.get('[data-cy="forgot-password-link"]').should('be.visible');
});
it('pozwala użytkownikowi się zalogować', () => {
// Wypełnienie formularza
cy.get('[data-cy="email-input"]').type('jan@example.com');
cy.get('[data-cy="password-input"]').type('SecurePass123');
cy.get('[data-cy="submit-button"]').click();
// Weryfikacja przekierowania
cy.url().should('include', '/dashboard');
});
it('wyświetla błąd przy nieprawidłowych danych', () => {
cy.get('[data-cy="email-input"]').type('wrong@example.com');
cy.get('[data-cy="password-input"]').type('wrongpass');
cy.get('[data-cy="submit-button"]').click();
// Sprawdzenie komunikatu błędu
cy.get('[data-cy="error-message"]')
.should('be.visible')
.and('contain', 'Nieprawidłowe dane logowania');
});
it('nawiguje do strony odzyskiwania hasła', () => {
cy.get('[data-cy="forgot-password-link"]').click();
cy.url().should('include', '/forgot-password');
});
});
// ========================================
// Przykład komponentu Vue z data-cy
// ========================================
/*
<template>
<div class="product-card" :data-cy="`product-${product.id}`">
<img
:src="product.image"
:alt="product.name"
:data-cy="`product-image-${product.id}`"
/>
<h3 data-cy="product-name">{{ product.name }}</h3>
<p data-cy="product-price" class="price">
{{ formatPrice(product.price) }}
</p>
<div class="quantity-controls">
<button
@click="decrementQuantity"
data-cy="decrease-quantity"
:disabled="quantity === 0"
>
-
</button>
<span data-cy="quantity-value">{{ quantity }}</span>
<button
@click="incrementQuantity"
data-cy="increase-quantity"
>
+
</button>
</div>
<button
@click="addToCart"
data-cy="add-to-cart"
class="btn-add-to-cart"
:disabled="quantity === 0"
>
Dodaj do koszyka
</button>
</div>
</template>
*/
// Test dla komponentu produktu
describe('Karta produktu', () => {
beforeEach(() => {
cy.visit('/products/123');
});
it('pozwala dodać produkt do koszyka', () => {
// Sprawdzenie początkowej ilości
cy.get('[data-cy="quantity-value"]').should('have.text', '0');
// Zwiększenie ilości
cy.get('[data-cy="increase-quantity"]').click().click().click();
cy.get('[data-cy="quantity-value"]').should('have.text', '3');
// Dodanie do koszyka
cy.get('[data-cy="add-to-cart"]').click();
// Weryfikacja
cy.get('[data-cy="cart-badge"]').should('contain', '3');
});
});
// ========================================
// Custom command dla uproszczenia
// ========================================
// cypress/support/commands.js
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy="${value}"]`);
});
// Użycie custom command
describe('Test z custom command', () => {
it('używa uproszczonej składni', () => {
cy.visit('/login');
// Zamiast cy.get('[data-cy="email-input"]')
cy.dataCy('email-input').type('test@example.com');
cy.dataCy('password-input').type('password123');
cy.dataCy('submit-button').click();
cy.dataCy('welcome-message').should('be.visible');
});
});
Diagram:
graph LR
A[Strategie Selektorów] --> B[Złe Praktyki]
A --> C[Dobre Praktyki]
B --> B1[.btn.primary - Klasy CSS<br/>Zmieniają się często]
B --> B2[#submit-btn-123 - ID<br/>Może być dynamiczne]
B --> B3[div > form > button:nth-child - Struktura<br/>Krucha przy zmianach]
C --> C1[data-cy='submit-btn'<br/>✓ Stabilny<br/>✓ Czytelny<br/>✓ Niezależny od UI]
C1 --> D[Korzyści]
D --> D1[Odporność na refaktoring]
D --> D2[Jasny kontrakt test-dev]
D --> D3[Łatwa konserwacja]
D --> D4[Lepsza dokumentacja]
style B1 fill:#fbb,stroke:#333,stroke-width:2px
style B2 fill:#fbb,stroke:#333,stroke-width:2px
style B3 fill:#fbb,stroke:#333,stroke-width:2px
style C1 fill:#bfb,stroke:#333,stroke-width:2px
Materiały:
Jak wykonać kliknięcie, wpisanie tekstu i inne interakcje?
Odpowiedź w 30 sekund:
Cypress oferuje intuicyjne komendy do interakcji: .click() dla kliknięć, .type() dla wpisywania tekstu, .clear() dla czyszczenia pól, .check() i .uncheck() dla checkboxów, .select() dla dropdownów. Wszystkie komendy są automatycznie retry'owane i czekają na możliwość wykonania akcji. Obsługują również opcje modyfikatorów jak force, multiple czy delay.
Odpowiedź w 2 minuty:
Cypress dostarcza bogaty zestaw komend do symulowania interakcji użytkownika z aplikacją webową. Najczęściej używane to .click() do klikania elementów, .type() do wprowadzania tekstu, .clear() do czyszczenia pól tekstowych oraz .dblclick() do podwójnego kliknięcia. Każda z tych komend automatycznie czeka, aż element stanie się widoczny, interaktywny i nie będzie zakryty przez inne elementy.
Komenda .type() jest szczególnie zaawansowana - obsługuje znaki specjalne (np. {enter}, {backspace}, {selectall}), modyfikatory klawiatury ({ctrl}, {shift}, {alt}), oraz opcje jak delay do kontrolowania prędkości pisania czy force do wymuszenia akcji nawet gdy element jest zakryty. Można również symulować zachowania użytkownika jak zaznaczanie tekstu i jego zastępowanie.
Dla formularzy dostępne są dedykowane komendy: .check() i .uncheck() dla checkboxów i radio buttonów, .select() dla elementów select, oraz .focus() i .blur() dla zarządzania fokusem. Cypress automatycznie przewija stronę do elementu przed wykonaniem akcji i sprawdza, czy element nie jest disabled.
Wszystkie interakcje można dostosować za pomocą opcji - na przykład .click({ force: true }) wymusza kliknięcie pomijając sprawdzanie widoczności, .click({ multiple: true }) klika wszystkie znalezione elementy, a .type('text', { delay: 100 }) spowalnia pisanie. Istnieje również możliwość symulowania kliknięć w określone punkty elementu używając pozycji jak 'topLeft', 'center', czy współrzędnych x, y.
Przykład kodu:
describe('Interakcje użytkownika w Cypress', () => {
beforeEach(() => {
cy.visit('https://example.com');
});
// ========================================
// PODSTAWOWE INTERAKCJE
// ========================================
it('wykonuje podstawowe kliknięcia', () => {
// Proste kliknięcie
cy.get('[data-cy="submit-btn"]').click();
// Podwójne kliknięcie
cy.get('[data-cy="edit-title"]').dblclick();
// Prawy przycisk myszy (menu kontekstowe)
cy.get('[data-cy="file-item"]').rightclick();
// Kliknięcie w określone miejsce elementu
cy.get('[data-cy="canvas"]').click('topLeft');
cy.get('[data-cy="canvas"]').click('center');
cy.get('[data-cy="canvas"]').click(150, 200); // x, y w pikselach
// Kliknięcie z modyfikatorami
cy.get('[data-cy="item"]').click({ ctrlKey: true }); // Ctrl+Click
cy.get('[data-cy="item"]').click({ shiftKey: true }); // Shift+Click
cy.get('[data-cy="item"]').click({ altKey: true }); // Alt+Click
});
it('korzysta z opcji force dla zakrytych elementów', () => {
// Czasami element jest zakryty przez modal, overlay itp.
// Force pomija sprawdzanie widoczności
cy.get('[data-cy="hidden-button"]').click({ force: true });
// Kliknięcie wielu elementów
cy.get('.checkbox-item').click({ multiple: true });
});
// ========================================
// WPISYWANIE TEKSTU
// ========================================
it('wpisuje tekst w różnych scenariuszach', () => {
// Proste wpisanie
cy.get('[data-cy="username"]').type('jan.kowalski');
// Czyszczenie i wpisanie nowej wartości
cy.get('[data-cy="email"]')
.clear()
.type('nowy@email.com');
// Wpisywanie z opóźnieniem (symulacja wolnego pisania)
cy.get('[data-cy="search"]').type('Cypress Testing', { delay: 100 });
// Wpisywanie bez czyszczenia poprzedniej wartości
cy.get('[data-cy="message"]').type('Dodatkowy tekst');
});
it('używa znaków specjalnych podczas pisania', () => {
cy.get('[data-cy="comment"]').type('To jest komentarz');
// Zaznaczenie całego tekstu i zastąpienie
cy.get('[data-cy="comment"]')
.type('{selectall}')
.type('Nowy tekst');
// Backspace i Delete
cy.get('[data-cy="input"]')
.type('Błądny tekst{backspace}{backspace}{backspace}{backspace}');
// Enter (submit formularza)
cy.get('[data-cy="search-box"]')
.type('Wyszukiwana fraza{enter}');
// Tab (przejście do następnego pola)
cy.get('[data-cy="first-name"]')
.type('Jan{tab}')
.get('[data-cy="last-name"]') // Focus przeszedł automatycznie
.type('Kowalski');
// Kombinacje z Ctrl/Cmd
cy.get('[data-cy="editor"]')
.type('Zaznaczony tekst')
.type('{selectall}')
.type('{ctrl}c'); // Ctrl+C (kopiowanie)
// Strzałki
cy.get('[data-cy="text"]')
.type('Test')
.type('{leftarrow}{leftarrow}') // Kursor 2 znaki w lewo
.type('XX'); // Wstawi XX w środku
});
it('obsługuje znaki specjalne wymagające escape', () => {
// Klamry muszą być podwojone, bo są znakami specjalnymi
cy.get('[data-cy="code-input"]').type('function() {{}}');
// Wpisywanie adresu email
cy.get('[data-cy="email"]').type('user@example.com');
// Wpisywanie URL
cy.get('[data-cy="url"]').type('https://example.com/path?param=value');
});
// ========================================
// CHECKBOXY I RADIO BUTTONY
// ========================================
it('obsługuje checkboxy', () => {
// Zaznaczenie checkboxa
cy.get('[data-cy="terms-checkbox"]').check();
cy.get('[data-cy="terms-checkbox"]').should('be.checked');
// Odznaczenie checkboxa
cy.get('[data-cy="newsletter"]').uncheck();
cy.get('[data-cy="newsletter"]').should('not.be.checked');
// Zaznaczenie wielu checkboxów naraz
cy.get('.permission-checkbox').check({ multiple: true });
// Zaznaczenie checkboxa po wartości
cy.get('input[type="checkbox"]').check('option1');
cy.get('input[type="checkbox"]').check(['option2', 'option3']);
});
it('obsługuje radio buttony', () => {
// Radio button - tylko check, nie ma uncheck
cy.get('[data-cy="payment-credit-card"]').check();
cy.get('[data-cy="payment-credit-card"]').should('be.checked');
// Zaznaczenie radio po wartości
cy.get('input[name="size"]').check('medium');
// Weryfikacja że inne radio zostało odznaczone
cy.get('[data-cy="payment-paypal"]').should('not.be.checked');
});
// ========================================
// DROPDOWN (SELECT)
// ========================================
it('obsługuje elementy select', () => {
// Wybór po tekście widocznym dla użytkownika
cy.get('[data-cy="country-select"]').select('Polska');
// Wybór po wartości (value)
cy.get('[data-cy="country-select"]').select('PL');
// Wybór po indeksie (0-based)
cy.get('[data-cy="month-select"]').select(5); // Czerwiec
// Wybór wielu opcji (dla multiselect)
cy.get('[data-cy="languages"]').select(['pl', 'en', 'de']);
// Weryfikacja wybranej opcji
cy.get('[data-cy="country-select"]')
.should('have.value', 'PL');
});
// ========================================
// FOCUS I BLUR
// ========================================
it('zarządza fokusem elementów', () => {
// Ustawienie fokusa
cy.get('[data-cy="username"]').focus();
cy.get('[data-cy="username"]').should('have.focus');
// Wpisanie i utrata fokusa (blur)
cy.get('[data-cy="email"]')
.type('test@example.com')
.blur();
// Blur może wywołać walidację
cy.get('[data-cy="validation-error"]').should('not.exist');
});
// ========================================
// HOVER (najechanie myszą)
// ========================================
it('symuluje najechanie myszą', () => {
// Cypress nie ma natywnej komendy .hover()
// Używamy .trigger() do wywołania zdarzeń myszy
cy.get('[data-cy="menu-item"]').trigger('mouseover');
// Sprawdzenie czy submenu się pokazało
cy.get('[data-cy="submenu"]').should('be.visible');
// Zjechanie z elementu
cy.get('[data-cy="menu-item"]').trigger('mouseout');
});
// ========================================
// SCROLLOWANIE
// ========================================
it('przewija stronę i elementy', () => {
// Przewinięcie do elementu (automatyczne przy większości akcji)
cy.get('[data-cy="footer"]').scrollIntoView();
cy.get('[data-cy="footer"]').should('be.visible');
// Przewinięcie do pozycji
cy.scrollTo('bottom');
cy.scrollTo('top');
cy.scrollTo('center');
// Przewinięcie do współrzędnych
cy.scrollTo(0, 500); // x, y
// Przewinięcie w kontenerze z overflow
cy.get('[data-cy="scrollable-list"]').scrollTo('bottom');
});
// ========================================
// ZŁOŻONE SCENARIUSZE
// ========================================
it('symuluje wypełnienie kompleksowego formularza', () => {
// Dane osobowe
cy.get('[data-cy="first-name"]').type('Jan');
cy.get('[data-cy="last-name"]').type('Kowalski');
cy.get('[data-cy="email"]').type('jan.kowalski@example.com');
// Data urodzenia
cy.get('[data-cy="birth-day"]').select('15');
cy.get('[data-cy="birth-month"]').select('05');
cy.get('[data-cy="birth-year"]').select('1990');
// Płeć (radio)
cy.get('[data-cy="gender-male"]').check();
// Zainteresowania (checkboxy)
cy.get('[data-cy="interest-sport"]').check();
cy.get('[data-cy="interest-music"]').check();
cy.get('[data-cy="interest-travel"]').check();
// Kraj
cy.get('[data-cy="country"]').select('Polska');
// Zgody
cy.get('[data-cy="terms"]').check();
cy.get('[data-cy="privacy"]').check();
// Komentarz
cy.get('[data-cy="message"]')
.type('To jest testowy komentarz.{enter}Nowa linia tekstu.');
// Submit
cy.get('[data-cy="submit-form"]').click();
// Weryfikacja
cy.url().should('include', '/success');
});
});
Diagram:
graph TD
A[Interakcje Cypress] --> B[Kliknięcia]
A --> C[Wprowadzanie Tekstu]
A --> D[Elementy Formularzy]
A --> E[Zaawansowane]
B --> B1[.click]
B --> B2[.dblclick]
B --> B3[.rightclick]
B --> B4[Opcje: force, multiple, position]
C --> C1[.type - wpisywanie]
C --> C2[.clear - czyszczenie]
C --> C3[Znaki specjalne: enter, tab, backspace]
C --> C4[Modyfikatory: ctrl, shift, alt]
D --> D1[.check/.uncheck - checkbox]
D --> D2[.select - dropdown]
D --> D3[.focus/.blur - fokus]
E --> E1[.trigger - zdarzenia]
E --> E2[.scrollIntoView - scroll]
E --> E3[.invoke - metody DOM]
style A fill:#f9f,stroke:#333,stroke-width:4px
style B1 fill:#bbf,stroke:#333,stroke-width:2px
style C1 fill:#bbf,stroke:#333,stroke-width:2px
style D1 fill:#bbf,stroke:#333,stroke-width:2px
Materiały:
Jak obsługiwać formularze w Cypress?
Odpowiedź w 30 sekund:
Formularze w Cypress obsługuje się przez wypełnianie pól komendą .type(), wybieranie opcji przez .select(), zaznaczanie checkboxów przez .check(), a następnie submit przez .click() na przycisku lub .submit() na formularzu. Cypress automatycznie czeka na gotowość elementów i weryfikuje możliwość interakcji. Można też używać .invoke('val', value) do bezpośredniego ustawiania wartości.
Odpowiedź w 2 minuty:
Obsługa formularzy w Cypress jest intuicyjna i przypomina rzeczywiste interakcje użytkownika. Podstawowy workflow polega na lokalizowaniu pól formularza (preferowane przez atrybuty data-cy), wypełnianiu ich odpowiednimi danymi używając .type(), .select(), .check(), a następnie wysłaniu formularza przez kliknięcie przycisku submit lub wywołanie .submit() bezpośrednio na elemencie <form>.
Cypress oferuje kilka strategii wypełniania formularzy. Najprostsza to sekwencyjne wypełnianie każdego pola z użyciem .type() dla tekstów, .select() dla dropdownów i .check() dla checkboxów. Dla bardziej zaawansowanych scenariuszy można użyć .invoke('val', value) do bezpośredniego ustawienia wartości bez symulowania pisania, co jest szybsze ale pomija walidację onChange. Można też łączyć te podejścia w zależności od potrzeb.
Ważnym aspektem jest testowanie walidacji formularzy. Cypress pozwala sprawdzać komunikaty błędów, disabled state przycisków submit, wymagane pola oraz walidację po stronie klienta. Można testować zarówno walidację inline (podczas wypełniania) jak i po wysłaniu formularza. Przydatne są asercje .should('have.attr', 'required') czy .should('have.class', 'is-invalid').
Dla formularzy z dynamicznymi polami (dodawanie/usuwanie sekcji) Cypress radzi sobie dobrze dzięki automatycznemu retry. Można również testować upload plików używając .selectFile(), czyszczenie formularzy, zapisywanie drafts oraz przywracanie wartości. Best practice to tworzenie custom commands dla często wypełnianych formularzy (np. cy.fillLoginForm(email, password)) aby zwiększyć reużywalność i czytelność testów.
Przykład kodu:
describe('Obsługa formularzy w Cypress', () => {
// ========================================
// PODSTAWOWE WYPEŁNIANIE FORMULARZA
// ========================================
it('wypełnia i wysyła prosty formularz kontaktowy', () => {
cy.visit('/contact');
// Wypełnienie wszystkich pól
cy.get('[data-cy="name-input"]').type('Jan Kowalski');
cy.get('[data-cy="email-input"]').type('jan@example.com');
cy.get('[data-cy="phone-input"]').type('+48 123 456 789');
cy.get('[data-cy="subject-select"]').select('Zapytanie ogólne');
cy.get('[data-cy="message-textarea"]')
.type('To jest treść wiadomości testowej.{enter}Nowa linia.');
// Zaznaczenie zgody
cy.get('[data-cy="consent-checkbox"]').check();
// Wysłanie formularza przez kliknięcie przycisku
cy.get('[data-cy="submit-button"]').click();
// Weryfikacja sukcesu
cy.get('[data-cy="success-message"]')
.should('be.visible')
.and('contain', 'Dziękujemy za wiadomość');
});
// ========================================
// FORMULARZ REJESTRACJI Z WALIDACJĄ
// ========================================
it('testuje walidację formularza rejestracji', () => {
cy.visit('/register');
// Próba wysłania pustego formularza
cy.get('[data-cy="register-button"]').click();
// Sprawdzenie komunikatów błędów
cy.get('[data-cy="username-error"]')
.should('be.visible')
.and('contain', 'Nazwa użytkownika jest wymagana');
cy.get('[data-cy="email-error"]')
.should('be.visible')
.and('contain', 'Email jest wymagany');
// Wypełnienie nieprawidłowym emailem
cy.get('[data-cy="email-input"]').type('nieprawidlowy-email');
cy.get('[data-cy="email-input"]').blur();
cy.get('[data-cy="email-error"]')
.should('contain', 'Podaj prawidłowy adres email');
// Poprawienie emaila
cy.get('[data-cy="email-input"]')
.clear()
.type('poprawny@example.com');
// Błąd powinien zniknąć
cy.get('[data-cy="email-error"]').should('not.exist');
// Test zgodności haseł
cy.get('[data-cy="password-input"]').type('SecurePass123!');
cy.get('[data-cy="confirm-password-input"]').type('DifferentPass');
cy.get('[data-cy="confirm-password-input"]').blur();
cy.get('[data-cy="password-match-error"]')
.should('contain', 'Hasła muszą być identyczne');
// Poprawienie hasła
cy.get('[data-cy="confirm-password-input"]')
.clear()
.type('SecurePass123!');
// Wypełnienie pozostałych wymaganych pól
cy.get('[data-cy="username-input"]').type('jankowalski');
cy.get('[data-cy="terms-checkbox"]').check();
// Teraz submit powinien działać
cy.get('[data-cy="register-button"]').should('not.be.disabled');
cy.get('[data-cy="register-button"]').click();
// Weryfikacja przekierowania
cy.url().should('include', '/welcome');
});
// ========================================
// FORMULARZ Z DYNAMICZNYMI POLAMI
// ========================================
it('obsługuje dynamiczne dodawanie i usuwanie pól', () => {
cy.visit('/job-application');
// Początkowe dane
cy.get('[data-cy="applicant-name"]').type('Jan Kowalski');
// Sprawdzenie że jest tylko jedna sekcja doświadczenia
cy.get('[data-cy^="experience-"]').should('have.length', 1);
// Wypełnienie pierwszego doświadczenia
cy.get('[data-cy="experience-0-company"]').type('Firma ABC');
cy.get('[data-cy="experience-0-position"]').type('Frontend Developer');
cy.get('[data-cy="experience-0-years"]').select('2-3 lata');
// Dodanie kolejnego doświadczenia
cy.get('[data-cy="add-experience-button"]').click();
// Sprawdzenie że jest już 2 sekcje
cy.get('[data-cy^="experience-"]').should('have.length', 2);
// Wypełnienie drugiego doświadczenia
cy.get('[data-cy="experience-1-company"]').type('Firma XYZ');
cy.get('[data-cy="experience-1-position"]').type('Junior Developer');
cy.get('[data-cy="experience-1-years"]').select('1-2 lata');
// Usunięcie pierwszego doświadczenia
cy.get('[data-cy="remove-experience-0"]').click();
// Weryfikacja że został tylko jeden element
cy.get('[data-cy^="experience-"]').should('have.length', 1);
// Weryfikacja że pozostał właściwy element
cy.get('[data-cy="experience-0-company"]').should('have.value', 'Firma XYZ');
});
// ========================================
// UPLOAD PLIKÓW
// ========================================
it('obsługuje upload plików', () => {
cy.visit('/upload-document');
// Upload jednego pliku
cy.get('[data-cy="file-input"]').selectFile('cypress/fixtures/cv.pdf');
// Weryfikacja nazwy pliku
cy.get('[data-cy="file-name"]').should('contain', 'cv.pdf');
// Upload wielu plików
cy.get('[data-cy="multiple-files-input"]').selectFile([
'cypress/fixtures/photo1.jpg',
'cypress/fixtures/photo2.jpg'
]);
// Weryfikacja ilości plików
cy.get('[data-cy="uploaded-files-list"] li').should('have.length', 2);
// Upload przez drag & drop
cy.get('[data-cy="drop-zone"]').selectFile('cypress/fixtures/document.docx', {
action: 'drag-drop'
});
});
// ========================================
// BEZPOŚREDNIE USTAWIANIE WARTOŚCI
// ========================================
it('używa invoke do szybkiego wypełniania formularza', () => {
cy.visit('/profile-edit');
// Bezpośrednie ustawienie wartości (szybsze, ale pomija zdarzenia)
cy.get('[data-cy="bio-textarea"]').invoke('val', 'Moja biografia testowa');
// Wywołanie zdarzenia input aby uruchomić walidację
cy.get('[data-cy="bio-textarea"]').trigger('input');
// Weryfikacja licznika znaków
cy.get('[data-cy="char-counter"]').should('contain', '23');
// Kombinacja invoke i type dla konkretnych scenariuszy
cy.get('[data-cy="tags-input"]')
.invoke('val', 'JavaScript, React') // Wstępne dane
.type(', TypeScript{enter}'); // Dodanie kolejnego taga
});
// ========================================
// CUSTOM COMMAND DLA FORMULARZY
// ========================================
// W pliku cypress/support/commands.js:
/*
Cypress.Commands.add('fillLoginForm', (email, password) => {
cy.get('[data-cy="email-input"]').clear().type(email);
cy.get('[data-cy="password-input"]').clear().type(password);
});
Cypress.Commands.add('submitForm', (formSelector) => {
cy.get(formSelector).within(() => {
cy.get('[type="submit"]').click();
});
});
*/
it('używa custom commands dla czytelności', () => {
cy.visit('/login');
// Użycie custom command
cy.fillLoginForm('test@example.com', 'password123');
cy.submitForm('[data-cy="login-form"]');
// Weryfikacja
cy.url().should('include', '/dashboard');
});
// ========================================
// FORMULARZ Z AUTOUZUPEŁNIANIEM
// ========================================
it('obsługuje autocomplete/autosuggest', () => {
cy.visit('/search');
// Wpisanie tekstu wywołującego sugestie
cy.get('[data-cy="search-input"]').type('Cypre');
// Czekanie na pojawienie się sugestii
cy.get('[data-cy="suggestions-list"]').should('be.visible');
// Sprawdzenie czy pierwsza sugestia zawiera wpisany tekst
cy.get('[data-cy="suggestion-0"]').should('contain', 'Cypress');
// Kliknięcie w sugestię
cy.get('[data-cy="suggestion-0"]').click();
// Weryfikacja że wartość została wstawiona
cy.get('[data-cy="search-input"]').should('have.value', 'Cypress Testing');
});
// ========================================
// RESETOWANIE I CZYSZCZENIE FORMULARZA
// ========================================
it('testuje czyszczenie formularza', () => {
cy.visit('/feedback');
// Wypełnienie formularza
cy.get('[data-cy="name"]').type('Jan Kowalski');
cy.get('[data-cy="email"]').type('jan@example.com');
cy.get('[data-cy="rating"]').select('5');
cy.get('[data-cy="comment"]').type('Świetna aplikacja!');
// Kliknięcie przycisku reset/clear
cy.get('[data-cy="clear-button"]').click();
// Weryfikacja że pola zostały wyczyszczone
cy.get('[data-cy="name"]').should('have.value', '');
cy.get('[data-cy="email"]').should('have.value', '');
cy.get('[data-cy="rating"]').should('have.value', '');
cy.get('[data-cy="comment"]').should('have.value', '');
});
// ========================================
// FORMULARZ WIELOSTRONICOWY (WIZARD)
// ========================================
it('przechodzi przez formularz wielostronicowy', () => {
cy.visit('/registration-wizard');
// Krok 1: Dane osobowe
cy.get('[data-cy="current-step"]').should('contain', 'Krok 1 z 3');
cy.get('[data-cy="first-name"]').type('Jan');
cy.get('[data-cy="last-name"]').type('Kowalski');
cy.get('[data-cy="next-button"]').click();
// Krok 2: Dane kontaktowe
cy.get('[data-cy="current-step"]').should('contain', 'Krok 2 z 3');
cy.get('[data-cy="email"]').type('jan@example.com');
cy.get('[data-cy="phone"]').type('123456789');
cy.get('[data-cy="next-button"]').click();
// Krok 3: Podsumowanie
cy.get('[data-cy="current-step"]').should('contain', 'Krok 3 z 3');
cy.get('[data-cy="summary-name"]').should('contain', 'Jan Kowalski');
cy.get('[data-cy="summary-email"]').should('contain', 'jan@example.com');
// Możliwość powrotu
cy.get('[data-cy="back-button"]').click();
cy.get('[data-cy="current-step"]').should('contain', 'Krok 2 z 3');
// Powrót do kroku 3 i submit
cy.get('[data-cy="next-button"]').click();
cy.get('[data-cy="submit-button"]').click();
// Weryfikacja sukcesu
cy.url().should('include', '/success');
});
});
Diagram:
graph TD
A[Strategie Testowania Formularzy] --> B[Wypełnianie]
A --> C[Walidacja]
A --> D[Submit]
A --> E[Edge Cases]
B --> B1[.type - symulacja użytkownika]
B --> B2[.invoke - bezpośrednie wartości]
B --> B3[.select - dropdowny]
B --> B4[.check - checkboxy]
B --> B5[.selectFile - upload]
C --> C1[Walidacja inline]
C --> C2[Komunikaty błędów]
C --> C3[Disabled state]
C --> C4[Required fields]
D --> D1[.click na button]
D --> D2[.submit na form]
D --> D3[Enter w polu]
E --> E1[Dynamiczne pola]
E --> E2[Autocomplete]
E --> E3[Multi-step wizards]
E --> E4[Czyszczenie formularza]
style A fill:#f9f,stroke:#333,stroke-width:4px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bfb,stroke:#333,stroke-width:2px
Materiały:
Jak pracować z elementami dynamicznymi i oczekiwać na ich pojawienie się?
Odpowiedź w 30 sekund:
Cypress automatycznie retry'uje komendy przez 4 sekundy (domyślny timeout), więc elementy dynamiczne są obsługiwane out-of-the-box. Można dostosować timeout przez opcję { timeout: 10000 }. Dla bardziej złożonych scenariuszy używa się .should() z callback, cy.wait() dla requesta API, lub cy.waitUntil() z pluginu dla custom warunków.
Odpowiedź w 2 minuty:
Cypress został zaprojektowany z myślą o aplikacjach Single Page Application (SPA), które dynamicznie ładują i zmieniają content. W przeciwieństwie do innych narzędzi testowych, Cypress ma wbudowany mechanizm automatycznego retry dla większości komend - jeśli element nie istnieje lub nie spełnia warunku, Cypress będzie ponawiać próby przez określony czas (domyślnie 4 sekundy) zanim rzuci błąd.
Ten built-in retry działa dla komend query (.get(), .find(), .contains()) oraz asercji (.should()). Oznacza to, że w większości przypadków nie musisz explicite czekać - wystarczy napisać cy.get('[data-cy="dynamic-element"]').should('be.visible') i Cypress będzie sprawdzać warunek aż się spełni. Możesz dostosować timeout dla konkretnej komendy przez opcję { timeout: 10000 } lub globalnie w konfiguracji.
Dla elementów pojawiających się po zapytaniach API best practice to użycie cy.intercept() do przechwycenia requestu i cy.wait('@alias') do oczekiwania na odpowiedź. To daje pewność, że czekasz na konkretne zdarzenie, a nie arbitralny czas. Możesz również używać cy.waitUntil() z cypress-wait-until plugin dla bardziej zaawansowanych warunków oczekiwania.
Ważne jest unikanie cy.wait(1000) z hardcoded czasem - to anti-pattern prowadzący do niestabilnych testów (flaky tests). Zamiast tego zawsze czekaj na konkretny warunek: pojawienie się elementu, zmianę tekstu, wykonanie requestu API. Cypress oferuje również metody jak .should('have.length.greaterThan', 0) dla dynamicznych list czy .should('not.exist') dla elementów które powinny zniknąć.
Przykład kodu:
describe('Praca z elementami dynamicznymi w Cypress', () => {
// ========================================
// AUTOMATYCZNE RETRY (BUILT-IN)
// ========================================
it('automatycznie czeka na pojawienie się elementu', () => {
cy.visit('/dashboard');
// Element pojawia się po załadowaniu danych - Cypress czeka automatycznie
cy.get('[data-cy="user-profile"]').should('be.visible');
// Kliknięcie przycisku który pokazuje modal
cy.get('[data-cy="open-modal"]').click();
// Modal pojawia się z animacją - Cypress czeka automatycznie
cy.get('[data-cy="modal"]').should('be.visible');
cy.get('[data-cy="modal-title"]').should('contain', 'Witaj');
});
// ========================================
// DOSTOSOWANIE TIMEOUTU
// ========================================
it('używa custom timeout dla wolno ładujących się elementów', () => {
cy.visit('/reports');
// Element może się ładować do 15 sekund
cy.get('[data-cy="complex-chart"]', { timeout: 15000 })
.should('be.visible');
// Alternatywnie - timeout w should()
cy.get('[data-cy="data-table"]')
.should('be.visible', { timeout: 10000 });
});
// ========================================
// CZEKANIE NA REQUESTY API
// ========================================
it('czeka na konkretne zapytanie API', () => {
// Przechwycenie requestu przed wizytą
cy.intercept('GET', '/api/users*').as('getUsers');
cy.visit('/users');
// Czekanie na zakończenie requestu
cy.wait('@getUsers').then((interception) => {
// Weryfikacja odpowiedzi
expect(interception.response.statusCode).to.equal(200);
expect(interception.response.body).to.have.length.greaterThan(0);
});
// Teraz elementy na pewno są załadowane
cy.get('[data-cy="user-list"] li').should('have.length.greaterThan', 0);
});
it('czeka na wiele requestów', () => {
cy.intercept('GET', '/api/users').as('getUsers');
cy.intercept('GET', '/api/settings').as('getSettings');
cy.intercept('GET', '/api/notifications').as('getNotifications');
cy.visit('/dashboard');
// Czekanie na wszystkie requesty
cy.wait(['@getUsers', '@getSettings', '@getNotifications']);
// Wszystkie dane są załadowane
cy.get('[data-cy="dashboard-content"]').should('be.visible');
});
// ========================================
// DYNAMICZNE LISTY
// ========================================
it('pracuje z dynamicznie ładowaną listą', () => {
cy.visit('/products');
// Czekanie aż lista będzie miała przynajmniej 1 element
cy.get('[data-cy="product-list"] .product-item')
.should('have.length.greaterThan', 0);
// Czekanie na konkretną ilość elementów
cy.get('[data-cy="product-list"] .product-item')
.should('have.length', 12);
// Kliknięcie "Załaduj więcej"
cy.get('[data-cy="load-more"]').click();
// Czekanie aż liczba elementów wzrośnie
cy.get('[data-cy="product-list"] .product-item')
.should('have.length', 24);
});
// ========================================
// INFINITE SCROLL
// ========================================
it('obsługuje infinite scroll', () => {
cy.intercept('GET', '/api/posts?page=*').as('getPosts');
cy.visit('/feed');
// Początkowe posty załadowane
cy.wait('@getPosts');
cy.get('.post-item').should('have.length', 10);
// Scroll w dół aby załadować więcej
cy.scrollTo('bottom');
// Czekanie na kolejny request
cy.wait('@getPosts');
// Sprawdzenie że liczba postów wzrosła
cy.get('.post-item').should('have.length', 20);
// Kolejny scroll
cy.scrollTo('bottom');
cy.wait('@getPosts');
cy.get('.post-item').should('have.length', 30);
});
// ========================================
// ELEMENTY ZNIKAJĄCE
// ========================================
it('czeka aż element zniknie', () => {
cy.visit('/page');
// Loader widoczny na początku
cy.get('[data-cy="loading-spinner"]').should('be.visible');
// Czekanie aż loader zniknie
cy.get('[data-cy="loading-spinner"]').should('not.exist');
// lub
cy.get('[data-cy="loading-spinner"]').should('not.be.visible');
// Teraz content jest widoczny
cy.get('[data-cy="page-content"]').should('be.visible');
});
// ========================================
// TOAST NOTIFICATIONS
// ========================================
it('obsługuje przemijające notyfikacje', () => {
cy.visit('/dashboard');
// Akcja wywołująca notyfikację
cy.get('[data-cy="save-button"]').click();
// Notyfikacja pojawia się
cy.get('[data-cy="toast-notification"]')
.should('be.visible')
.and('contain', 'Zapisano pomyślnie');
// Czekanie aż zniknie (auto-hide po 3 sekundach)
cy.get('[data-cy="toast-notification"]', { timeout: 5000 })
.should('not.exist');
});
// ========================================
// SEARCH Z DEBOUNCE
// ========================================
it('obsługuje search z debounce/throttle', () => {
cy.intercept('GET', '/api/search?q=*').as('search');
cy.visit('/search');
// Wpisanie zapytania
cy.get('[data-cy="search-input"]').type('Cypress Testing');
// Czekanie na request (debounce zwykle 300-500ms)
cy.wait('@search');
// Wyniki pojawiły się
cy.get('[data-cy="search-results"]').should('be.visible');
cy.get('.search-result-item').should('have.length.greaterThan', 0);
});
// ========================================
// WARUNKOWE CZEKANIE NA ELEMENT
// ========================================
it('używa should() z funkcją callback', () => {
cy.visit('/dashboard');
// Czekanie aż element będzie miał konkretny tekst
cy.get('[data-cy="status"]').should(($el) => {
expect($el).to.have.text('Aktywny');
});
// Czekanie na konkretną klasę
cy.get('[data-cy="badge"]').should(($badge) => {
expect($badge).to.have.class('badge-success');
});
// Złożony warunek
cy.get('[data-cy="counter"]').should(($counter) => {
const count = parseInt($counter.text());
expect(count).to.be.greaterThan(0);
expect(count).to.be.lessThan(100);
});
});
// ========================================
// SKELETON LOADERS
// ========================================
it('czeka aż skeleton loader zostanie zastąpiony danymi', () => {
cy.visit('/profile');
// Skeleton loader widoczny
cy.get('[data-cy="profile-skeleton"]').should('be.visible');
cy.get('[data-cy="profile-data"]').should('not.exist');
// Czekanie aż dane się załadują
cy.get('[data-cy="profile-skeleton"]').should('not.exist');
cy.get('[data-cy="profile-data"]').should('be.visible');
// Weryfikacja załadowanych danych
cy.get('[data-cy="user-name"]').should('not.be.empty');
cy.get('[data-cy="user-avatar"]').should('have.attr', 'src');
});
// ========================================
// ANIMACJE I TRANSITIONS
// ========================================
it('czeka na zakończenie animacji', () => {
cy.visit('/page');
// Kliknięcie triggera animacji
cy.get('[data-cy="expand-section"]').click();
// Czekanie aż element stanie się widoczny (po animacji)
cy.get('[data-cy="expanded-content"]')
.should('be.visible')
.and('have.css', 'opacity', '1'); // Sprawdzenie że animacja się zakończyła
// Alternatywnie - czekanie na klasę wskazującą koniec animacji
cy.get('[data-cy="animated-element"]')
.should('have.class', 'animation-complete');
});
// ========================================
// UŻYCIE CYPRESS-WAIT-UNTIL (PLUGIN)
// ========================================
/*
// Wymagane: npm install -D cypress-wait-until
// W cypress/support/e2e.js: import 'cypress-wait-until';
it('używa waitUntil dla złożonych warunków', () => {
cy.visit('/realtime-dashboard');
// Czekanie aż wartość licznika przekroczy 100
cy.waitUntil(() =>
cy.get('[data-cy="counter"]').then($el => {
const value = parseInt($el.text());
return value > 100;
}),
{
timeout: 10000,
interval: 500,
errorMsg: 'Licznik nie osiągnął wartości > 100'
}
);
// Czekanie na pojawienie się konkretnego tekstu w liście
cy.waitUntil(() =>
cy.get('[data-cy="messages-list"]').then($list => {
return $list.text().includes('Nowa wiadomość');
})
);
});
*/
// ========================================
// ANTI-PATTERN: UNIKAĆ!
// ========================================
it('ZŁE: używanie cy.wait z hardcoded czasem', () => {
cy.visit('/page');
// ❌ ZŁE - arbitralny timeout
cy.wait(3000);
cy.get('[data-cy="content"]').should('be.visible');
// ✅ DOBRE - czekanie na konkretny warunek
cy.get('[data-cy="content"]').should('be.visible');
});
});
Diagram:
graph TD
A[Elementy Dynamiczne w Cypress] --> B[Automatyczne Retry]
A --> C[API Requests]
A --> D[Timeouty]
A --> E[Zaawansowane]
B --> B1[cy.get - retry do 4s]
B --> B2[.should - retry asercji]
B --> B3[.find - retry potomków]
C --> C1[cy.intercept - przechwycenie]
C --> C2[cy.wait alias - czekanie]
C --> C3[Weryfikacja response]
D --> D1[timeout option lokalne]
D --> D2[defaultCommandTimeout globalne]
D --> D3[requestTimeout dla API]
E --> E1[.should callback - warunki]
E --> E2[cy.waitUntil plugin]
E --> E3[Dynamiczne listy]
E --> E4[not.exist - znikanie]
F[Anti-patterns] --> F1[❌ cy.wait hardcoded]
F --> F2[✅ Czekaj na warunki]
style A fill:#f9f,stroke:#333,stroke-width:4px
style B fill:#bfb,stroke:#333,stroke-width:2px
style F1 fill:#fbb,stroke:#333,stroke-width:2px
style F2 fill:#bfb,stroke:#333,stroke-width:2px
Materiały:
Jak obsługiwać dropdown, checkbox i radio button?
Odpowiedź w 30 sekund:
Do obsługi dropdown używa się .select() z tekstem, wartością lub indeksem opcji. Checkboxy zaznacza się przez .check() i odznacza .uncheck(), można również przekazać wartość lub tablicę wartości. Radio buttony obsługuje tylko .check() bo nie można ich odznaczyć. Wszystkie komendy automatycznie scrollują do elementu i weryfikują czy nie jest disabled.
Odpowiedź w 2 minuty:
Cypress oferuje dedykowane komendy do obsługi podstawowych elementów formularzy. Dla tradycyjnych elementów <select> komenda .select() przyjmuje tekst widoczny dla użytkownika, wartość atrybutu value, lub indeks opcji (0-based). Można również wybrać wiele opcji jednocześnie dla elementów multiselect, przekazując tablicę. Cypress automatycznie sprawdza czy element nie jest disabled i czy wybrana opcja istnieje.
Checkboxy obsługuje się przez .check() do zaznaczania i .uncheck() do odznaczania. Komendy te działają zarówno na pojedynczych elementach jak i na grupach checkboxów - można przekazać konkretną wartość lub tablicę wartości do zaznaczenia. Cypress automatycznie pomija elementy już zaznaczone przy .check() i odznaczone przy .uncheck(), dzięki czemu komendy są idempotentne. Weryfikacja stanu odbywa się przez asercje .should('be.checked') lub .should('not.be.checked').
Radio buttony mają specyficzną charakterystykę - można je tylko zaznaczyć (.check()), nie ma komendy .uncheck() ponieważ radio button odznacza się automatycznie gdy zaznaczymy inny z tej samej grupy. Najlepszą praktyką jest targetowanie radio buttonów po atrybucie name i wartości value, lub używanie dedykowanych atrybutów testowych data-cy.
Dla niestandardowych dropdownów (custom select, autocomplete, multi-select) które nie używają natywnego <select>, trzeba stosować standardowe komendy jak .click() i .type(). W takich przypadkach przydatne są komendy .within() do zawężenia kontekstu oraz czekanie na pojawienie się listy opcji. Często wymaga to kombinacji kilku kroków: kliknięcie w trigger, czekanie na listę, kliknięcie w opcję.
Przykład kodu:
describe('Obsługa dropdown, checkbox i radio button', () => {
// ========================================
// DROPDOWN (NATYWNY SELECT)
// ========================================
it('obsługuje natywny element select na różne sposoby', () => {
cy.visit('/form');
// Wybór po tekście widocznym dla użytkownika
cy.get('[data-cy="country-select"]').select('Polska');
// Wybór po wartości (value attribute)
cy.get('[data-cy="country-select"]').select('PL');
// Wybór po indeksie (0-based, 0 to zwykle placeholder)
cy.get('[data-cy="month-select"]').select(5); // Czerwiec
// Weryfikacja wybranej opcji
cy.get('[data-cy="country-select"]')
.should('have.value', 'PL');
// Weryfikacja wyświetlanego tekstu
cy.get('[data-cy="country-select"] option:selected')
.should('have.text', 'Polska');
});
it('obsługuje multiselect', () => {
cy.visit('/preferences');
// Wybór wielu opcji naraz
cy.get('[data-cy="languages-multiselect"]')
.select(['pl', 'en', 'de']);
// Weryfikacja że wszystkie są wybrane
cy.get('[data-cy="languages-multiselect"]')
.invoke('val')
.should('deep.equal', ['pl', 'en', 'de']);
// Dodanie kolejnej opcji
cy.get('[data-cy="languages-multiselect"]')
.select(['pl', 'en', 'de', 'fr']);
});
it('weryfikuje dostępne opcje w dropdown', () => {
cy.visit('/form');
// Sprawdzenie ilości opcji
cy.get('[data-cy="city-select"] option')
.should('have.length', 10);
// Sprawdzenie konkretnych opcji
cy.get('[data-cy="city-select"]')
.find('option')
.first()
.should('have.text', '-- Wybierz miasto --');
// Weryfikacja że opcja istnieje
cy.get('[data-cy="city-select"]')
.find('option')
.contains('Warszawa')
.should('exist');
});
// ========================================
// CHECKBOXY
// ========================================
it('zaznacza i odznacza checkboxy', () => {
cy.visit('/settings');
// Zaznaczenie checkboxa
cy.get('[data-cy="newsletter-checkbox"]').check();
// Weryfikacja że jest zaznaczony
cy.get('[data-cy="newsletter-checkbox"]')
.should('be.checked');
// Odznaczenie
cy.get('[data-cy="newsletter-checkbox"]').uncheck();
// Weryfikacja że nie jest zaznaczony
cy.get('[data-cy="newsletter-checkbox"]')
.should('not.be.checked');
});
it('obsługuje grupę checkboxów', () => {
cy.visit('/permissions');
// Zaznaczenie wszystkich checkboxów w grupie
cy.get('[data-cy="permissions"] input[type="checkbox"]')
.check();
// Weryfikacja że wszystkie są zaznaczone
cy.get('[data-cy="permissions"] input[type="checkbox"]')
.should('be.checked');
// Odznaczenie wszystkich
cy.get('[data-cy="permissions"] input[type="checkbox"]')
.uncheck();
// Zaznaczenie konkretnych checkboxów po wartości
cy.get('[data-cy="permissions"] input[type="checkbox"]')
.check(['read', 'write']); // value attributes
// Weryfikacja konkretnych stanów
cy.get('input[value="read"]').should('be.checked');
cy.get('input[value="write"]').should('be.checked');
cy.get('input[value="delete"]').should('not.be.checked');
});
it('obsługuje checkbox z labelką klikalną', () => {
cy.visit('/form');
// Kliknięcie w label również zaznacza checkbox
cy.get('label[for="terms-checkbox"]').click();
// Weryfikacja
cy.get('#terms-checkbox').should('be.checked');
// Lub bezpośrednio przez data-cy na checkboxie
cy.get('[data-cy="privacy-checkbox"]').check();
});
it('weryfikuje disabled state checkboxa', () => {
cy.visit('/form');
// Sprawdzenie że checkbox jest disabled
cy.get('[data-cy="admin-only-checkbox"]')
.should('be.disabled');
// Próba zaznaczenia disabled checkboxa zakończy się błędem
// cy.get('[data-cy="admin-only-checkbox"]').check(); // ❌ Rzuci błąd
// Force pominie sprawdzanie disabled (niezalecane)
cy.get('[data-cy="admin-only-checkbox"]')
.check({ force: true });
});
// ========================================
// RADIO BUTTONY
// ========================================
it('obsługuje radio buttony', () => {
cy.visit('/survey');
// Zaznaczenie radio buttona
cy.get('[data-cy="satisfaction-good"]').check();
// Weryfikacja że jest zaznaczony
cy.get('[data-cy="satisfaction-good"]')
.should('be.checked');
// Zaznaczenie innego radio z tej samej grupy
cy.get('[data-cy="satisfaction-excellent"]').check();
// Sprawdzenie że poprzedni został automatycznie odznaczony
cy.get('[data-cy="satisfaction-good"]')
.should('not.be.checked');
cy.get('[data-cy="satisfaction-excellent"]')
.should('be.checked');
});
it('wybiera radio button po wartości', () => {
cy.visit('/survey');
// Wybór radio po nazwie grupy i wartości
cy.get('input[name="payment-method"]')
.check('credit-card');
// Weryfikacja
cy.get('input[name="payment-method"]:checked')
.should('have.value', 'credit-card');
// Zmiana wyboru
cy.get('input[name="payment-method"]')
.check('paypal');
cy.get('input[name="payment-method"]:checked')
.should('have.value', 'paypal');
});
it('weryfikuje wszystkie opcje radio w grupie', () => {
cy.visit('/quiz');
// Sprawdzenie ilości opcji
cy.get('input[name="question-1"]')
.should('have.length', 4);
// Sprawdzenie że żadna nie jest zaznaczona początkowo
cy.get('input[name="question-1"]:checked')
.should('not.exist');
// Zaznaczenie odpowiedzi
cy.get('input[name="question-1"][value="B"]').check();
// Weryfikacja że tylko jedna jest zaznaczona
cy.get('input[name="question-1"]:checked')
.should('have.length', 1)
.and('have.value', 'B');
});
// ========================================
// CUSTOM DROPDOWN (niestandardowy)
// ========================================
it('obsługuje custom dropdown (nie natywny select)', () => {
cy.visit('/advanced-form');
// Kliknięcie w trigger custom dropdowna
cy.get('[data-cy="custom-select-trigger"]').click();
// Czekanie na pojawienie się listy opcji
cy.get('[data-cy="custom-select-options"]')
.should('be.visible');
// Kliknięcie w wybraną opcję
cy.get('[data-cy="custom-select-options"]')
.contains('Warszawa')
.click();
// Weryfikacja że dropdown się zamknął
cy.get('[data-cy="custom-select-options"]')
.should('not.be.visible');
// Weryfikacja wybranej wartości
cy.get('[data-cy="custom-select-trigger"]')
.should('contain', 'Warszawa');
});
it('obsługuje searchable custom dropdown', () => {
cy.visit('/advanced-form');
// Otwarcie dropdowna
cy.get('[data-cy="country-dropdown"]').click();
// Wpisanie w pole wyszukiwania
cy.get('[data-cy="country-search-input"]')
.type('Pol');
// Czekanie na przefiltrowane wyniki
cy.get('[data-cy="country-options"] li')
.should('have.length', 1)
.and('contain', 'Polska');
// Kliknięcie w wynik
cy.get('[data-cy="country-options"] li')
.first()
.click();
});
// ========================================
// CUSTOM CHECKBOX/TOGGLE
// ========================================
it('obsługuje custom checkbox (toggle switch)', () => {
cy.visit('/settings');
// Custom checkbox może wyglądać jak toggle switch
// Sprawdzenie stanu przed kliknięciem
cy.get('[data-cy="notifications-toggle"]')
.should('have.attr', 'aria-checked', 'false');
// Kliknięcie w toggle
cy.get('[data-cy="notifications-toggle"]').click();
// Weryfikacja zmiany stanu
cy.get('[data-cy="notifications-toggle"]')
.should('have.attr', 'aria-checked', 'true');
// Lub sprawdzenie klasy CSS
cy.get('[data-cy="notifications-toggle"]')
.should('have.class', 'toggle-on');
});
// ========================================
// AUTOCOMPLETE (COMBOBOX)
// ========================================
it('obsługuje autocomplete/combobox', () => {
cy.visit('/booking');
// Kliknięcie w pole autocomplete
cy.get('[data-cy="destination-autocomplete"]').click();
// Wpisanie tekstu
cy.get('[data-cy="destination-autocomplete"]')
.type('Warsz');
// Czekanie na sugestie
cy.get('[data-cy="autocomplete-suggestions"]')
.should('be.visible');
// Sprawdzenie że sugestie zawierają wpisany tekst
cy.get('[data-cy="autocomplete-suggestions"] li')
.should('have.length.greaterThan', 0)
.first()
.should('contain', 'Warszawa');
// Wybór sugestii
cy.get('[data-cy="autocomplete-suggestions"]')
.contains('Warszawa, Polska')
.click();
// Weryfikacja wybranej wartości
cy.get('[data-cy="destination-autocomplete"]')
.should('have.value', 'Warszawa, Polska');
});
// ========================================
// MULTI-SELECT Z TAGAMI
// ========================================
it('obsługuje multi-select z tagami', () => {
cy.visit('/project-settings');
// Kliknięcie w multi-select
cy.get('[data-cy="tags-multiselect"]').click();
// Wybór pierwszego taga
cy.get('[data-cy="tag-options"]')
.contains('JavaScript')
.click();
// Wybór drugiego taga (dropdown pozostaje otwarty)
cy.get('[data-cy="tag-options"]')
.contains('React')
.click();
// Weryfikacja wybranych tagów
cy.get('[data-cy="selected-tags"] .tag')
.should('have.length', 2);
cy.get('[data-cy="selected-tags"]')
.should('contain', 'JavaScript')
.and('contain', 'React');
// Usunięcie taga
cy.get('[data-cy="selected-tags"] .tag')
.contains('JavaScript')
.find('.remove-tag')
.click();
// Weryfikacja że został 1 tag
cy.get('[data-cy="selected-tags"] .tag')
.should('have.length', 1);
});
// ========================================
// WITHIN DLA KONTEKSTU
// ========================================
it('używa within() dla skomplikowanych struktur', () => {
cy.visit('/complex-form');
// Praca w kontekście konkretnego fieldset
cy.get('[data-cy="shipping-address"]').within(() => {
cy.get('[data-cy="country"]').select('Polska');
cy.get('[data-cy="city"]').select('Kraków');
cy.get('[data-cy="express-delivery"]').check();
});
// Praca w kontekście innego fieldset
cy.get('[data-cy="billing-address"]').within(() => {
cy.get('[data-cy="same-as-shipping"]').check();
});
});
});
Diagram:
graph TD
A[Elementy Formularzy] --> B[Natywne Elementy]
A --> C[Custom Elementy]
B --> B1[Select]
B --> B2[Checkbox]
B --> B3[Radio Button]
B1 --> B1A[.select text/value/index]
B1 --> B1B[Multiselect z tablicą]
B1 --> B1C[Weryfikacja have.value]
B2 --> B2A[.check - zaznacz]
B2 --> B2B[.uncheck - odznacz]
B2 --> B2C[Weryfikacja be.checked]
B2 --> B2D[Wiele: check array]
B3 --> B3A[.check tylko - no uncheck]
B3 --> B3B[Auto-odznaczanie innych]
B3 --> B3C[Po name i value]
C --> C1[Custom Dropdown]
C --> C2[Autocomplete]
C --> C3[Toggle Switch]
C --> C4[Multi-select Tags]
C1 --> C1A[.click trigger]
C1 --> C1B[.click opcja]
C1 --> C1C[Weryfikacja be.visible]
style A fill:#f9f,stroke:#333,stroke-width:4px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#fdb,stroke:#333,stroke-width:2px
Materiały:
Asercje i Oczekiwania
Jakie rodzaje asercji są dostępne w Cypress?
Odpowiedź w 30 sekund
Cypress oferuje dwa główne style asercji: BDD (Chai-BDD) z metodami should() i and() oraz TDD (Chai-Assert) z funkcją expect() i assert(). Najczęściej używa się składni BDD should(), która automatycznie korzysta z mechanizmu retry. Asercje mogą dotyczyć elementów DOM, obiektów JavaScript, odpowiedzi API oraz stanu aplikacji.
Odpowiedź w 2 minuty
Cypress integruje kilka bibliotek asercyjnych, oferując bogaty zestaw możliwości testowania. Podstawą są asercje Chai w dwóch stylach: BDD (Behavior-Driven Development) i TDD (Test-Driven Development). Styl BDD wykorzystuje metody should() i and() bezpośrednio na łańcuchu komend Cypress, co zapewnia automatyczny retry i lepszą czytelność testów. Dostępne są assertery jak be.visible, have.text, contain, have.class, have.attr i wiele innych.
Styl TDD używa funkcji expect() i assert(), które są przydatne w callbackach i przy testowaniu wartości JavaScript bezpośrednio. Te asercje nie mają wbudowanego retry, więc wykonują się natychmiast. Cypress dodaje również własne assertery specyficzne dla testowania webowego, takie jak be.checked, be.disabled, be.focused, have.value.
Dodatkowo Cypress oferuje asercje jQuery poprzez should('have.length'), should('exist') oraz asercje Sinon-Chai do testowania spyów i stubów (have.been.called, have.been.calledWith). Można również tworować własne assertery używając Chai.Assertion.addMethod().
Wybór odpowiedniego stylu asercji zależy od kontekstu: should() dla elementów DOM i komend Cypress (z retry), expect() dla logiki JavaScript w callbackach (bez retry), oraz assert() dla bardziej złożonych warunków logicznych.
Przykład kodu
describe('Rodzaje asercji w Cypress', () => {
it('BDD style - should() z automatycznym retry', () => {
cy.visit('https://example.com')
// Asercje Chai-BDD dla elementów DOM
cy.get('h1')
.should('be.visible')
.and('have.text', 'Witaj w aplikacji')
.and('have.class', 'header-title')
.and('have.css', 'color', 'rgb(0, 0, 0)')
// Asercje dla atrybutów i właściwości
cy.get('input[type="email"]')
.should('have.attr', 'placeholder', 'Wpisz email')
.and('have.value', '')
.and('be.enabled')
.and('not.be.disabled')
// Asercje dla list elementów
cy.get('.menu-item')
.should('have.length', 5)
.and('contain', 'Start')
})
it('TDD style - expect() bez retry', () => {
cy.visit('https://example.com')
// expect() w callbacku - dla wartości JavaScript
cy.get('button').then(($btn) => {
const text = $btn.text()
expect(text).to.equal('Kliknij mnie')
expect(text).to.have.length.greaterThan(5)
expect($btn).to.have.class('btn-primary')
})
// Asercje dla obiektów i tablic
cy.wrap({ name: 'Jan', age: 30 }).then((user) => {
expect(user).to.have.property('name', 'Jan')
expect(user).to.deep.equal({ name: 'Jan', age: 30 })
expect(user.age).to.be.a('number')
})
cy.wrap([1, 2, 3]).then((arr) => {
expect(arr).to.have.length(3)
expect(arr).to.include(2)
expect(arr).to.deep.equal([1, 2, 3])
})
})
it('Asercje dla stanu aplikacji', () => {
cy.visit('https://example.com/login')
// URL
cy.url()
.should('include', '/login')
.and('match', /\/login\?.*/)
// Cookies
cy.getCookie('session')
.should('exist')
.and('have.property', 'value')
.and('match', /^[a-z0-9]+$/)
// LocalStorage
cy.window().then((win) => {
expect(win.localStorage.getItem('theme')).to.equal('dark')
})
})
it('Asercje dla API', () => {
cy.request('GET', '/api/users/1')
.should((response) => {
expect(response.status).to.equal(200)
expect(response.body).to.have.property('id', 1)
expect(response.body.name).to.be.a('string')
expect(response.headers).to.have.property('content-type')
})
})
it('Asercje Sinon-Chai dla spyów', () => {
cy.visit('https://example.com')
// Spy na metodę window.alert
cy.window().then((win) => {
cy.spy(win, 'alert').as('alertSpy')
})
cy.get('#show-alert-btn').click()
cy.get('@alertSpy')
.should('have.been.calledOnce')
.and('have.been.calledWith', 'Ważna wiadomość!')
})
it('Własne assertery', () => {
// Dodanie własnego assertera
Chai.Assertion.addMethod('polishPhoneNumber', function() {
const obj = this._obj
const regex = /^\+48\d{9}$/
this.assert(
regex.test(obj),
'oczekiwano poprawnego numeru telefonu polskiego',
'nie oczekiwano poprawnego numeru telefonu polskiego',
'+48XXXXXXXXX',
obj
)
})
// Użycie własnego assertera
cy.wrap('+48123456789').should('be.polishPhoneNumber')
})
it('Asercje negatywne', () => {
cy.get('button')
.should('not.be.disabled')
.and('not.have.class', 'hidden')
.and('not.contain', 'Anuluj')
})
})
Diagram
graph TB
A[Asercje w Cypress] --> B[Chai-BDD Style]
A --> C[Chai-TDD Style]
A --> D[Własne Assertery]
B --> B1[should/and<br/>z retry]
B --> B2[be.visible]
B --> B3[have.text]
B --> B4[have.class]
B --> B5[have.attr]
C --> C1[expect<br/>bez retry]
C --> C2[assert<br/>bez retry]
C --> C3[Użycie w then]
B1 --> E[Elementy DOM]
B1 --> F[Komendy Cypress]
C1 --> G[Wartości JS]
C1 --> H[Obiekty]
C1 --> I[Tablice]
D --> D1[Chai.Assertion.addMethod]
D --> D2[Własna logika]
J[Biblioteki] --> K[Chai]
J --> L[jQuery]
J --> M[Sinon-Chai]
K --> B
K --> C
L --> B2
M --> N[have.been.called]
style A fill:#e1f5ff
style B1 fill:#c3f0c3
style C1 fill:#ffe6cc
Materiały
- Cypress Assertions Documentation - oficjalna dokumentacja wszystkich dostępnych asercji
- Chai Assertions Library - pełna lista asserterów BDD i TDD
- Cypress Custom Assertions Guide - jak tworzyć własne assertery
Jak działa mechanizm retry w Cypress?
Odpowiedź w 30 sekund
Mechanizm retry w Cypress automatycznie powtarza komendy i asercje przez określony czas (domyślnie 4 sekundy), czekając aż warunek zostanie spełniony. Dotyczy to komend query (np. cy.get(), cy.find()) połączonych z asercjami, ale nie dotyczy komend akcji (np. cy.click()). Dzięki retry testy są bardziej stabilne i nie wymagają ręcznego dodawania czekania.
Odpowiedź w 2 minuty
Mechanizm retry-ability jest jedną z kluczowych cech Cypress, która odróżnia go od tradycyjnych narzędzi testowych. Cypress automatycznie ponawia próby wykonania komend query i asercji, dopóki nie zakończą się sukcesem lub nie upłynie timeout (domyślnie 4000ms, konfigurowalny przez defaultCommandTimeout). Mechanizm ten rozwiązuje problem asynchroniczności aplikacji webowych bez potrzeby ręcznego dodawania waitów i sleep.
Retry działa na zasadzie inteligentnej pętli: Cypress najpierw wykonuje komendę query (np. cy.get('.button')), następnie sprawdza wszystkie powiązane asercje (np. .should('be.visible')). Jeśli którakolwiek asercja nie jest spełniona, Cypress cofa się do początku łańcucha query, czeka krótką chwilę i próbuje ponownie. Proces ten powtarza się aż do sukcesu lub timeoutu.
Ważne jest rozróżnienie między komendami, które mają retry a tymi, które go nie mają. Komendy query (get, find, contains, its) są retry-able i będą ponawiane razem z asercjami. Komendy akcji (click, type, select) wykonują się tylko raz i nie są ponawiane - jeśli akcja nie może być wykonana, test natychmiast failuje.
Mechanizm retry można kontrolować poprzez timeouty: globalnie w cypress.config.js, per-komenda przez parametr timeout, lub per-test przez Cypress.config(). Można również tworzyć własne warunki retry używając should() z funkcją callback, która zwraca asercje. To pozwala na tworzenie złożonych warunków oczekiwania bez sztywnych sleep-ów.
Przykład kodu
describe('Mechanizm retry w Cypress', () => {
beforeEach(() => {
cy.visit('https://example.com')
})
it('Automatyczny retry dla komend query + asercje', () => {
// Element pojawi się po 2 sekundach, ale test przejdzie
// bo Cypress będzie retry przez 4 sekundy (domyślny timeout)
cy.get('.delayed-element')
.should('be.visible')
.and('contain', 'Załadowano!')
// Cypress automatycznie czeka aż element spełni wszystkie warunki
cy.get('.loading-spinner').should('not.exist')
cy.get('.content').should('be.visible')
})
it('Łańcuch komend query - wszystkie są retry', () => {
// Cały łańcuch jest ponnawiany dopóki ostatnia asercja nie przejdzie
cy.get('.user-list') // Query 1 - retry
.find('.user-item') // Query 2 - retry
.first() // Query 3 - retry
.should('contain', 'Jan') // Asercja - trigger retry
// Jeśli asercja failuje, Cypress wraca do cy.get('.user-list')
})
it('Komendy akcji NIE mają retry', () => {
// Click wykonuje się tylko raz - jeśli button nie istnieje, test failuje
cy.get('.submit-btn').click() // Akcja - brak retry!
// Poprawne podejście - najpierw asercja (retry), potem akcja
cy.get('.submit-btn')
.should('be.visible') // Retry aż będzie widoczny
.and('be.enabled') // Retry aż będzie aktywny
.click() // Dopiero teraz klik (bez retry)
})
it('Konfiguracja timeoutów', () => {
// Timeout dla konkretnej komendy - 10 sekund
cy.get('.slow-loading-element', { timeout: 10000 })
.should('exist')
// Timeout dla asercji
cy.get('.dynamic-content')
.should('contain', 'Dane załadowane', { timeout: 8000 })
})
it('Własny warunek retry z callback', () => {
// Złożony warunek - czekaj aż lista ma 5 elementów
cy.get('.item-list').should(($list) => {
expect($list.find('.item')).to.have.length(5)
})
// Czekaj aż element ma konkretny tekst (ładowany asynchronicznie)
cy.get('.counter').should(($counter) => {
const count = parseInt($counter.text())
expect(count).to.be.greaterThan(0)
expect(count).to.be.lessThan(100)
})
})
it('Retry dla właściwości i atrybutów', () => {
// Czekaj aż atrybut zmieni wartość
cy.get('button')
.should('have.attr', 'data-status', 'ready')
// Czekaj aż klasa zostanie dodana
cy.get('.notification')
.should('have.class', 'success')
// Czekaj aż tekst się zmieni
cy.get('.message')
.should('have.text', 'Operacja zakończona')
})
it('Retry z alias i then()', () => {
// Alias nie powoduje retry!
cy.get('.user-name').as('userName')
// To NIE będzie retry - alias jest już zapisany
cy.get('@userName').should('contain', 'Jan') // ❌ Może failować
// Poprawne podejście - bezpośrednie query z asercją
cy.get('.user-name').should('contain', 'Jan') // ✅ Retry działa
})
it('Retry dla wartości zwracanych przez then()', () => {
// then() przerywa retry! Wartość jest "zablokowana"
cy.get('.price').then(($price) => {
const value = $price.text() // Wartość pobrana w tym momencie
expect(value).to.equal('100 zł') // Brak retry jeśli fail
})
// Lepsze podejście - invoke z retry
cy.get('.price')
.invoke('text')
.should('equal', '100 zł') // Retry działa!
})
it('Retry dla API responses', () => {
// Intercept i wait mają swoje timeouty
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/dashboard')
// Czekaj max 10 sekund na request
cy.wait('@getData', { timeout: 10000 })
.its('response.statusCode')
.should('equal', 200)
})
it('Kontrola retry dla złożonych scenariuszy', () => {
// Użyj should() z callback dla złożonej logiki
cy.get('.data-table').should(($table) => {
// Ta funkcja będzie wywoływana wielokrotnie (retry)
const rows = $table.find('tr')
// Wszystkie te warunki muszą być spełnione
expect(rows).to.have.length.greaterThan(0)
expect(rows.first()).to.contain('Nagłówek')
// Sprawdź zawartość pierwszego wiersza danych
const firstDataRow = rows.eq(1)
expect(firstDataRow.find('td')).to.have.length(3)
})
})
it('Wyłączenie retry - natychmiastowa asercja', () => {
cy.visit('/page')
// Użyj then() + expect() dla natychmiastowej asercji (bez retry)
cy.get('.counter').then(($el) => {
const count = parseInt($el.text())
expect(count).to.equal(0) // Failuje natychmiast jeśli nie 0
})
})
it('Debugowanie retry', () => {
// Dodaj log aby zobaczyć ile razy retry był wykonany
cy.get('.dynamic-element').should(($el) => {
console.log('Retry próba:', $el.text())
expect($el.text()).to.contain('Gotowe')
})
})
})
// Globalna konfiguracja timeoutów
// cypress.config.js
// {
// defaultCommandTimeout: 4000, // Domyślny timeout dla komend
// requestTimeout: 5000, // Timeout dla cy.request()
// responseTimeout: 30000, // Timeout dla odpowiedzi
// pageLoadTimeout: 60000, // Timeout dla ładowania strony
// execTimeout: 60000 // Timeout dla cy.exec()
// }
Diagram
sequenceDiagram
participant T as Test
participant C as Cypress
participant D as DOM
T->>C: cy.get('.button')
C->>D: Szukaj elementu
D-->>C: Element znaleziony
C->>C: Sprawdź asercje
Note over C: should('be.visible')
alt Element niewidoczny (retry)
C->>C: Czekaj 50ms
C->>D: Ponów query
D-->>C: Element znaleziony
C->>C: Sprawdź asercje
Note over C: Retry aż timeout<br/>lub sukces
else Element widoczny
C-->>T: Sukces ✓
end
alt Timeout przekroczony
C-->>T: Błąd ✗
Note over T: AssertionError:<br/>expected element<br/>to be visible
end
Note over C,D: Komendy Query:<br/>get, find, contains<br/>→ RETRY ✓
Note over C,D: Komendy Akcji:<br/>click, type, select<br/>→ BRAK RETRY ✗
Materiały
- Cypress Retry-ability Guide - oficjalny przewodnik po mechanizmie retry
- Understanding Cypress Retry Logic - szczegółowe wyjaśnienie jak działa retry
- Cypress Timeouts Configuration - konfiguracja wszystkich timeoutów
Czym jest implicit waiting w Cypress?
Odpowiedź w 30 sekund
Implicit waiting (czekanie niejawne) to wbudowany w Cypress mechanizm automatycznego oczekiwania na elementy i warunki, bez potrzeby ręcznego dodawania wait() lub sleep(). Cypress domyślnie czeka do 4 sekund na każdą komendę query i asercję, automatycznie ponnawiając próby aż warunek zostanie spełniony. Dzięki temu testy są bardziej niezawodne i nie zawierają sztywnych opóźnień.
Odpowiedź w 2 minuty
Implicit waiting to fundamentalna cecha architektury Cypress, która automatycznie czeka na elementy DOM, asercje i warunki aplikacji bez wymagania od programisty ręcznego dodawania czekania. W przeciwieństwie do tradycyjnych narzędzi jak Selenium, gdzie trzeba używać explicit wait lub implicit wait z konkretnymi wartościami czasowymi, Cypress inteligentnie zarządza oczekiwaniem automatycznie.
Mechanizm działa na kilku poziomach. Po pierwsze, wszystkie komendy query (jak cy.get(), cy.find(), cy.contains()) automatycznie czekają na pojawienie się elementu w DOM przez czas określony w defaultCommandTimeout (domyślnie 4000ms). Po drugie, asercje są automatycznie ponawiane dopóki nie zostaną spełnione lub nie upłynie timeout. Po trzecie, komendy akcji czekają na możliwość interakcji z elementem (czy jest widoczny, czy nie jest zasłonięty, czy nie jest disabled).
Cypress automatycznie czeka również na zakończenie animacji CSS przed wykonaniem akcji, na routing przejścia między stronami, na zakończenie requestów sieciowych (gdy używamy cy.wait() z aliasem), oraz na stabilizację DOM po zmianach. To wszystko dzieje się "za kulisami" bez dodatkowego kodu.
Implicit waiting eliminuje problemy typowe dla testów E2E: flaky testy wynikające z race conditions, niepotrzebne sleep() które spowalniają testy, oraz trudne do debugowania timeouty. Programista może skupić się na logice testu, a Cypress zadba o synchronizację z aplikacją. W rzadkich przypadkach, gdy potrzebujemy bardziej precyzyjnej kontroli, możemy użyć explicit waiting poprzez cy.wait(alias) lub własne funkcje retry w should().
Przykład kodu
describe('Implicit waiting w Cypress', () => {
it('Automatyczne czekanie na elementy DOM', () => {
cy.visit('https://example.com')
// Cypress czeka do 4 sekund aż element się pojawi
// Nie trzeba dodawać cy.wait() ani sleep()
cy.get('.lazy-loaded-content')
.should('be.visible')
// Nawet jeśli element pojawi się po 3 sekundach, test przejdzie
cy.get('.delayed-button').click()
})
it('Czekanie na warunki asercji', () => {
cy.visit('https://example.com/counter')
cy.get('#start-btn').click()
// Cypress automatycznie czeka aż licznik osiągnie 10
// Ponawia sprawdzenie co ~50ms przez 4 sekundy
cy.get('.counter-value')
.should('have.text', '10')
// Bez implicit waiting musielibyśmy napisać:
// cy.wait(5000) // ❌ Złe - sztywne czekanie
// cy.get('.counter-value').should('have.text', '10')
})
it('Czekanie na widoczność i interakcję', () => {
cy.visit('https://example.com')
// Cypress automatycznie czeka aż:
// 1. Element istnieje w DOM
// 2. Element jest widoczny
// 3. Element nie jest disabled
// 4. Element nie jest zasłonięty przez inny element
// 5. Animacje CSS się zakończą
cy.get('button.submit')
.click() // Click wykona się tylko gdy wszystkie warunki spełnione
})
it('Czekanie na zmiany stanu', () => {
cy.visit('https://example.com/form')
cy.get('input[name="email"]').type('test@example.com')
cy.get('form').submit()
// Automatyczne czekanie na pojawienie się komunikatu
cy.get('.success-message')
.should('be.visible')
.and('contain', 'Email wysłany')
// Czekanie aż klasa zostanie dodana
cy.get('.form-container')
.should('have.class', 'submitted')
})
it('Czekanie na zakończenie animacji', () => {
cy.visit('https://example.com')
cy.get('.menu-toggle').click()
// Cypress czeka aż animacja slidowania menu się zakończy
// przed wykonaniem click na elemencie wewnątrz
cy.get('.menu-item')
.contains('Profil')
.click() // Wykona się po zakończeniu animacji
})
it('Czekanie na routing i nawigację', () => {
cy.visit('https://example.com')
cy.get('a[href="/dashboard"]').click()
// Cypress automatycznie czeka na:
// - Załadowanie nowej strony
// - Event 'load'
// - Stabilizację DOM
cy.url().should('include', '/dashboard')
cy.get('.dashboard-header').should('be.visible')
})
it('Czekanie na requesty sieciowe', () => {
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('https://example.com/users')
// Explicit wait dla konkretnego requestu
cy.wait('@getUsers')
.its('response.statusCode')
.should('equal', 200)
// Teraz implicit wait dla elementów UI
cy.get('.user-list .user-item')
.should('have.length.greaterThan', 0)
})
it('Czekanie na wielokrotne zmiany', () => {
cy.visit('https://example.com/live-data')
// Cypress będzie sprawdzać warunek co ~50ms
// aż zostanie spełniony lub timeout (4s)
cy.get('.status-indicator')
.should(($el) => {
const status = $el.text()
expect(status).to.be.oneOf(['Ready', 'Active', 'Complete'])
expect($el).to.have.class('status-ok')
})
})
it('Dostosowanie timeoutu dla wolniejszych operacji', () => {
cy.visit('https://example.com')
// Zwiększenie timeoutu dla konkretnej operacji
cy.get('.slow-loading-chart', { timeout: 10000 })
.should('be.visible')
// Lub dla asercji
cy.get('.data-table')
.should('contain', 'Dane załadowane', { timeout: 15000 })
})
it('Implicit wait vs explicit wait', () => {
cy.visit('https://example.com')
// ❌ ZŁE - sztywne czekanie (explicit)
cy.wait(3000) // Zawsze czeka 3s, nawet jeśli element jest już gotowy
cy.get('.content').should('be.visible')
// ✅ DOBRE - implicit waiting
cy.get('.content').should('be.visible') // Czeka tylko tyle ile potrzeba
// ✅ DOBRE - explicit wait na konkretny request
cy.intercept('GET', '/api/data').as('getData')
cy.wait('@getData') // Czeka konkretnie na ten request
})
it('Czekanie na localStorage i cookies', () => {
cy.visit('https://example.com/login')
cy.get('input[name="username"]').type('user')
cy.get('input[name="password"]').type('pass')
cy.get('button[type="submit"]').click()
// Implicit wait dla cookie
cy.getCookie('session')
.should('exist')
.and('have.property', 'value')
// Wait dla localStorage (przez własną komendę)
cy.window().its('localStorage.token')
.should('exist')
})
it('Czekanie w łańcuchu komend', () => {
cy.visit('https://example.com/products')
// Każdy krok ma implicit waiting
cy.get('.product-list') // Czeka na listę
.find('.product-item') // Czeka na elementy
.first() // Czeka aż first() jest dostępny
.find('.add-to-cart') // Czeka na przycisk
.should('be.visible') // Czeka aż widoczny
.and('not.be.disabled') // Czeka aż aktywny
.click() // Wykonuje click
cy.get('.cart-badge')
.should('contain', '1') // Czeka aż licznik się zaktualizuje
})
it('Czekanie z warunkami alternatywnymi', () => {
cy.visit('https://example.com')
// Czekaj na jeden z możliwych stanów
cy.get('.notification').should(($notification) => {
const text = $notification.text()
// Akceptujemy kilka możliwych stanów
expect(text).to.match(/Sukces|Ostrzeżenie|Info/)
})
})
it('Brak czekania dla then() - snapshot wartości', () => {
cy.visit('https://example.com')
// then() przerywa retry - wartość jest "zamrożona"
cy.get('.price').then(($price) => {
const value = parseFloat($price.text())
// Ten expect NIE ma implicit waiting!
expect(value).to.be.greaterThan(0) // Failuje natychmiast jeśli false
})
// Aby zachować implicit waiting, użyj invoke() + should()
cy.get('.price')
.invoke('text')
.then(parseFloat)
.should('be.greaterThan', 0) // MA implicit waiting
})
})
// Przykład konfiguracji globalnych timeoutów
// cypress.config.js
/*
export default {
e2e: {
defaultCommandTimeout: 4000, // Domyślne czekanie dla komend query
requestTimeout: 5000, // Timeout dla cy.request()
responseTimeout: 30000, // Timeout dla odpowiedzi sieciowych
pageLoadTimeout: 60000, // Timeout dla cy.visit()
execTimeout: 60000, // Timeout dla cy.exec()
}
}
*/
// Własna komenda z implicit waiting
Cypress.Commands.add('waitForText', (selector, text, timeout = 4000) => {
// Ta komenda używa wbudowanego implicit waiting
cy.get(selector, { timeout }).should('contain', text)
})
// Użycie
// cy.waitForText('.message', 'Załadowano', 8000)
Diagram
graph TB
subgraph "Cypress Implicit Waiting"
A[Wykonaj komendę] --> B{Element<br/>istnieje?}
B -->|NIE| C[Czekaj 50ms]
C --> B
B -->|TAK| D{Asercje<br/>spełnione?}
D -->|NIE| E{Timeout<br/>przekroczony?}
E -->|NIE| C
E -->|TAK| F[BŁĄD ✗]
D -->|TAK| G[SUKCES ✓]
end
subgraph "Tradycyjne narzędzia (np. Selenium)"
H[Wykonaj komendę] --> I{Element<br/>istnieje?}
I -->|NIE| J[BŁĄD ✗]
I -->|TAK| K[SUKCES ✓]
L[Sleep 3000ms] --> M[Wykonaj komendę]
M --> N[...]
end
subgraph "Automatyczne czekanie na"
O[Elementy DOM]
P[Warunki asercji]
Q[Widoczność]
R[Zakończenie animacji]
S[Brak zasłonięcia]
T[Stan enabled]
end
style G fill:#90EE90
style F fill:#FFB6C6
style A fill:#E1F5FF
style H fill:#FFE6CC
Materiały
- Cypress Retry-ability and Waiting - oficjalny przewodnik po czekaniu i retry
- Introduction to Cypress - Automatic Waiting - wprowadzenie do implicit waiting
- Cypress vs Selenium: Waiting - porównanie czekania w Cypress i Selenium
Jak używać should() vs expect() w asercjach?
Odpowiedź w 30 sekund
should() to metoda łańcuchowa Cypress z automatycznym retry, używana głównie dla elementów DOM i komend Cypress. expect() to funkcja Chai bez retry, używana w callbackach then() dla wartości JavaScript. Używaj should() domyślnie dla testowania UI (retry działa), a expect() wewnątrz then() dla asercji na konkretnych wartościach (bez retry, natychmiastowe sprawdzenie).
Odpowiedź w 2 minuty
Główna różnica między should() a expect() leży w mechanizmie retry i sposobie użycia. should() jest metodą Cypress, która działa na łańcuchu komend i automatycznie korzysta z mechanizmu retry-ability. Oznacza to, że asercje z should() są sprawdzane wielokrotnie przez określony czas (domyślnie 4 sekundy), co czyni je idealnymi do testowania dynamicznych elementów DOM i stanu aplikacji. Składnia to cy.get('.element').should('be.visible').
expect() natomiast to funkcja z biblioteki Chai, która wykonuje asercję natychmiast, bez retry. Jest używana głównie w callbackach funkcji then(), gdzie pracujemy z "zamrożonymi" wartościami JavaScript. Gdy użyjemy then(), przerywamy łańcuch retry Cypress i otrzymujemy snapshot wartości w danym momencie. expect() jest wtedy naturalnym wyborem do weryfikacji tej wartości. Składnia to expect(value).to.equal(10).
Wybór między nimi zależy od kontekstu: dla elementów DOM i komend Cypress używaj should() z retry, dla logiki JavaScript w callbackach używaj expect() bez retry. Można również używać should() z funkcją callback, która zawiera expect() - wtedy callback jest wywoływany wielokrotnie (retry), ale wewnętrzne expect() failuje natychmiast jeśli warunek nie jest spełniony w danej iteracji.
Cypress oferuje też alias and() dla should(), który poprawia czytelność przy łańcuchu wielu asercji. Możliwe jest również mieszanie stylów: should() dla warunków DOM, a expect() dla dodatkowej logiki wewnątrz callback. Kluczowa zasada: jeśli potrzebujesz retry (czekanie na zmianę stanu), użyj should(). Jeśli testujesz wartość która już jest znana i nie zmieni się, użyj expect().
Przykład kodu
describe('should() vs expect() w Cypress', () => {
beforeEach(() => {
cy.visit('https://example.com')
})
it('should() - z automatycznym retry', () => {
// should() dla elementów DOM - ma retry
cy.get('.notification')
.should('be.visible') // Czeka aż będzie widoczny
.and('have.class', 'success') // and() to alias dla should()
.and('contain', 'Zapisano') // Czeka aż tekst się pojawi
// should() dla właściwości elementu - ma retry
cy.get('input#email')
.should('have.value', '') // Czeka aż value będzie puste
.and('be.enabled') // Czeka aż będzie aktywny
.and('have.attr', 'type', 'email') // Sprawdza atrybut
// should() dla URL - ma retry
cy.url()
.should('include', '/dashboard') // Czeka na zmianę URL
.and('match', /\/dashboard\?.*/) // Walidacja regex
})
it('expect() - bez retry, natychmiastowa asercja', () => {
// expect() w callback then() - NIE ma retry
cy.get('.price').then(($price) => {
const value = parseFloat($price.text())
expect(value).to.be.a('number')
expect(value).to.be.greaterThan(0)
expect(value).to.be.lessThan(1000)
})
// expect() dla obiektów JavaScript
cy.window().then((win) => {
expect(win.localStorage.getItem('theme')).to.equal('dark')
expect(win.location.pathname).to.include('/home')
})
// expect() dla tablic i obiektów
cy.wrap({ name: 'Jan', age: 30 }).then((user) => {
expect(user).to.have.property('name')
expect(user.age).to.equal(30)
expect(user).to.deep.equal({ name: 'Jan', age: 30 })
})
})
it('Kiedy używać should() - elementy DOM i retry', () => {
// ✅ DOBRE - should() dla elementów które mogą się zmienić
cy.get('.loading-spinner')
.should('not.exist') // Czeka aż zniknie
cy.get('.data-loaded')
.should('be.visible') // Czeka aż się pojawi
cy.get('.counter')
.should('have.text', '5') // Czeka aż licznik osiągnie 5
// ✅ DOBRE - should() dla komendy Cypress
cy.getCookie('session')
.should('exist')
.and('have.property', 'value')
})
it('Kiedy używać expect() - wartości JavaScript bez retry', () => {
// ✅ DOBRE - expect() dla przetworzonych wartości
cy.get('.items').then(($items) => {
const count = $items.length
const texts = $items.map((i, el) => el.textContent).get()
expect(count).to.equal(3)
expect(texts).to.include('Item 1')
expect(texts).to.deep.equal(['Item 1', 'Item 2', 'Item 3'])
})
// ✅ DOBRE - expect() dla obliczeń
cy.get('.price-total').then(($total) => {
const total = parseFloat($total.text())
const expectedTotal = 100 * 1.23 // VAT 23%
expect(total).to.be.closeTo(expectedTotal, 0.01)
})
})
it('should() z callback - łączenie retry + expect()', () => {
// should() z funkcją callback - callback jest retry-owany
cy.get('.status-list').should(($list) => {
// Ten blok wykonuje się wielokrotnie (retry)
const items = $list.find('.status-item')
// expect() wewnątrz jest sprawdzany przy każdej próbie
expect(items).to.have.length(3)
// Możemy robić złożone sprawdzenia
const statuses = items.map((i, el) => el.textContent).get()
expect(statuses).to.include('Active')
})
})
it('Porównanie: ta sama asercja w obu stylach', () => {
// Styl 1: should() - z retry
cy.get('.message')
.should('contain', 'Sukces') // Czeka aż tekst się pojawi
// Styl 2: expect() w then() - bez retry
cy.get('.message').then(($msg) => {
expect($msg.text()).to.include('Sukces') // Sprawdza natychmiast
})
// Styl 3: should() z callback + expect() - retry
cy.get('.message').should(($msg) => {
expect($msg.text()).to.include('Sukces') // Callback retry-owany
})
})
it('Łączenie should() i expect()', () => {
cy.get('form')
.should('be.visible') // should() dla DOM
.find('input')
.should('have.length', 3) // should() dla liczby elementów
.then(($inputs) => {
// Przełączenie na expect() dla logiki JS
const values = $inputs.map((i, el) => el.value).get()
expect(values).to.deep.equal(['', '', ''])
expect(values.every(v => v.length === 0)).to.be.true
})
})
it('should() dla asynchronicznych zmian', () => {
cy.get('#load-data-btn').click()
// should() czeka na asynchroniczną zmianę
cy.get('.data-container')
.should('not.have.class', 'loading') // Czeka aż loading zniknie
.and('have.class', 'loaded') // Czeka aż loaded się pojawi
.find('.data-row')
.should('have.length.greaterThan', 0) // Czeka aż dane się załadują
})
it('expect() dla wartości statycznych', () => {
// Dla wartości które już znamy i się nie zmienią
const config = { apiUrl: 'https://api.example.com', timeout: 5000 }
cy.wrap(config).then((cfg) => {
expect(cfg.apiUrl).to.be.a('string')
expect(cfg.timeout).to.equal(5000)
expect(cfg).to.have.all.keys('apiUrl', 'timeout')
})
})
it('Błędy przy niepoprawnym użyciu', () => {
// ❌ ZŁE - expect() bez then() nie ma sensu
// cy.get('.element').expect('be.visible') // Syntax error!
// ❌ ZŁE - should() w then() - traci retry
cy.get('.element').then(($el) => {
// To nie zadziała - should() potrzebuje być w łańcuchu Cypress
// $el.should('be.visible') // Błąd!
// Użyj expect() zamiast tego
expect($el).to.be.visible
})
// ✅ DOBRE - should() poza then()
cy.get('.element').should('be.visible')
// ✅ DOBRE - expect() wewnątrz then()
cy.get('.element').then(($el) => {
expect($el).to.be.visible
})
})
it('Negacja w obu stylach', () => {
// should() z negacją - retry działa
cy.get('.error-message')
.should('not.exist')
.should('not.be.visible')
.should('not.have.class', 'active')
// expect() z negacją - natychmiast
cy.get('.button').then(($btn) => {
expect($btn).to.not.have.class('disabled')
expect($btn.text()).to.not.equal('')
})
})
it('Własne komunikaty błędów', () => {
// should() - komunikat w trzecim parametrze (rzadko używane)
cy.get('.element').should('be.visible') // Domyślny komunikat
// expect() - własny komunikat w drugim parametrze
cy.get('.price').then(($price) => {
const value = parseFloat($price.text())
expect(value, 'Cena powinna być dodatnia').to.be.greaterThan(0)
expect(value, 'Cena nie może przekraczać 1000').to.be.lessThan(1000)
})
})
it('Multiple asercje - różne style', () => {
// Styl 1: Łańcuch should()
cy.get('.user-card')
.should('be.visible')
.and('have.class', 'active')
.and('contain', 'Jan Kowalski')
// Styl 2: should() z callback - wszystko w jednym
cy.get('.user-card').should(($card) => {
expect($card).to.be.visible
expect($card).to.have.class('active')
expect($card.text()).to.include('Jan Kowalski')
})
// Styl 3: Osobne should() (każde retry osobno)
cy.get('.user-card').should('be.visible')
cy.get('.user-card').should('have.class', 'active')
cy.get('.user-card').should('contain', 'Jan Kowalski')
})
it('Testowanie API responses', () => {
cy.request('/api/users/1').then((response) => {
// expect() dla struktury response
expect(response.status).to.equal(200)
expect(response.body).to.have.property('id', 1)
expect(response.body.name).to.be.a('string')
expect(response.headers['content-type']).to.include('application/json')
})
// Alternatywnie z should()
cy.request('/api/users/1')
.its('status').should('equal', 200)
.its('body').should('have.property', 'id', 1)
})
})
// Podsumowanie: Kiedy czego używać
/*
┌─────────────────────────────────────────────────────────────┐
│ UŻYJ should() │
├─────────────────────────────────────────────────────────────┤
│ • Elementy DOM (cy.get, cy.find) │
│ • Komendy Cypress (cy.url, cy.getCookie) │
│ • Kiedy potrzebujesz retry (czekanie na zmianę) │
│ • Asynchroniczne operacje │
│ • Gdy warunek może się zmienić w czasie │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ UŻYJ expect() │
├─────────────────────────────────────────────────────────────┤
│ • Wewnątrz then() dla wartości JavaScript │
│ • Dla "zamrożonych" wartości (snapshot) │
│ • Obliczenia i transformacje danych │
│ • Testowanie struktur (obiekty, tablice) │
│ • Gdy wartość jest statyczna i znana │
└─────────────────────────────────────────────────────────────┘
*/
Diagram
graph TB
A[Potrzebujesz asercji] --> B{Element DOM<br/>czy wartość JS?}
B -->|Element DOM| C[Użyj should]
B -->|Wartość JS| D{W callback<br/>then?}
D -->|TAK| E[Użyj expect]
D -->|NIE| F[Użyj should + wrap]
C --> G{Potrzebujesz<br/>retry?}
G -->|TAK| H[cy.get.should<br/>✓ Retry]
G -->|NIE| I[cy.get.then + expect<br/>✗ Bez retry]
E --> J[then $el =><br/>expect $el]
subgraph "should() - Retry ✓"
H --> K[Próba 1]
K -->|Fail| L[Czekaj 50ms]
L --> M[Próba 2]
M -->|Fail| L
M -->|Pass| N[Sukces]
end
subgraph "expect() - Natychmiast ✗"
J --> O[Sprawdź raz]
O -->|Pass| P[Sukces]
O -->|Fail| Q[Błąd]
end
R[Przykłady should] --> R1[.should 'be.visible']
R --> R2[.should 'have.text']
R --> R3[.should callback]
S[Przykłady expect] --> S1[expect x .to.equal]
S --> S2[expect arr .to.include]
S --> S3[expect obj .to.have.property]
style H fill:#90EE90
style J fill:#FFE6CC
style N fill:#90EE90
style Q fill:#FFB6C6
Materiały
- Cypress Assertions Guide - pełna dokumentacja wszystkich asserterów
- should() vs expect() Best Practices - oficjalne wytyczne
- Chai Assertion Library - dokumentacja biblioteki Chai używanej przez Cypress
Jak sprawdzić widoczność, tekst i atrybuty elementu?
Odpowiedź w 30 sekund
W Cypress sprawdzamy widoczność przez should('be.visible') lub should('not.be.visible'), tekst przez should('have.text', 'exact') lub should('contain', 'partial'), a atrybuty przez should('have.attr', 'name', 'value') lub invoke('attr', 'name'). Wszystkie te asercje korzystają z automatycznego retry, więc czekają na pojawienie się oczekiwanego stanu.
Odpowiedź w 2 minuty
Cypress oferuje bogaty zestaw asserterów do testowania różnych aspektów elementów DOM. Widoczność sprawdzamy głównie przez be.visible, który weryfikuje czy element istnieje w DOM, ma niezerowe wymiary, nie ma display: none ani visibility: hidden, i nie jest zasłonięty przez inny element. Dla ukrytych elementów używamy not.be.visible lub not.exist.
Tekst elementu można sprawdzać na kilka sposobów w zależności od potrzeb. have.text wymaga dokładnego dopasowania całego tekstu (łącznie z białymi znakami), contain sprawdza czy tekst zawiera podany fragment (częściowe dopasowanie), a have.html weryfikuje zawartość HTML. Można też używać wyrażeń regularnych przez match. Metoda invoke('text') pozwala na pobranie tekstu i dalsze operacje.
Atrybuty sprawdzamy przez have.attr podając nazwę atrybutu i opcjonalnie oczekiwaną wartość, lub przez invoke('attr', 'name') aby pobrać wartość do dalszej analizy. Dla klas CSS używamy dedykowanego have.class, dla wartości inputów have.value, a dla właściwości have.prop. Ważne jest rozróżnienie między atrybutami HTML (attr) a właściwościami DOM (prop).
Wszystkie te asercje działają z mechanizmem retry Cypress, automatycznie czekając aż warunek zostanie spełniony. Można je łączyć w łańcuchy używając and(), sprawdzać wiele warunków jednocześnie w callback, oraz negować przez not. Dla bardziej złożonych scenariuszy można używać should() z funkcją callback, która pozwala na dowolną logikę walidacji.
Przykład kodu
describe('Sprawdzanie widoczności, tekstu i atrybutów', () => {
beforeEach(() => {
cy.visit('https://example.com')
})
describe('Widoczność elementów', () => {
it('Sprawdzanie czy element jest widoczny', () => {
// Podstawowe sprawdzenie widoczności
cy.get('.header').should('be.visible')
// Element istnieje w DOM i jest widoczny
cy.get('.notification')
.should('exist')
.and('be.visible')
// Negacja - element ukryty
cy.get('.hidden-content').should('not.be.visible')
// Element w ogóle nie istnieje w DOM
cy.get('.non-existent').should('not.exist')
})
it('Różne stany widoczności', () => {
// Element istnieje ale jest ukryty przez CSS
cy.get('.display-none').should('exist').and('not.be.visible')
// Element poza ekranem (nadal widoczny dla Cypress)
cy.get('.offscreen').should('be.visible')
// Element z opacity: 0 (nadal widoczny)
cy.get('.transparent').should('be.visible')
// Element z zerową wysokością/szerokością
cy.get('.zero-size').should('not.be.visible')
})
it('Czekanie na pojawienie się elementu', () => {
cy.get('#load-btn').click()
// Cypress czeka aż element stanie się widoczny
cy.get('.loaded-content')
.should('be.visible', { timeout: 10000 })
// Czekanie aż loader zniknie
cy.get('.loading-spinner').should('not.exist')
})
it('Sprawdzanie widoczności wielu elementów', () => {
cy.get('.menu-item')
.should('have.length', 5)
.and('be.visible') // Wszystkie są widoczne
// Sprawdzenie konkretnego elementu z listy
cy.get('.menu-item').eq(2).should('be.visible')
// Sprawdzenie pierwszego i ostatniego
cy.get('.menu-item').first().should('be.visible')
cy.get('.menu-item').last().should('be.visible')
})
})
describe('Sprawdzanie tekstu', () => {
it('Dokładne dopasowanie tekstu', () => {
// have.text - dokładne dopasowanie (bez białych znaków)
cy.get('h1').should('have.text', 'Witaj w aplikacji')
// Tekst z wieloma spacjami/enterami jest normalizowany
cy.get('.title').should('have.text', 'Tytuł strony')
})
it('Częściowe dopasowanie tekstu', () => {
// contain - zawiera fragment tekstu
cy.get('.description')
.should('contain', 'Lorem ipsum')
.and('contain', 'dolor sit amet')
// Wielkość liter ma znaczenie
cy.get('.info').should('contain', 'Info')
})
it('Tekst z wyrażeniami regularnymi', () => {
// match - wyrażenie regularne
cy.get('.email')
.invoke('text')
.should('match', /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i)
// Walidacja formatu numeru telefonu
cy.get('.phone')
.invoke('text')
.should('match', /^\+48\s?\d{3}\s?\d{3}\s?\d{3}$/)
})
it('Pobieranie i analiza tekstu', () => {
// invoke('text') - pobiera tekst do dalszej analizy
cy.get('.price')
.invoke('text')
.then((text) => {
const price = parseFloat(text.replace('zł', '').trim())
expect(price).to.be.greaterThan(0)
expect(price).to.be.lessThan(1000)
})
})
it('Sprawdzanie tekstu z białymi znakami', () => {
// Tekst dokładnie taki jak w HTML (z białymi znakami)
cy.get('.formatted')
.invoke('text')
.should('equal', ' Tekst z spacjami ')
// Tekst po trim()
cy.get('.formatted')
.invoke('text')
.invoke('trim')
.should('equal', 'Tekst z spacjami')
})
it('Sprawdzanie pustego tekstu', () => {
cy.get('.empty-div').should('have.text', '')
cy.get('.empty-div').should('be.empty')
// Input bez wartości
cy.get('input#name').should('have.value', '')
})
it('Tekst wieloliniowy', () => {
cy.get('.multiline')
.should('contain', 'Linia 1')
.and('contain', 'Linia 2')
.and('contain', 'Linia 3')
})
it('Sprawdzanie HTML content', () => {
// have.html - zawartość HTML
cy.get('.rich-content')
.should('have.html', '<strong>Ważne</strong> informacje')
// Zawiera tag HTML
cy.get('.formatted-text').should('contain.html', '<em>')
})
it('Tekst elementów z listy', () => {
// Sprawdzenie tekstu każdego elementu
cy.get('.item').each(($item, index) => {
expect($item.text()).to.include('Item')
})
// Konkretny tekst w konkretnym elemencie
cy.get('.item').eq(0).should('have.text', 'Item 1')
cy.get('.item').eq(1).should('have.text', 'Item 2')
})
})
describe('Sprawdzanie atrybutów', () => {
it('Podstawowe sprawdzanie atrybutów', () => {
// have.attr - sprawdzenie wartości atrybutu
cy.get('a.link')
.should('have.attr', 'href', '/page')
.and('have.attr', 'target', '_blank')
// Sprawdzenie czy atrybut istnieje (bez wartości)
cy.get('input').should('have.attr', 'required')
// Sprawdzenie że atrybut nie istnieje
cy.get('button').should('not.have.attr', 'disabled')
})
it('Pobieranie wartości atrybutu', () => {
// invoke('attr') - pobiera wartość atrybutu
cy.get('img')
.invoke('attr', 'src')
.should('include', '/images/')
.and('match', /\.(jpg|png|gif)$/)
// Analiza wartości atrybutu
cy.get('a').invoke('attr', 'href').then((href) => {
expect(href).to.be.a('string')
expect(href.startsWith('http')).to.be.true
})
})
it('Atrybuty data-*', () => {
// Sprawdzanie custom data attributes
cy.get('.product')
.should('have.attr', 'data-id', '123')
.and('have.attr', 'data-category', 'electronics')
.and('have.attr', 'data-price', '99.99')
// Pobieranie i parsowanie data-*
cy.get('.product')
.invoke('attr', 'data-price')
.then(parseFloat)
.should('be.greaterThan', 50)
})
it('Sprawdzanie klas CSS', () => {
// have.class - sprawdzenie pojedynczej klasy
cy.get('.button')
.should('have.class', 'btn')
.and('have.class', 'btn-primary')
.and('have.class', 'active')
// Negacja
cy.get('.button').should('not.have.class', 'disabled')
// Sprawdzenie czy ma którykolwiek z klas
cy.get('.alert').should(($el) => {
const classes = $el.attr('class')
expect(classes).to.match(/success|warning|error/)
})
})
it('Sprawdzanie wartości inputów', () => {
// have.value - dla input, select, textarea
cy.get('input#email')
.should('have.value', '')
cy.get('input#email').type('test@example.com')
cy.get('input#email')
.should('have.value', 'test@example.com')
// Checkbox
cy.get('input[type="checkbox"]')
.should('not.be.checked')
.check()
.should('be.checked')
})
it('Sprawdzanie właściwości vs atrybutów', () => {
// Atrybut HTML (początkowa wartość)
cy.get('input#name')
.should('have.attr', 'value', 'Jan')
// Właściwość DOM (aktualna wartość)
cy.get('input#name')
.should('have.prop', 'value', 'Jan')
// Po wpisaniu tekstu
cy.get('input#name').clear().type('Anna')
// Atrybut się nie zmienił
cy.get('input#name').should('have.attr', 'value', 'Jan')
// Ale właściwość tak
cy.get('input#name').should('have.prop', 'value', 'Anna')
})
it('Sprawdzanie wielu atrybutów jednocześnie', () => {
cy.get('button#submit').should(($btn) => {
expect($btn).to.have.attr('type', 'submit')
expect($btn).to.have.attr('class')
expect($btn).to.have.class('btn-primary')
expect($btn.attr('disabled')).to.be.undefined
})
})
it('Atrybuty boolean', () => {
// Atrybuty boolean (disabled, required, readonly)
cy.get('input#required')
.should('have.attr', 'required')
cy.get('button').should('not.have.attr', 'disabled')
// Sprawdzenie przez właściwość (zwraca boolean)
cy.get('input#required')
.should('have.prop', 'required', true)
})
it('Style CSS', () => {
// have.css - sprawdzenie computed style
cy.get('.header')
.should('have.css', 'background-color', 'rgb(255, 0, 0)')
.and('have.css', 'font-size', '24px')
.and('have.css', 'display', 'flex')
// Pobieranie wartości CSS
cy.get('.element')
.invoke('css', 'color')
.should('equal', 'rgb(0, 0, 0)')
})
})
describe('Kombinacje sprawdzeń', () => {
it('Widoczność + tekst + atrybuty razem', () => {
cy.get('.alert')
.should('be.visible') // Widoczność
.and('have.class', 'alert-success') // Klasa
.and('contain', 'Operacja zakończona') // Tekst
.and('have.attr', 'role', 'alert') // Atrybut
.and('have.css', 'background-color') // Style
})
it('Callback z wieloma sprawdzeniami', () => {
cy.get('.user-card').should(($card) => {
// Widoczność
expect($card).to.be.visible
// Tekst
expect($card.text()).to.include('Jan Kowalski')
// Atrybuty
expect($card).to.have.attr('data-user-id')
expect($card).to.have.class('active')
// Dzieci
expect($card.find('.avatar')).to.exist
expect($card.find('.name')).to.have.text('Jan Kowalski')
})
})
it('Dynamiczne czekanie na zmiany', () => {
cy.get('#start-process').click()
// Czekaj aż status się zmieni
cy.get('.status')
.should('not.have.text', 'Nieaktywny')
.and('have.text', 'Aktywny')
.and('have.class', 'status-active')
// Czekaj aż progress osiągnie 100%
cy.get('.progress-bar')
.should('have.attr', 'aria-valuenow', '100')
.and('have.text', '100%')
})
})
})
Diagram
graph TB
A[Element DOM] --> B[Widoczność]
A --> C[Tekst]
A --> D[Atrybuty]
B --> B1[should 'be.visible']
B --> B2[should 'not.be.visible']
B --> B3[should 'exist']
B --> B4[should 'not.exist']
C --> C1[should 'have.text']
C --> C2[should 'contain']
C --> C3[should 'match' regex]
C --> C4[invoke 'text']
C --> C5[should 'have.html']
D --> D1[should 'have.attr']
D --> D2[invoke 'attr']
D --> D3[should 'have.class']
D --> D4[should 'have.value']
D --> D5[should 'have.prop']
D --> D6[should 'have.css']
subgraph "Widoczność - Warunki"
E1[Istnieje w DOM]
E2[Niezerowe wymiary]
E3[Nie display:none]
E4[Nie visibility:hidden]
E5[Nie zasłonięty]
end
B1 --> E1
B1 --> E2
B1 --> E3
B1 --> E4
B1 --> E5
subgraph "Tekst - Typy"
F1[Dokładne: have.text]
F2[Częściowe: contain]
F3[Regex: match]
F4[HTML: have.html]
end
C1 --> F1
C2 --> F2
C3 --> F3
C5 --> F4
subgraph "Atrybuty - Rodzaje"
G1[HTML: href, src, id]
G2[Data-*: data-id]
G3[Klasy: class]
G4[Style: css]
G5[Właściwości: prop]
end
D1 --> G1
D1 --> G2
D3 --> G3
D6 --> G4
D5 --> G5
style A fill:#E1F5FF
style B1 fill:#C3F0C3
style C1 fill:#FFE6CC
style D1 fill:#FFD9E6
Materiały
- Cypress Assertions - Visibility - dokumentacja asercji widoczności
- Cypress Assertions - Text Content - asercje dla tekstu
- Cypress Assertions - Attributes - sprawdzanie atrybutów i właściwości
Jak testować stan aplikacji (URL, cookies, localStorage)?
Odpowiedź w 30 sekund
Do testowania URL używamy cy.url() z asercjami should('include') lub should('eq'). Cookies sprawdzamy przez cy.getCookie('name') i cy.getCookies(), a ustawiamy przez cy.setCookie(). LocalStorage testujemy przez cy.window().its('localStorage') lub dedykowane komendy cy.clearLocalStorage(). Wszystkie te operacje mają wbudowany retry i działają asynchronicznie.
Odpowiedź w 2 minuty
Cypress oferuje bogate API do testowania stanu aplikacji na różnych poziomach. URL testujemy przez komendę cy.url(), która zwraca aktualny adres i automatycznie czeka na zmiany routingu. Możemy sprawdzać pełny URL przez should('eq'), fragment przez should('include'), lub używać wyrażeń regularnych przez should('match'). Dodatkowo cy.location() daje dostęp do wszystkich części URL (pathname, search, hash, protocol).
Cookies są testowane przez dedykowane komendy: cy.getCookie('name') pobiera pojedyncze cookie, cy.getCookies() zwraca wszystkie cookies dla danej domeny, cy.setCookie() ustawia cookie, a cy.clearCookie() i cy.clearCookies() je usuwają. Można sprawdzać wartości, daty wygaśnięcia, flagi secure/httpOnly oraz domain i path. Cookies są automatycznie zachowywane między testami w tym samym it(), ale czyszczone między różnymi testami.
LocalStorage i sessionStorage testujemy przez obiekt window. Używamy cy.window().its('localStorage.key') do sprawdzenia wartości, lub cy.window().then() do bardziej złożonych operacji. Cypress oferuje też cy.clearLocalStorage() do czyszczenia storage. Podobnie dla sessionStorage. Można również tworzyć własne komendy do wygodniejszego zarządzania storage.
Stan aplikacji to także Redux/Vuex store, który można testować przez cy.window().its('store'), zmienne globalne, query parametry URL, oraz session data. Cypress pozwala na pełną introspekcję i modyfikację stanu aplikacji w trakcie testów, co jest bardzo przydatne do setupu scenariuszy testowych czy debugowania.
Przykład kodu
describe('Testowanie stanu aplikacji', () => {
describe('Testowanie URL', () => {
it('Sprawdzanie pełnego URL', () => {
cy.visit('https://example.com/products?category=electronics')
// Pełny URL
cy.url().should('eq', 'https://example.com/products?category=electronics')
// Częściowe dopasowanie
cy.url().should('include', '/products')
cy.url().should('include', 'category=electronics')
// Wyrażenie regularne
cy.url().should('match', /\/products\?.*/)
})
it('Sprawdzanie części URL', () => {
cy.visit('https://example.com/users/123?tab=profile#settings')
// cy.location() - dostęp do wszystkich części URL
cy.location('protocol').should('eq', 'https:')
cy.location('hostname').should('eq', 'example.com')
cy.location('pathname').should('eq', '/users/123')
cy.location('search').should('eq', '?tab=profile')
cy.location('hash').should('eq', '#settings')
// Obiekt location
cy.location().should((loc) => {
expect(loc.protocol).to.eq('https:')
expect(loc.pathname).to.eq('/users/123')
expect(loc.search).to.include('tab=profile')
expect(loc.hash).to.eq('#settings')
})
})
it('Czekanie na zmianę URL', () => {
cy.visit('https://example.com')
cy.get('a[href="/about"]').click()
// Cypress czeka aż URL się zmieni
cy.url().should('include', '/about')
// Czekanie na konkretny parametr query
cy.get('#filter-btn').click()
cy.url().should('include', 'filter=active')
})
it('Testowanie navigacji', () => {
cy.visit('https://example.com/page1')
cy.url().should('include', '/page1')
cy.visit('https://example.com/page2')
cy.url().should('include', '/page2')
// Powrót
cy.go('back')
cy.url().should('include', '/page1')
// Dalej
cy.go('forward')
cy.url().should('include', '/page2')
// Odświeżenie
cy.reload()
cy.url().should('include', '/page2')
})
it('Query parameters', () => {
cy.visit('https://example.com/search?q=cypress&lang=pl&page=2')
// Sprawdzenie query params
cy.location('search')
.should('include', 'q=cypress')
.and('include', 'lang=pl')
.and('include', 'page=2')
// Parsowanie query params
cy.location('search').then((search) => {
const params = new URLSearchParams(search)
expect(params.get('q')).to.equal('cypress')
expect(params.get('lang')).to.equal('pl')
expect(params.get('page')).to.equal('2')
})
})
it('Hash/fragment', () => {
cy.visit('https://example.com/docs#installation')
cy.location('hash').should('eq', '#installation')
// Kliknięcie w link z hash
cy.get('a[href="#api"]').click()
cy.location('hash').should('eq', '#api')
})
})
describe('Testowanie Cookies', () => {
beforeEach(() => {
cy.visit('https://example.com')
})
it('Pobieranie i sprawdzanie cookie', () => {
// Ustawienie cookie
cy.setCookie('session_id', 'abc123')
// Pobranie cookie
cy.getCookie('session_id')
.should('exist')
.and('have.property', 'value', 'abc123')
// Sprawdzenie właściwości cookie
cy.getCookie('session_id').should((cookie) => {
expect(cookie.value).to.eq('abc123')
expect(cookie.domain).to.include('example.com')
expect(cookie.httpOnly).to.be.false
expect(cookie.secure).to.be.false
})
})
it('Sprawdzanie nieistniejącego cookie', () => {
cy.getCookie('non_existent').should('be.null')
})
it('Pobieranie wszystkich cookies', () => {
cy.setCookie('cookie1', 'value1')
cy.setCookie('cookie2', 'value2')
cy.setCookie('cookie3', 'value3')
cy.getCookies()
.should('have.length', 3)
.then((cookies) => {
expect(cookies[0]).to.have.property('name', 'cookie1')
expect(cookies[1]).to.have.property('name', 'cookie2')
expect(cookies[2]).to.have.property('name', 'cookie3')
})
})
it('Ustawianie cookie z opcjami', () => {
cy.setCookie('auth_token', 'xyz789', {
domain: 'example.com',
path: '/api',
secure: true,
httpOnly: true,
expiry: Math.floor(Date.now() / 1000) + 3600 // 1 godzina
})
cy.getCookie('auth_token').should((cookie) => {
expect(cookie.value).to.eq('xyz789')
expect(cookie.secure).to.be.true
expect(cookie.httpOnly).to.be.true
expect(cookie.path).to.eq('/api')
})
})
it('Usuwanie cookies', () => {
cy.setCookie('temp', 'temporary')
cy.getCookie('temp').should('exist')
// Usunięcie konkretnego cookie
cy.clearCookie('temp')
cy.getCookie('temp').should('be.null')
// Usunięcie wszystkich cookies
cy.setCookie('cookie1', 'value1')
cy.setCookie('cookie2', 'value2')
cy.clearCookies()
cy.getCookies().should('be.empty')
})
it('Testowanie logowania przez cookies', () => {
// Login
cy.visit('https://example.com/login')
cy.get('input[name="username"]').type('user')
cy.get('input[name="password"]').type('password')
cy.get('button[type="submit"]').click()
// Sprawdzenie że cookie sesji zostało ustawione
cy.getCookie('session')
.should('exist')
.and('have.property', 'value')
.and('match', /^[a-z0-9]+$/i)
// Nawigacja - cookie powinno być zachowane
cy.visit('https://example.com/dashboard')
cy.getCookie('session').should('exist')
})
it('Cookie między różnymi domenami', () => {
// Cypress domyślnie czyści cookies przy zmianie superdomain
cy.visit('https://example.com')
cy.setCookie('site_cookie', 'value')
// Ta wizyta wyczyści cookies (inna domena)
cy.visit('https://different-site.com')
// Powrót - cookie już nie istnieje
cy.visit('https://example.com')
cy.getCookie('site_cookie').should('be.null')
})
})
describe('Testowanie LocalStorage', () => {
beforeEach(() => {
cy.visit('https://example.com')
})
it('Sprawdzanie wartości w localStorage', () => {
// Ustawienie wartości przez aplikację
cy.window().then((win) => {
win.localStorage.setItem('theme', 'dark')
win.localStorage.setItem('language', 'pl')
})
// Sprawdzenie wartości
cy.window()
.its('localStorage.theme')
.should('equal', 'dark')
cy.window()
.its('localStorage.language')
.should('equal', 'pl')
})
it('Sprawdzanie złożonych obiektów w localStorage', () => {
const user = { id: 1, name: 'Jan', role: 'admin' }
cy.window().then((win) => {
win.localStorage.setItem('user', JSON.stringify(user))
})
cy.window().then((win) => {
const stored = JSON.parse(win.localStorage.getItem('user'))
expect(stored).to.deep.equal(user)
expect(stored.name).to.equal('Jan')
expect(stored.role).to.equal('admin')
})
})
it('Czyszczenie localStorage', () => {
cy.window().then((win) => {
win.localStorage.setItem('key1', 'value1')
win.localStorage.setItem('key2', 'value2')
})
// Czyszczenie wszystkiego
cy.clearLocalStorage()
cy.window().then((win) => {
expect(win.localStorage.length).to.equal(0)
})
})
it('Czyszczenie wybranych kluczy', () => {
cy.window().then((win) => {
win.localStorage.setItem('keep_this', 'value1')
win.localStorage.setItem('remove_this', 'value2')
})
// Czyszczenie kluczy pasujących do regex
cy.clearLocalStorage('remove_')
cy.window().then((win) => {
expect(win.localStorage.getItem('keep_this')).to.exist
expect(win.localStorage.getItem('remove_this')).to.be.null
})
})
it('Testowanie zapisywania przez aplikację', () => {
cy.get('#save-settings-btn').click()
// Sprawdzenie że aplikacja zapisała w localStorage
cy.window()
.its('localStorage.settings')
.should('exist')
.then((settings) => {
const parsed = JSON.parse(settings)
expect(parsed).to.have.property('notifications', true)
expect(parsed).to.have.property('theme')
})
})
it('Setup stanu przez localStorage', () => {
// Ustawienie stanu przed testem
cy.visit('https://example.com', {
onBeforeLoad(win) {
win.localStorage.setItem('auth_token', 'test-token-123')
win.localStorage.setItem('user_id', '42')
}
})
// Aplikacja powinna być już zalogowana
cy.get('.user-menu').should('be.visible')
cy.get('.user-id').should('contain', '42')
})
})
describe('Testowanie SessionStorage', () => {
it('Sprawdzanie sessionStorage', () => {
cy.visit('https://example.com')
cy.window().then((win) => {
win.sessionStorage.setItem('temp_data', 'temporary')
})
cy.window()
.its('sessionStorage.temp_data')
.should('equal', 'temporary')
// SessionStorage czyszczone przy nowej karcie/oknie
cy.reload()
cy.window()
.its('sessionStorage.temp_data')
.should('equal', 'temporary') // Nadal istnieje po reload
})
})
describe('Testowanie stanu Redux/Vuex', () => {
it('Sprawdzanie Redux store', () => {
cy.visit('https://example.com') // Aplikacja z Redux
// Dostęp do store przez window
cy.window()
.its('store')
.invoke('getState')
.should((state) => {
expect(state.user).to.exist
expect(state.user.isLoggedIn).to.be.false
})
})
it('Modyfikacja stanu Redux', () => {
cy.visit('https://example.com')
// Dispatch akcji
cy.window().then((win) => {
win.store.dispatch({
type: 'LOGIN_SUCCESS',
payload: { id: 1, name: 'Test User' }
})
})
// Sprawdzenie że UI się zaktualizowało
cy.get('.user-name').should('contain', 'Test User')
// Sprawdzenie stanu
cy.window()
.its('store')
.invoke('getState')
.its('user.isLoggedIn')
.should('be.true')
})
})
describe('Własne komendy dla stanu', () => {
// Definicja własnych komend
before(() => {
// Komenda do ustawiania localStorage
Cypress.Commands.add('setLocalStorage', (key, value) => {
cy.window().then((win) => {
win.localStorage.setItem(key, value)
})
})
// Komenda do pobierania localStorage
Cypress.Commands.add('getLocalStorage', (key) => {
return cy.window().then((win) => {
return win.localStorage.getItem(key)
})
})
// Komenda do logowania przez localStorage
Cypress.Commands.add('loginByLocalStorage', (token) => {
cy.setLocalStorage('auth_token', token)
cy.setLocalStorage('logged_in', 'true')
})
})
it('Użycie własnych komend', () => {
cy.visit('https://example.com')
cy.setLocalStorage('theme', 'dark')
cy.getLocalStorage('theme').should('equal', 'dark')
cy.loginByLocalStorage('test-token')
cy.reload()
cy.get('.logged-in-indicator').should('be.visible')
})
})
describe('Testowanie wszystkich stanów razem', () => {
it('Kompletny stan aplikacji', () => {
cy.visit('https://example.com')
// URL
cy.url().should('include', '/home')
// Cookies
cy.setCookie('preferences', 'saved')
cy.getCookie('preferences').should('exist')
// LocalStorage
cy.window().then((win) => {
win.localStorage.setItem('version', '1.0')
})
cy.window().its('localStorage.version').should('equal', '1.0')
// Wykonanie akcji
cy.get('#navigate-btn').click()
// Sprawdzenie zmian stanu
cy.url().should('include', '/other-page')
cy.getCookie('preferences').should('exist') // Zachowane
cy.window().its('localStorage.version').should('equal', '1.0') // Zachowane
})
})
})
Diagram
graph TB
A[Stan Aplikacji] --> B[URL]
A --> C[Cookies]
A --> D[Storage]
A --> E[App State]
B --> B1[cy.url]
B --> B2[cy.location]
B --> B3[cy.visit]
B --> B4[cy.go back/forward]
B1 --> B1A[should include]
B1 --> B1B[should eq]
B1 --> B1C[should match]
B2 --> B2A[pathname]
B2 --> B2B[search query]
B2 --> B2C[hash]
B2 --> B2D[protocol]
C --> C1[cy.getCookie]
C --> C2[cy.getCookies]
C --> C3[cy.setCookie]
C --> C4[cy.clearCookie]
C1 --> C1A[Wartość]
C1 --> C1B[Domain]
C1 --> C1C[Path]
C1 --> C1D[Secure/HttpOnly]
C1 --> C1E[Expiry]
D --> D1[LocalStorage]
D --> D2[SessionStorage]
D1 --> D1A[cy.window.its localStorage]
D1 --> D1B[cy.clearLocalStorage]
D1 --> D1C[setItem/getItem]
E --> E1[Redux Store]
E --> E2[Vuex Store]
E --> E3[Zmienne globalne]
E1 --> E1A[window.store.getState]
E1 --> E1B[window.store.dispatch]
subgraph "Lifecycle"
F1[visit] --> F2[Interakcja]
F2 --> F3[Zmiana URL]
F2 --> F4[Ustawienie Cookie]
F2 --> F5[Zapis Storage]
F3 --> F6[Asercje]
F4 --> F6
F5 --> F6
end
style A fill:#E1F5FF
style B1 fill:#C3F0C3
style C1 fill:#FFE6CC
style D1A fill:#FFD9E6
Materiały
- Cypress Location and Navigation - dokumentacja testowania URL i nawigacji
- Cypress Cookies API - pełne API do zarządzania cookies
- Cypress Window and LocalStorage - testowanie window, localStorage i sessionStorage
Komendy i Custom Commands
Czym są komendy Cypress i jak działają łańcuchowo?
Odpowiedź w 30 sekund: Komendy Cypress to asynchroniczne operacje zwracające obiekty podlegające dalszym operacjom w łańcuchu. Działają w kolejce i są wykonywane sekwencyjnie, co eliminuje potrzebę używania async/await. Każda komenda zwraca nowy obiekt, który można dalej modyfikować za pomocą kolejnych komend połączonych kropką.
Odpowiedź w 2 minuty: Komendy Cypress są fundamentem frameworka testowego i stanowią asynchroniczne operacje, które są dodawane do kolejki wykonania. W przeciwieństwie do tradycyjnychPromisów JavaScript, komendy Cypress wykorzystują wzorzec projektowy "chaining" (łańcuchowanie), gdzie każda komenda zwraca nowy obiekt Cypress, który może być dalej przetwarzany. To podejście sprawia, że kod testowy jest czytelny i przypomina naturalny język.
Mechanizm łańcuchowy działa poprzez automatyczne zarządzanie kolejką komend. Cypress czeka na zakończenie każdej komendy przed wykonaniem następnej, automatycznie ponawiając operacje w przypadku niepowodzenia (retry-ability). Nie ma potrzeby ręcznego zarządzania Promise'ami czy używania async/await, ponieważ Cypress obsługuje to wewnętrznie.
Istnieją dwa główne typy komend: parent commands (komendy nadrzędne), które zaczynają nowy łańcuch jak cy.get(), oraz child commands (komendy podrzędne), które wymagają poprzedniego podmiotu jak .click() czy .type(). Dodatkowo są dual commands, które mogą działać w obu rolach, jak cy.contains().
Kluczową cechą jest to, że komendy nie zwracają bezpośrednio wartości - aby uzyskać dostęp do rezultatu, używamy .then(). Dzięki temu Cypress może zarządzać czasem wykonania i automatycznie ponawiać operacje, co czyni testy bardziej odpornymi na problemy z timingiem.
Przykład kodu:
// Przykład łańcuchowania komend Cypress
describe('Łańcuchowanie komend', () => {
it('demonstruje działanie łańcuchowe komend', () => {
// Parent command - rozpoczyna łańcuch
cy.visit('https://example.com')
// Child commands - kontynuują łańcuch
cy.get('.user-form') // Parent: znajduje element
.find('input[name="email"]') // Child: szuka w znalezionym elemencie
.type('test@example.com') // Child: wpisuje tekst
.should('have.value', 'test@example.com') // Assertion w łańcuchu
// Nowy łańcuch z dual command
cy.contains('Submit') // Może być parent lub child
.click() // Child: kliknięcie
.should('be.disabled') // Sprawdzenie stanu po kliknięciu
// Użycie .then() do dostępu do wartości
cy.get('.result-message')
.then(($element) => {
// $element to jQuery obiekt
const text = $element.text()
expect(text).to.include('Sukces')
// Możemy wykonać synchroniczne operacje
cy.log(`Otrzymana wiadomość: ${text}`)
})
// Alternatywne łańcuchowanie z rozgałęzieniem
cy.get('.items')
.find('li')
.should('have.length.greaterThan', 0)
.first() // Bierze pierwszy element
.should('contain', 'Item')
})
it('pokazuje różnicę między comandami a Promise', () => {
// ❌ NIE DZIAŁA - komendy nie są Promise'ami
// const button = cy.get('button') // undefined!
// ✅ DZIAŁA - użycie .then()
cy.get('button').then(($button) => {
// Mamy dostęp do elementu
const text = $button.text()
expect(text).to.equal('Kliknij mnie')
})
// ✅ DZIAŁA - alias dla późniejszego użycia
cy.get('button').as('submitButton')
// Użycie aliasu później
cy.get('@submitButton').click()
})
it('demonstruje automatyczne retry dla komend', () => {
cy.visit('/dynamic-content')
// Cypress automatycznie ponawia przez 4 sekundy
cy.get('.loading-spinner')
.should('not.exist') // Czeka aż zniknie
cy.get('.content')
.should('be.visible') // Czeka aż się pojawi
.and('contain', 'Załadowano') // Kolejny warunek w łańcuchu
})
})
// Przykład tworzenia własnego łańcucha komend
describe('Złożone łańcuchowanie', () => {
it('łączy wiele operacji w jeden przepływ', () => {
cy.visit('/dashboard')
// Długi łańcuch logiczny
cy.get('.user-menu')
.click() // Otwórz menu
.parent() // Przejdź do rodzica
.find('.dropdown-menu') // Znajdź dropdown
.should('be.visible') // Sprawdź widoczność
.within(() => { // Kontekst wewnątrz elementu
cy.contains('Profil').click() // Znajdź i kliknij
})
// Weryfikacja nowej strony
cy.url().should('include', '/profile')
cy.get('h1').should('contain', 'Mój Profil')
})
})
Diagram:
sequenceDiagram
participant Test as Kod Testu
participant Queue as Kolejka Cypress
participant Browser as Przeglądarka
Test->>Queue: cy.visit('/page')
Test->>Queue: cy.get('.button')
Test->>Queue: .click()
Test->>Queue: .should('be.visible')
Note over Test,Queue: Wszystkie komendy dodane do kolejki
Queue->>Browser: visit('/page')
Browser-->>Queue: ✓ Załadowano
Queue->>Browser: get('.button')
Browser-->>Queue: ✗ Nie znaleziono
Queue->>Browser: get('.button') [retry]
Browser-->>Queue: ✓ Znaleziono
Queue->>Browser: click()
Browser-->>Queue: ✓ Kliknięto
Queue->>Browser: should('be.visible')
Browser-->>Queue: ✓ Widoczny
Note over Queue: Wszystkie komendy wykonane sekwencyjnie
Materiały:
Jak tworzyć własne komendy (Custom Commands)?
Odpowiedź w 30 sekund:
Custom Commands w Cypress tworzy się za pomocą Cypress.Commands.add() w pliku support/commands.js. Pozwalają one hermetyzować powtarzalne operacje w reużywalne funkcje. Mogą przyjmować parametry, używać argumentów opcjonalnych i być łańcuchowane z innymi komendami Cypress.
Odpowiedź w 2 minuty:
Custom Commands to mechanizm rozszerzania API Cypress o własne, wielokrotnie używane komendy. Definiuje się je w pliku cypress/support/commands.js (lub w osobnych plikach importowanych w tym pliku) za pomocą Cypress.Commands.add(). Jest to idealne rozwiązanie dla operacji wykonywanych w wielu testach, jak logowanie, wypełnianie formularzy czy złożone interakcje z UI.
Istnieją trzy typy Custom Commands: parent commands (rozpoczynające nowy łańcuch), child commands (działające na poprzednim podmiocie) oraz dual commands (mogące działać w obu rolach). Każdy typ ma inne zastosowanie i różni się sposobem deklaracji, szczególnie w zakresie parametru określającego poprzedni podmiot.
Custom Commands mają dostęp do wszystkich standardowych komend Cypress i mogą zwracać wartości, które będą używane w łańcuchu. Mogą również przyjmować opcje konfiguracyjne, używać TypeScript dla type safety, i być nadpisywane za pomocą Cypress.Commands.overwrite() do modyfikacji istniejących komend.
Ważne jest, by nie nadużywać Custom Commands - powinny być używane dla prawdziwej reużywalności, nie do ukrywania złożoności czy tworzenia abstrakcji na siłę. Dla prostych operacji lepiej używać standardowych funkcji JavaScript lub helpers.
Przykład kodu:
// cypress/support/commands.js
// 1. PARENT COMMAND - rozpoczyna nowy łańcuch
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('input[name="email"]').type(email)
cy.get('input[name="password"]').type(password)
cy.get('button[type="submit"]').click()
// Weryfikacja poprawnego logowania
cy.url().should('include', '/dashboard')
cy.getCookie('auth_token').should('exist')
})
// Użycie w teście
describe('Test logowania', () => {
it('loguje użytkownika', () => {
cy.login('user@example.com', 'password123')
cy.contains('Witaj, użytkowniku!')
})
})
// 2. CHILD COMMAND - działa na poprzednim podmiocie
Cypress.Commands.add('getById', { prevSubject: 'element' }, (subject, id) => {
// subject to poprzedni element w łańcuchu
return cy.wrap(subject).find(`[data-testid="${id}"]`)
})
// Użycie child command
cy.get('.container')
.getById('user-profile')
.should('be.visible')
// 3. DUAL COMMAND - może być parent lub child
Cypress.Commands.add('clickLink', { prevSubject: 'optional' }, (subject, label) => {
if (subject) {
// Jako child - szuka w subject
return cy.wrap(subject).contains('a', label).click()
} else {
// Jako parent - szuka globalnie
return cy.contains('a', label).click()
}
})
// Użycie jako parent
cy.clickLink('Strona główna')
// Użycie jako child
cy.get('.navigation').clickLink('Kontakt')
// 4. COMMAND Z OPCJAMI KONFIGURACYJNYMI
Cypress.Commands.add('loginByAPI', (username, password, options = {}) => {
const defaults = {
endpoint: '/api/auth/login',
rememberMe: false,
}
const config = { ...defaults, ...options }
cy.request({
method: 'POST',
url: config.endpoint,
body: {
username,
password,
rememberMe: config.rememberMe,
},
}).then((response) => {
// Zapisz token w localStorage
window.localStorage.setItem('authToken', response.body.token)
// Ustaw cookie jeśli zaznaczono "pamiętaj mnie"
if (config.rememberMe) {
cy.setCookie('remember_token', response.body.rememberToken)
}
})
})
// Użycie z opcjami
cy.loginByAPI('admin', 'admin123', {
rememberMe: true,
endpoint: '/api/v2/login',
})
// 5. COMMAND DO WYPEŁNIANIA FORMULARZY
Cypress.Commands.add('fillForm', (formData) => {
Object.entries(formData).forEach(([field, value]) => {
if (typeof value === 'boolean') {
// Dla checkboxów
const action = value ? 'check' : 'uncheck'
cy.get(`[name="${field}"]`)[action]()
} else if (Array.isArray(value)) {
// Dla select multiple
cy.get(`[name="${field}"]`).select(value)
} else {
// Dla zwykłych inputów
cy.get(`[name="${field}"]`).clear().type(value)
}
})
})
// Użycie
cy.fillForm({
firstName: 'Jan',
lastName: 'Kowalski',
email: 'jan@example.com',
newsletter: true,
country: 'Poland',
})
// 6. OVERWRITE - nadpisanie istniejącej komendy
Cypress.Commands.overwrite('type', (originalFn, subject, text, options) => {
// Dodaj opóźnienie przed każdym wpisywaniem
if (options && options.sensitive) {
// Nie loguj wrażliwych danych
options.log = false
Cypress.log({
name: 'type',
message: '***',
})
}
// Wywołaj oryginalną komendę
return originalFn(subject, text, options)
})
// Użycie nadpisanej komendy
cy.get('[name="password"]').type('secretPassword', { sensitive: true })
// 7. COMMAND Z ALIASEM I RETRY
Cypress.Commands.add('waitForResource', (resourceName, timeout = 10000) => {
const started = Date.now()
return cy.window().then((win) => {
return new Cypress.Promise((resolve, reject) => {
const check = () => {
const elapsed = Date.now() - started
if (win[resourceName]) {
resolve(win[resourceName])
} else if (elapsed > timeout) {
reject(new Error(`Zasób ${resourceName} nie załadował się w ${timeout}ms`))
} else {
setTimeout(check, 100)
}
}
check()
})
})
})
// Użycie
cy.waitForResource('myGlobalLibrary').then((library) => {
expect(library.version).to.exist
})
// 8. COMMAND DO MOCKOWANIA DATY
Cypress.Commands.add('setDate', (date) => {
cy.clock(new Date(date).getTime())
})
// Użycie
cy.setDate('2024-01-15')
cy.visit('/calendar') // Strona zobaczy zamrożoną datę
Przykład TypeScript definition:
// cypress/support/index.d.ts
declare namespace Cypress {
interface Chainable {
/**
* Loguje użytkownika przez formularz
* @param email - Email użytkownika
* @param password - Hasło użytkownika
* @example cy.login('user@example.com', 'password123')
*/
login(email: string, password: string): Chainable<void>
/**
* Znajduje element po data-testid wewnątrz poprzedniego elementu
* @param id - Wartość data-testid
* @example cy.get('.container').getById('user-profile')
*/
getById(id: string): Chainable<JQuery<HTMLElement>>
/**
* Wypełnia formularz na podstawie obiektu
* @param formData - Obiekt z danymi formularza
*/
fillForm(formData: Record<string, string | boolean | string[]>): Chainable<void>
}
}
Diagram:
graph TD
A[Test File] -->|używa| B[Custom Command]
B -->|zdefiniowana w| C[commands.js]
C -->|importowana przez| D[support/index.js]
D -->|ładowana przez| E[Cypress]
B --> F{Typ Command}
F -->|Parent| G[Rozpoczyna łańcuch]
F -->|Child| H[Kontynuuje łańcuch]
F -->|Dual| I[Parent lub Child]
B --> J[Może używać]
J --> K[Standardowe komendy cy.*]
J --> L[Inne Custom Commands]
J --> M[JavaScript/TypeScript]
style B fill:#f9f,stroke:#333
style C fill:#bfb,stroke:#333
Materiały:
Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziTestowanie API
Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOrganizacja Testów
Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziZaawansowane Techniki
Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziCI/CD i Raportowanie
Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi