Fiszki Online Cypress (Preview)
Darmowy podgląd 15 z 40 dostępnych pytań
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 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:
↑ Powrót na górę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:
- Cypress: Selecting Elements
- Cypress: Best Practices - Selecting Elements
- Cypress API: Querying Commands
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:
- Cypress Best Practices: Selecting Elements
- Stop using CSS selectors in Cypress tests
- Testing Library: Priority for Queries
Komendy i Custom Commands
Jak używać cy.wrap() i cy.invoke()?
Odpowiedź w 30 sekund:
cy.wrap() zamienia zwykłe obiekty JavaScript w obiekty Cypress, umożliwiając ich użycie w łańcuchach komend. cy.invoke() wywołuje metody na obiektach w sposób kompatybilny z Cypress, z automatycznym retry. Obie komendy pozwalają pracować z danymi JavaScript w ekosystemie Cypress z zachowaniem jego automatycznych mechanizmów oczekiwania.
Odpowiedź w 2 minuty:
cy.wrap() jest kluczową komendą pozwalającą na "opakowanie" dowolnej wartości JavaScript (obiekt, tablica, Promise, element DOM) w obiekt Cypress. Dzięki temu można używać komend Cypress jak .should(), .then(), .its() na wartościach, które normalnie nie są częścią łańcucha Cypress. Jest szczególnie przydatna przy pracy z danymi pochodzącymi spoza Cypress, elementami jQuery, lub gdy chcemy kontynuować łańcuch po wykonaniu operacji synchronicznych.
cy.invoke() służy do wywoływania metod na obiektach w sposób asynchroniczny z możliwością automatycznego ponawiania (retry-ability). Zamiast bezpośrednio wywołać element.method(), używamy cy.invoke('method'), co pozwala Cypress na kontrolowanie wykonania i automatyczne ponawianie w przypadku błędów. Można przekazywać argumenty do wywoływanych metod.
Obie komendy są często używane razem: cy.wrap() do owinięcia obiektu, a następnie cy.invoke() do wywołania jego metod. Są niezbędne przy pracy z zewnętrznymi bibliotekami, manipulacji localStorage/sessionStorage, wywoływaniu metod na elementach DOM (jak scrollIntoView, focus), oraz przy testowaniu kodu JavaScript niezwiązanego bezpośrednio z UI.
Ważną cechą jest to, że obie komendy zachowują asynchroniczny charakter Cypress i mogą być retry'owane, co czyni testy bardziej stabilnymi niż bezpośrednie wywołania JavaScript.
Przykład kodu:
describe('cy.wrap() i cy.invoke()', () => {
// 1. PODSTAWOWE UŻYCIE cy.wrap()
it('owija wartości JavaScript w obiekty Cypress', () => {
// Owija zwykłą wartość
cy.wrap('Hello World')
.should('equal', 'Hello World')
// Owija obiekt
const user = {
name: 'Jan Kowalski',
age: 30,
email: 'jan@example.com'
}
cy.wrap(user)
.should('have.property', 'name', 'Jan Kowalski')
.its('age')
.should('be.greaterThan', 18)
// Owija tablicę
const numbers = [1, 2, 3, 4, 5]
cy.wrap(numbers)
.should('have.length', 5)
.its(2) // Dostęp do indeksu
.should('equal', 3)
})
// 2. cy.wrap() Z PROMISE
it('owija Promise i czeka na jego rozwiązanie', () => {
// Symulacja asynchronicznej operacji
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ status: 'success', data: [1, 2, 3] })
}, 1000)
})
}
// Cypress automatycznie czeka na Promise
cy.wrap(fetchData())
.should('have.property', 'status', 'success')
.its('data')
.should('have.length', 3)
})
// 3. cy.wrap() Z JQUERY ELEMENTAMI
it('owija elementy jQuery z .then()', () => {
cy.visit('https://example.com')
cy.get('h1').then(($element) => {
// $element jest jQuery obiektem
// Owijamy go aby kontynuować łańcuch Cypress
cy.wrap($element)
.should('be.visible')
.and('contain', 'Example')
.invoke('text')
.should('match', /Example/i)
})
})
// 4. PODSTAWOWE UŻYCIE cy.invoke()
it('wywołuje metody na elementach DOM', () => {
cy.visit('https://example.com')
// Wywołanie metody bez argumentów
cy.get('input[type="text"]')
.invoke('focus')
.should('have.focus')
// Wywołanie metody z argumentami
cy.get('.content')
.invoke('attr', 'data-status')
.should('equal', 'active')
// Wywołanie metody i praca z wynikiem
cy.get('p')
.invoke('text')
.then((text) => {
expect(text).to.have.length.greaterThan(0)
cy.log(`Tekst paragrafu: ${text}`)
})
})
// 5. cy.invoke() NA OBIEKTACH JAVASCRIPT
it('wywołuje metody na obiektach JS', () => {
const calculator = {
value: 0,
add(n) {
this.value += n
return this
},
multiply(n) {
this.value *= n
return this
},
getValue() {
return this.value
}
}
// Łańcuchowe wywołania metod
cy.wrap(calculator)
.invoke('add', 5)
.invoke('multiply', 2)
.invoke('getValue')
.should('equal', 10)
})
// 6. PRACA Z localStorage
it('manipuluje localStorage używając cy.wrap() i cy.invoke()', () => {
cy.visit('https://example.com')
// Ustawienie wartości w localStorage
cy.window().then((win) => {
win.localStorage.setItem('user', JSON.stringify({
name: 'Jan',
role: 'admin'
}))
})
// Odczyt z localStorage
cy.window()
.its('localStorage')
.invoke('getItem', 'user')
.then((userString) => {
const user = JSON.parse(userString)
expect(user.name).to.equal('Jan')
expect(user.role).to.equal('admin')
})
// Alternatywnie - krótszy zapis
cy.window()
.invoke('localStorage.getItem', 'user')
.then(JSON.parse)
.should('deep.equal', { name: 'Jan', role: 'admin' })
})
// 7. WYWOŁANIE METOD ZAGNIEŻDŻONYCH
it('wywołuje zagnieżdżone metody obiektów', () => {
cy.visit('https://example.com')
// Wywołanie metody na obiekcie window
cy.window()
.invoke('navigator.userAgent.toLowerCase')
.should('include', 'chrome')
// Dostęp do zagnieżdżonych właściwości i metod
cy.document()
.invoke('querySelectorAll', 'p')
.then((paragraphs) => {
cy.wrap(paragraphs).should('have.length.greaterThan', 0)
})
})
// 8. PRACA Z JQUERY METODAMI
it('używa metod jQuery na elementach', () => {
cy.visit('https://example.com')
// Wywołanie jQuery metod
cy.get('div')
.invoke('addClass', 'highlighted')
.should('have.class', 'highlighted')
cy.get('div.highlighted')
.invoke('removeClass', 'highlighted')
.should('not.have.class', 'highlighted')
// Pobieranie wartości z metod jQuery
cy.get('h1')
.invoke('height')
.should('be.greaterThan', 0)
cy.get('input')
.invoke('val', 'Nowa wartość')
.invoke('val')
.should('equal', 'Nowa wartość')
})
// 9. SCROLLING Z cy.invoke()
it('kontroluje scrollowanie używając invoke', () => {
cy.visit('/long-page')
// Scroll do elementu
cy.get('#footer')
.invoke('scrollIntoView')
.should('be.visible')
// Scroll okna do konkretnej pozycji
cy.window()
.invoke('scrollTo', 0, 500)
// Sprawdzenie pozycji scroll
cy.window()
.its('scrollY')
.should('be.greaterThan', 400)
})
// 10. ŁĄCZENIE cy.wrap() I cy.invoke() W CUSTOM COMMAND
it('łączy obie komendy w złożonych operacjach', () => {
// Symulacja zewnętrznej biblioteki
cy.visit('https://example.com')
cy.window().then((win) => {
// Dodajemy bibliotekę do window
win.myLib = {
users: [
{ id: 1, name: 'Jan' },
{ id: 2, name: 'Anna' },
{ id: 3, name: 'Piotr' }
],
findUser(id) {
return this.users.find(u => u.id === id)
},
getUserNames() {
return this.users.map(u => u.name)
}
}
})
// Praca z biblioteką
cy.window()
.its('myLib')
.invoke('findUser', 2)
.should('deep.equal', { id: 2, name: 'Anna' })
cy.window()
.its('myLib')
.invoke('getUserNames')
.should('deep.equal', ['Jan', 'Anna', 'Piotr'])
})
// 11. RETRY-ABILITY Z cy.invoke()
it('automatycznie powtarza invoke aż do sukcesu', () => {
cy.visit('https://example.com')
cy.window().then((win) => {
// Symulacja wartości która zmienia się asynchronicznie
win.asyncValue = null
setTimeout(() => {
win.asyncValue = 'loaded'
}, 2000)
})
// cy.invoke() będzie retry'ować aż wartość się pojawi
cy.window()
.its('asyncValue')
.should('equal', 'loaded') // Czeka do 2 sekund
})
// 12. PRAKTYCZNY PRZYKŁAD - TESTOWANIE API KLIENTA
it('testuje API klienta z cy.wrap() i cy.invoke()', () => {
// Symulacja API klienta
const apiClient = {
baseUrl: 'https://api.example.com',
async fetchUser(id) {
// Symulacja fetch
return {
id,
name: `User ${id}`,
active: true
}
},
async updateUser(id, data) {
return {
...data,
id,
updated: true
}
}
}
// Test metod API
cy.wrap(apiClient)
.invoke('fetchUser', 123)
.should('have.property', 'name', 'User 123')
.and('have.property', 'active', true)
cy.wrap(apiClient)
.invoke('updateUser', 123, { name: 'Jan Kowalski' })
.should('have.property', 'updated', true)
.and('have.property', 'name', 'Jan Kowalski')
})
})
// CUSTOM COMMAND UŻYWAJĄCY cy.wrap() I cy.invoke()
Cypress.Commands.add('getLocalStorage', (key) => {
return cy.window()
.its('localStorage')
.invoke('getItem', key)
.then((value) => {
return value ? JSON.parse(value) : null
})
})
Cypress.Commands.add('setLocalStorage', (key, value) => {
return cy.window()
.its('localStorage')
.invoke('setItem', key, JSON.stringify(value))
})
// Użycie custom commands
describe('Custom localStorage commands', () => {
it('używa custom commands do zarządzania localStorage', () => {
cy.visit('https://example.com')
// Ustawienie wartości
cy.setLocalStorage('settings', {
theme: 'dark',
language: 'pl'
})
// Odczyt wartości
cy.getLocalStorage('settings')
.should('deep.equal', {
theme: 'dark',
language: 'pl'
})
})
})
Diagram:
graph TD
A[Wartość JavaScript] -->|cy.wrap| B[Obiekt Cypress]
B --> C{Dostępne operacje}
C --> D[.should - asercje]
C --> E[.its - dostęp do właściwości]
C --> F[.invoke - wywołanie metod]
C --> G[.then - transformacje]
H[Element/Obiekt] -->|cy.invoke| I[Wywołanie metody]
I --> J[Automatyczne retry]
J -->|sukces| K[Zwrócona wartość]
J -->|błąd| L[Ponowienie]
L --> J
M[cy.wrap Promise] --> N[Czeka na rozwiązanie]
N --> O[Zwraca wartość]
style B fill:#f9f,stroke:#333
style I fill:#bfb,stroke:#333
style J fill:#fbb,stroke:#333
Materiały:
- cy.wrap() - Cypress Documentation
- cy.invoke() - Cypress Documentation
- Working with JavaScript Objects in Cypress
Organizacja Testów
Jak pomijać lub uruchamiać wybrane testy (skip, only)?
Odpowiedź w 30 sekund
Cypress oferuje metody .skip i .only do kontrolowania wykonywania testów: describe.skip() lub it.skip() pomija wybrane testy/bloki, a describe.only() lub it.only() uruchamia tylko oznaczone testy, ignorując pozostałe. To przydatne podczas debugowania, rozwoju nowych testów lub tymczasowego wyłączania niestabilnych testów, ale nie powinno być commitowane do repozytorium.
Odpowiedź w 2 minuty
Cypress, bazując na frameworku Mocha, dostarcza dwie kluczowe metody do selektywnego uruchamiania testów: .skip i .only. Metoda .skip służy do pomijania testów - można ją zastosować do pojedynczego testu (it.skip()) lub całego bloku testowego (describe.skip()). Pominięte testy są widoczne w raporcie jako "pending" i nie są wykonywane, co jest przydatne przy tymczasowym wyłączaniu niestabilnych testów lub testów wymagających naprawy.
Metoda .only działa odwrotnie - oznacza testy, które mają być uruchomione, ignorując wszystkie pozostałe. Jest niezwykle przydatna podczas rozwoju nowych testów lub debugowania, pozwalając skupić się na konkretnym przypadku bez uruchamiania całej suity. Można użyć wielu .only w pliku - wszystkie oznaczone testy zostaną wykonane.
Ważne aspekty używania .skip i .only: nie należy commitować kodu z tymi modyfikatorami do repozytorium (można to wymusić przez linter lub git hooks), .only w pliku nadpisuje .only w innych plikach przy uruchamianiu całej suity, pominięte testy nadal są analizowane pod kątem błędów składniowych, można łączyć z hookami - pominięty test nie wykonuje beforeEach ani afterEach.
Alternatywą dla .skip i .only jest używanie tagów w nazwach testów i filtrowanie przez konfigurację lub zmienne środowiskowe, co jest bardziej elastyczne i przyjazne dla CI/CD. Można też użyć Cypress.grep plugin do zaawansowanego filtrowania testów.
Przykład kodu
// Podstawowe użycie skip i only
describe('Testy funkcjonalności sklepu', () => {
// ✅ Ten test zostanie uruchomiony
it('powinien wyświetlić listę produktów', () => {
cy.visit('/products')
cy.get('[data-cy="product-item"]').should('have.length.gt', 0)
})
// ⏭️ Ten test zostanie pominięty
it.skip('powinien filtrować produkty po kategorii', () => {
// Test tymczasowo wyłączony - wymaga poprawki API
cy.visit('/products')
cy.get('[data-cy="category-filter"]').select('Electronics')
cy.get('[data-cy="product-item"]').should('have.length', 5)
})
// ✅ Ten test zostanie uruchomiony
it('powinien dodać produkt do koszyka', () => {
cy.visit('/products')
cy.get('[data-cy="product-item"]').first().click()
cy.get('[data-cy="add-to-cart"]').click()
cy.get('[data-cy="cart-count"]').should('contain', '1')
})
})
// Pomijanie całego bloku testów
describe.skip('Funkcjonalność płatności', () => {
// Cały blok zostanie pominięty - w trakcie refaktoryzacji
beforeEach(() => {
cy.visit('/checkout')
})
it('powinien przetworzyć płatność kartą kredytową', () => {
// Ten test nie zostanie uruchomiony
})
it('powinien przetworzyć płatność PayPal', () => {
// Ten test także nie zostanie uruchomiony
})
})
// Uruchamianie tylko wybranych testów z .only
describe('Debugowanie logowania', () => {
// ⏭️ Ten test zostanie pominięty (nie ma .only)
it('powinien wyświetlić formularz logowania', () => {
cy.visit('/login')
cy.get('[data-cy="login-form"]').should('be.visible')
})
// ✅ TYLKO ten test zostanie uruchomiony
it.only('powinien zalogować użytkownika', () => {
cy.visit('/login')
cy.get('[data-cy="email"]').type('test@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.get('[data-cy="submit"]').click()
cy.url().should('include', '/dashboard')
})
// ⏭️ Ten test zostanie pominięty (nie ma .only)
it('powinien wyświetlić błąd dla nieprawidłowych danych', () => {
cy.visit('/login')
cy.get('[data-cy="email"]').type('wrong@example.com')
cy.get('[data-cy="password"]').type('wrongpass')
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error"]').should('be.visible')
})
})
// Uruchamianie tylko wybranego bloku
describe.only('Testy koszyka - debugowanie', () => {
// ✅ Wszystkie testy w tym bloku zostaną uruchomione
beforeEach(() => {
cy.visit('/cart')
})
it('powinien wyświetlić pusty koszyk', () => {
cy.get('[data-cy="empty-cart-message"]').should('be.visible')
})
it('powinien pozwolić dodać produkt do koszyka', () => {
cy.visit('/products')
cy.get('[data-cy="product-item"]').first().click()
cy.get('[data-cy="add-to-cart"]').click()
cy.visit('/cart')
cy.get('[data-cy="cart-item"]').should('have.length', 1)
})
})
// ⏭️ Ten cały blok zostanie pominięty (poprzedni ma .only)
describe('Testy wyszukiwania', () => {
it('powinien wyszukać produkty', () => {
// Nie zostanie uruchomiony
})
})
// Zaawansowane przypadki użycia
describe('Zaawansowane użycie skip/only', () => {
// Warunkowe pomijanie testów
const isCI = Cypress.env('CI')
const skipInCI = isCI ? it.skip : it
skipInCI('test niestabilny w CI', () => {
// Ten test zostanie pominięty tylko w środowisku CI
cy.visit('/flaky-feature')
})
// Pomijanie testów dla konkretnych przeglądarek
it('powinien działać tylko w Chrome', () => {
if (Cypress.browser.name !== 'chrome') {
cy.log('⏭️ Pomijam test - nie jest to Chrome')
return
}
// Test specyficzny dla Chrome
cy.visit('/chrome-only-feature')
})
// Dynamiczne pomijanie na podstawie wersji API
context('Testy nowego API v2', () => {
before(function() {
cy.request('/api/version').then((response) => {
if (response.body.version < 2) {
// Pomiń wszystkie testy w tym context
this.skip()
}
})
})
it('powinien użyć endpointu v2', () => {
cy.request('/api/v2/products').its('status').should('eq', 200)
})
})
})
// Organizacja testów z tagami (alternatywa dla skip/only)
describe('Testy E2E - System płatności', () => {
// [SMOKE] - testy krytyczne, szybkie
it('[SMOKE] powinien wyświetlić stronę checkout', () => {
cy.visit('/checkout')
cy.get('[data-cy="checkout-form"]').should('be.visible')
})
// [REGRESSION] - testy regresyjne, wolniejsze
it('[REGRESSION] powinien zapisać dane płatności', () => {
cy.visit('/checkout')
// ... szczegółowy test
})
// [FLAKY] - testy niestabilne, wymagają naprawy
it.skip('[FLAKY] powinien przetworzyć płatność 3D Secure', () => {
// Test tymczasowo wyłączony z powodu niestabilności
})
})
// Przykład z cypress-grep plugin (wymaga instalacji)
// npm install -D @cypress/grep
// cypress.config.js: setupNodeEvents(on, config) { require('@cypress/grep/src/plugin')(config); return config; }
describe('Testy z tagami dla cypress-grep', () => {
// Uruchamianie: npx cypress run --env grep="@critical"
it('test krytyczny @critical @smoke', () => {
cy.log('Test krytyczny')
})
// Uruchamianie: npx cypress run --env grep="@slow",grepOmit="@flaky"
it('test wolny @slow', () => {
cy.log('Test wolny')
})
it('test niestabilny @flaky', () => {
cy.log('Test niestabilny')
})
})
// UWAGA: Przykład ZŁEJ PRAKTYKI - NIE commitować do repozytorium!
describe.only('⚠️ NIE COMMITOWAĆ z .only!', () => {
it.only('⚠️ Ten kod nie powinien trafić do repo', () => {
// .only i .skip są do użytku lokalnego podczas developmentu
// W CI/CD wszystkie testy powinny być uruchamiane
})
})
Diagram
graph TB
subgraph "Normalny przebieg testów"
A[Test Suite] --> B[Test 1 ✓]
A --> C[Test 2 ✓]
A --> D[Test 3 ✓]
A --> E[Test 4 ✓]
end
subgraph "Z użyciem .skip"
F[Test Suite] --> G[Test 1 ✓]
F --> H[Test 2 ⏭️ skip]
F --> I[Test 3 ✓]
F --> J[Test 4 ⏭️ skip]
style H fill:#ffcccc,stroke:#ff0000,stroke-dasharray: 5 5
style J fill:#ffcccc,stroke:#ff0000,stroke-dasharray: 5 5
end
subgraph "Z użyciem .only"
K[Test Suite] --> L[Test 1 ⏭️]
K --> M[Test 2 ✓ only]
K --> N[Test 3 ⏭️]
K --> O[Test 4 ✓ only]
style M fill:#ccffcc,stroke:#00aa00,stroke-width:3px
style O fill:#ccffcc,stroke:#00aa00,stroke-width:3px
style L fill:#eeeeee,stroke:#999999,stroke-dasharray: 5 5
style N fill:#eeeeee,stroke:#999999,stroke-dasharray: 5 5
end
subgraph "Warunkowe pomijanie"
P{Warunek?} -->|CI Environment| Q[Pomiń test]
P -->|Local| R[Uruchom test]
P -->|Browser != Chrome| S[Pomiń test]
P -->|Browser == Chrome| T[Uruchom test]
style Q fill:#ffcccc
style S fill:#ffcccc
style R fill:#ccffcc
style T fill:#ccffcc
end
subgraph "Filtrowanie przez tagi cypress-grep"
U[npx cypress run] --> V{--env grep=?}
V -->|@smoke| W[Uruchom testy @smoke]
V -->|@critical| X[Uruchom testy @critical]
V -->|@slow| Y[Uruchom testy @slow]
Z[--env grepOmit=@flaky] --> AA[Pomiń testy @flaky]
style W fill:#ccffcc
style X fill:#ccffcc
style Y fill:#ccffcc
style AA fill:#ffcccc
end
Materiały
- Cypress Documentation: Excluding and Including Tests
- cypress-grep Plugin - Advanced Test Filtering
- Mocha Exclusive and Inclusive Tests
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
Testowanie API
Jak testować endpointy API za pomocą cy.request()?
Odpowiedź w 30 sekund:
cy.request() to komenda Cypress służąca do wykonywania żądań HTTP bezpośrednio z poziomu testów. Pozwala testować API bez ładowania przeglądarki, obsługuje wszystkie metody HTTP (GET, POST, PUT, DELETE) i automatycznie obsługuje cookies oraz sesje. Można weryfikować status odpowiedzi, nagłówki, ciało odpowiedzi oraz wykorzystywać API do przygotowania stanu aplikacji przed testami UI.
Odpowiedź w 2 minuty:
cy.request() jest jedną z najpotężniejszych komend w Cypress, umożliwiającą bezpośrednie testowanie endpointów API. W przeciwieństwie do tradycyjnych narzędzi do testowania API, cy.request() automatycznie zarządza cookies i sesjami, co czyni ją idealnym narzędziem do testowania aplikacji wymagających autentykacji. Komenda ta wykonuje żądania z poziomu Node.js (nie z przeglądarki), co oznacza brak ograniczeń CORS.
Podstawowa składnia cy.request(url) może być rozszerzona o obiekt konfiguracyjny zawierający metodę HTTP, nagłówki, ciało żądania i inne opcje. Cypress automatycznie rzuca błąd dla kodów statusu 4xx i 5xx (chyba że ustawimy failOnStatusCode: false), co upraszcza testowanie happy path. Odpowiedź zawiera kompletne informacje o żądaniu i odpowiedzi: status, nagłówki, ciało, czas trwania.
cy.request() jest często wykorzystywane do seed'owania danych testowych, omijania UI w celu przyspieszenia testów (np. logowanie przez API zamiast formularz) oraz do weryfikacji efektów ubocznych akcji wykonanych w UI. Można łączyć żądania w łańcuchy, wykorzystując dane z poprzednich odpowiedzi, co umożliwia testowanie złożonych scenariuszy API.
Ważną zaletą jest automatyczne oczekiwanie na zakończenie żądania i retry w przypadku błędów sieciowych, co czyni testy bardziej stabilnymi. cy.request() obsługuje również przekierowania, auth basic i może być używane do pobierania zasobów statycznych w celu weryfikacji ich dostępności.
Przykład kodu:
describe('Testowanie API z cy.request()', () => {
// Prosty GET request
it('powinien pobrać listę użytkowników', () => {
cy.request('GET', 'https://api.example.com/users')
.then((response) => {
// Weryfikacja statusu odpowiedzi
expect(response.status).to.eq(200);
// Weryfikacja nagłówka
expect(response.headers).to.have.property('content-type', 'application/json');
// Weryfikacja ciała odpowiedzi
expect(response.body).to.be.an('array');
expect(response.body).to.have.length.greaterThan(0);
});
});
// POST request z danymi
it('powinien utworzyć nowego użytkownika', () => {
const newUser = {
name: 'Jan Kowalski',
email: 'jan@example.com',
role: 'admin'
};
cy.request({
method: 'POST',
url: 'https://api.example.com/users',
body: newUser,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.body).to.have.property('id');
expect(response.body.name).to.eq(newUser.name);
// Zapisanie ID do użycia w następnych testach
cy.wrap(response.body.id).as('userId');
});
});
// Wykorzystanie danych z poprzedniego requesta
it('powinien zaktualizować utworzonego użytkownika', function() {
cy.request({
method: 'PUT',
url: `https://api.example.com/users/${this.userId}`,
body: {
name: 'Jan Nowak'
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.name).to.eq('Jan Nowak');
});
});
// Testowanie błędnych requestów
it('powinien zwrócić błąd dla nieprawidłowych danych', () => {
cy.request({
method: 'POST',
url: 'https://api.example.com/users',
body: { email: 'invalid-email' },
failOnStatusCode: false // Nie rzucaj błędu dla 4xx/5xx
}).then((response) => {
expect(response.status).to.eq(400);
expect(response.body.errors).to.exist;
});
});
// Wykorzystanie API do przygotowania danych testowych
it('powinien zalogować użytkownika przez API', () => {
// Logowanie przez API (szybsze niż przez UI)
cy.request({
method: 'POST',
url: 'https://api.example.com/auth/login',
body: {
email: 'test@example.com',
password: 'haslo123'
}
}).then((response) => {
// Cookies są automatycznie zapisywane
expect(response.status).to.eq(200);
// Teraz możemy odwiedzić chronioną stronę
cy.visit('/dashboard');
cy.contains('Witaj, Użytkowniku').should('be.visible');
});
});
// Testowanie z query parameters
it('powinien filtrować wyniki za pomocą query params', () => {
cy.request({
method: 'GET',
url: 'https://api.example.com/users',
qs: {
role: 'admin',
status: 'active',
limit: 10
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.every(user => user.role === 'admin')).to.be.true;
});
});
// Testowanie z timeout i retry
it('powinien obsłużyć timeout requesta', () => {
cy.request({
method: 'GET',
url: 'https://api.example.com/slow-endpoint',
timeout: 10000, // 10 sekund timeout
retryOnStatusCodeFailure: true
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.duration).to.be.lessThan(10000);
});
});
});
Diagram:
sequenceDiagram
participant Test as Test Cypress
participant CyRequest as cy.request()
participant API as API Server
participant Browser as Przeglądarka
Test->>CyRequest: cy.request('GET', '/users')
Note over CyRequest: Wykonuje żądanie<br/>z Node.js (nie z przeglądarki)
CyRequest->>API: HTTP GET /users
API->>CyRequest: 200 OK + JSON data
Note over CyRequest: Automatycznie zapisuje cookies
CyRequest->>Test: Response object
Test->>Test: Asercje na response
Test->>CyRequest: cy.request('POST', '/login')
CyRequest->>API: HTTP POST /login + credentials
API->>CyRequest: 200 OK + Set-Cookie
Note over CyRequest: Cookie zapisane<br/>dla tej domeny
Test->>Browser: cy.visit('/dashboard')
Note over Browser: Cookies z cy.request()<br/>są dostępne
Browser->>API: GET /dashboard (z cookies)
API->>Browser: Strona dashboard
Materiały:
- Cypress cy.request() - Oficjalna Dokumentacja
- Network Requests - Cypress Best Practices
- API Testing with Cypress - Complete Guide
Zaawansowane Techniki
Jak testować aplikacje z iframe?
Odpowiedź w 30 sekund:
Cypress domyślnie nie obsługuje bezpośredniego dostępu do elementów wewnątrz iframe ze względu na ograniczenia bezpieczeństwa przeglądarki. Można to obejść używając polecenia cy.wrap() w połączeniu z contents() i find(), lub korzystając z dedykowanego pluginu cypress-iframe. Najlepszą praktyką jest utworzenie niestandardowego polecenia, które enkapsuluje logikę dostępu do iframe.
Odpowiedź w 2 minuty: Testowanie aplikacji z iframe w Cypress wymaga specjalnego podejścia, ponieważ framework nie obsługuje bezpośrednio przełączania kontekstu do iframe (jak robi to Selenium). Wynika to z tego, że Cypress działa bezpośrednio w przeglądarce i musi respektować ograniczenia same-origin policy.
Podstawowe podejście polega na uzyskaniu dostępu do dokumentu iframe za pomocą jQuery poprzez .its('0.contentDocument'), a następnie zawijaniu go w obiekt Cypress za pomocą cy.wrap(). To pozwala na użycie standardowych poleceń Cypress na elementach wewnątrz iframe.Ważne jest, aby iframe było w pełni załadowane przed próbą dostępu do jego zawartości, co można zapewnić poprzez .should('exist') lub czekanie na konkretny element.
Dla bardziej złożonych scenariuszy, plugin cypress-iframe oferuje gotowe polecenia takie jak cy.frameLoaded() i cy.iframe(), które upraszczają interakcję z iframe. Plugin obsługuje również zagnieżdżone iframe i automatycznie czeka na załadowanie zawartości.
Należy pamiętać, że iframe musi pochodzić z tej samej domeny (same-origin) co strona nadrzędna, w przeciwnym razie Cypress nie będzie mógł uzyskać dostępu do jego zawartości ze względu na politykę CORS. W przypadku iframe z różnych domen, lepszym rozwiązaniem może być bezpośrednie testowanie zawartości iframe jako oddzielnej aplikacji.
Przykład kodu:
// Podejście 1: Natywne rozwiązanie Cypress
describe('Testowanie iframe', () => {
it('powinno wchodzić w interakcję z elementem w iframe', () => {
cy.visit('/strona-z-iframe');
// Uzyskanie dostępu do iframe i jego zawartości
cy.get('iframe#moj-iframe')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('#przycisk-w-iframe')
.click();
});
});
// Podejście 2: Niestandardowe polecenie (zalecane)
Cypress.Commands.add('getIframeBody', (iframeSelector) => {
return cy
.get(iframeSelector)
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap);
});
describe('Testowanie z niestandardowym poleceniem', () => {
it('powinno używać niestandardowego polecenia iframe', () => {
cy.visit('/strona-z-iframe');
cy.getIframeBody('iframe#moj-iframe')
.find('#formularz-logowania')
.within(() => {
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('haslo123');
cy.get('button[type="submit"]').click();
});
});
});
// Podejście 3: Użycie pluginu cypress-iframe
// npm install -D cypress-iframe
import 'cypress-iframe';
describe('Testowanie z pluginem cypress-iframe', () => {
it('powinno używać pluginu do obsługi iframe', () => {
cy.visit('/strona-z-iframe');
// Czekanie na załadowanie iframe
cy.frameLoaded('iframe#moj-iframe');
// Interakcja z elementami wewnątrz iframe
cy.iframe('iframe#moj-iframe')
.find('#welcome-message')
.should('contain', 'Witaj');
cy.iframe('iframe#moj-iframe')
.find('#data-table')
.find('tr')
.should('have.length', 5);
});
it('powinno obsługiwać zagnieżdżone iframe', () => {
cy.visit('/strona-z-zagniezdzonymi-iframe');
// Dostęp do zagnieżdżonego iframe
cy.frameLoaded('iframe#zewnetrzne-iframe');
cy.iframe('iframe#zewnetrzne-iframe')
.find('iframe#wewnetrzne-iframe')
.then($iframe => {
cy.wrap($iframe.contents().find('body'))
.find('#element-w-zagniezdzonnym-iframe')
.click();
});
});
});
// Podejście 4: Zaawansowane - obsługa wielu iframe
Cypress.Commands.add('switchToIframe', (iframeSelector, callback) => {
cy.get(iframeSelector)
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.then(callback);
});
describe('Testowanie wielu iframe', () => {
it('powinno przełączać się między różnymi iframe', () => {
cy.visit('/strona-z-wieloma-iframe');
// Interakcja z pierwszym iframe
cy.switchToIframe('iframe#pierwszy-iframe', ($body) => {
cy.wrap($body)
.find('#przycisk-1')
.click();
});
// Interakcja z drugim iframe
cy.switchToIframe('iframe#drugi-iframe', ($body) => {
cy.wrap($body)
.find('#przycisk-2')
.should('be.visible');
});
});
});
Diagram:
graph TD
A[Strona główna] --> B{Wykryj iframe}
B --> C[cy.get iframe selector]
C --> D[its 0.contentDocument.body]
D --> E{Sprawdź czy załadowane}
E -->|Nie| F[should not.be.empty]
F --> E
E -->|Tak| G[then cy.wrap]
G --> H[find element w iframe]
H --> I[Wykonaj akcje na elemencie]
J[Alternatywnie: Plugin] --> K[cy.frameLoaded]
K --> L[cy.iframe selector]
L --> H
M[Ograniczenia] --> N[Same-origin policy]
M --> O[Brak wsparcia cross-origin iframe]
M --> P[Wymaga pełnego załadowania iframe]
style A fill:#e1f5ff
style I fill:#c8e6c9
style M fill:#ffcdd2
Materiały:
- Cypress - Working with iframes
- cypress-iframe plugin - GitHub
- Cypress Recipes - Tab Handling and Links
CI/CD i Raportowanie
Jak zintegrować Cypress z CI/CD (GitHub Actions, Jenkins)?
Odpowiedź w 30 sekund:
Integracja Cypress z CI/CD polega na dodaniu kroków uruchamiających testy w pipeline'ie. W GitHub Actions używamy oficjalnej akcji cypress-io/github-action, która automatycznie instaluje zależności, buduje aplikację i uruchamia testy. W Jenkins tworzymy job z krokami instalacji Node.js, npm install, build i npm run cypress:run.
Odpowiedź w 2 minuty:
Cypress można łatwo zintegrować z popularnymi platformami CI/CD dzięki interfejsowi CLI i trybowi headless. W GitHub Actions najwygodniej używać oficjalnej akcji cypress-io/github-action@v6, która automatyzuje cały proces - od instalacji zależności, przez cache'owanie, budowanie aplikacji, aż po uruchamianie testów. Akcja wspiera równoległe wykonywanie testów, nagrywanie wyników w Cypress Cloud oraz konfigurację różnych przeglądarek.
W Jenkins proces wymaga ręcznego skonfigurowania każdego kroku. Instalujemy wtyczkę NodeJS, definiujemy job typu Pipeline lub Freestyle, instalujemy zależności (npm ci), budujemy aplikację, a następnie uruchamiamy npx cypress run. Ważne jest ustawienie odpowiednich zmiennych środowiskowych, szczególnie CI=1 oraz CYPRESS_RECORD_KEY dla nagrywania w Cypress Dashboard.
Dla obu platform krytyczne jest zapewnienie stabilnego środowiska - używanie Docker containers z zainstalowanymi zależnościami systemowymi (xvfb, gtk, libgtk), cache'owanie node_modules i binarki Cypress oraz odpowiednia konfiguracja timeoutów. Warto też podzielić testy na grupy i uruchamiać je równolegle na wielu maszynach, co znacząco skraca czas wykonania całego suite'u.
Dodatkowe optymalizacje to używanie cypress-split dla automatycznego podziału testów, retry mechanizmów dla niestabilnych testów oraz integracja z narzędziami do raportowania jak Allure czy Mochawesome.
Przykład kodu:
# GitHub Actions - .github/workflows/cypress.yml
name: Cypress Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
cypress-run:
runs-on: ubuntu-22.04
strategy:
# Uruchomienie testów na 3 równoległych maszynach
fail-fast: false
matrix:
containers: [1, 2, 3]
steps:
- name: Checkout kodu
uses: actions/checkout@v4
- name: Cypress run
uses: cypress-io/github-action@v6
with:
# Budowanie aplikacji przed testami
build: npm run build
# Uruchomienie serwera deweloperskiego
start: npm start
# Czekanie na dostępność serwera
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
# Przeglądarka do testów
browser: chrome
# Specyfikacja testów (opcjonalnie)
spec: cypress/e2e/**/*.cy.js
# Nagrywanie w Cypress Cloud
record: true
parallel: true
group: 'UI Tests - Chrome'
env:
# Klucz do Cypress Cloud
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# ID projektu
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# Token GitHub dla lepszej integracji
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots przy błędach
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-${{ matrix.containers }}
path: cypress/screenshots
retention-days: 7
- name: Upload video
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos-${{ matrix.containers }}
path: cypress/videos
retention-days: 7
# Job dla testów komponentowych
cypress-component:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
component: true
browser: chrome
// Jenkins - Jenkinsfile
pipeline {
agent {
// Użycie Docker image z zainstalowanym Cypress
docker {
image 'cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
// Zmienne środowiskowe
CI = '1'
CYPRESS_CACHE_FOLDER = "${WORKSPACE}/.cache/cypress"
CYPRESS_RECORD_KEY = credentials('cypress-record-key')
CYPRESS_PROJECT_ID = credentials('cypress-project-id')
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh '''
# Instalacja zależności (użycie ci dla deterministycznych wersji)
npm ci
# Weryfikacja instalacji Cypress
npx cypress verify
npx cypress info
'''
}
}
stage('Build Application') {
steps {
sh 'npm run build'
}
}
stage('Start Server') {
steps {
script {
// Uruchomienie serwera w tle
sh 'npm start &'
// Czekanie na dostępność serwera
sh '''
npx wait-on http://localhost:3000 \
--timeout 120000 \
--interval 2000 \
--verbose
'''
}
}
}
stage('Run Cypress Tests') {
parallel {
stage('Chrome Tests') {
steps {
sh '''
npx cypress run \
--browser chrome \
--record \
--parallel \
--group "Jenkins - Chrome" \
--ci-build-id ${BUILD_TAG}
'''
}
}
stage('Firefox Tests') {
steps {
sh '''
npx cypress run \
--browser firefox \
--record \
--parallel \
--group "Jenkins - Firefox" \
--ci-build-id ${BUILD_TAG}
'''
}
}
}
}
}
post {
always {
// Publikacja wyników testów
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'cypress/reports',
reportFiles: 'index.html',
reportName: 'Cypress Test Report'
])
// Archiwizacja artefaktów
archiveArtifacts artifacts: 'cypress/videos/**/*.mp4', allowEmptyArchive: true
archiveArtifacts artifacts: 'cypress/screenshots/**/*.png', allowEmptyArchive: true
// Czyszczenie workspace
cleanWs()
}
failure {
// Powiadomienie przy błędzie
emailext(
subject: "Cypress Tests Failed: ${env.JOB_NAME} - Build #${env.BUILD_NUMBER}",
body: "Check console output at ${env.BUILD_URL}",
recipientProviders: [developers(), requestor()]
)
}
}
}
// cypress.config.js - Konfiguracja dla CI/CD
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// Globalny timeout zwiększony dla wolniejszych maszyn CI
defaultCommandTimeout: 10000,
// Retry dla niestabilnych testów w CI
retries: {
runMode: 2, // 2 retry w CI
openMode: 0 // Bez retry w trybie deweloperskim
},
// Nagrywanie video tylko przy błędach w CI
video: true,
videoCompression: 32,
videoUploadOnPasses: false,
// Screenshots przy błędach
screenshotOnRunFailure: true,
// ID projektu dla Cypress Cloud
projectId: process.env.CYPRESS_PROJECT_ID,
e2e: {
setupNodeEvents(on, config) {
// Konfiguracja dla różnych środowisk
const environment = config.env.ENVIRONMENT || 'staging';
const environments = {
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api-staging.example.com'
},
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com'
}
};
config.baseUrl = environments[environment].baseUrl;
config.env.apiUrl = environments[environment].apiUrl;
return config;
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}'
}
});
# Docker - Dockerfile dla własnego CI image
FROM cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
# Utworzenie katalogu roboczego
WORKDIR /app
# Kopiowanie plików package
COPY package*.json ./
# Instalacja zależności
RUN npm ci
# Weryfikacja Cypress
RUN npx cypress verify
# Kopiowanie kodu aplikacji
COPY . .
# Budowanie aplikacji
RUN npm run build
# Domyślna komenda
CMD ["npx", "cypress", "run"]
// package.json - Skrypty dla CI/CD
{
"scripts": {
"test": "cypress run",
"test:chrome": "cypress run --browser chrome",
"test:firefox": "cypress run --browser firefox",
"test:edge": "cypress run --browser edge",
"test:headed": "cypress run --headed",
"test:record": "cypress run --record --key $CYPRESS_RECORD_KEY",
"test:parallel": "cypress run --record --parallel --group 'Parallel Tests'",
"test:component": "cypress run --component",
"cypress:open": "cypress open",
"cypress:verify": "cypress verify && cypress info"
}
}
Diagram:
graph TB
subgraph "CI/CD Pipeline"
A[Trigger: Push/PR] --> B[Checkout Code]
B --> C[Setup Environment]
C --> D[Install Dependencies]
D --> E[Cache node_modules + Cypress binary]
E --> F[Build Application]
F --> G[Start Application Server]
G --> H{Parallel Execution}
H --> I1[Worker 1<br/>Chrome Tests]
H --> I2[Worker 2<br/>Firefox Tests]
H --> I3[Worker 3<br/>Component Tests]
I1 --> J[Collect Results]
I2 --> J
I3 --> J
J --> K{Tests Passed?}
K -->|Yes| L[Upload Artifacts]
K -->|No| M[Capture Screenshots]
M --> L
L --> N[Generate Reports]
N --> O{Record to Cloud?}
O -->|Yes| P[Upload to Cypress Cloud]
O -->|No| Q[Store Locally]
P --> R[Notify Team]
Q --> R
R --> S{Deploy?}
S -->|Yes| T[Deploy to Environment]
S -->|No| U[End Pipeline]
T --> U
end
subgraph "Artifacts"
V[Videos]
W[Screenshots]
X[Test Reports]
Y[Coverage Data]
end
L -.-> V
L -.-> W
N -.-> X
N -.-> Y
subgraph "Notifications"
Z1[Slack]
Z2[Email]
Z3[GitHub Status]
end
R -.-> Z1
R -.-> Z2
R -.-> Z3
style A fill:#e1f5ff
style K fill:#fff3cd
style S fill:#fff3cd
style P fill:#d4edda
style T fill:#d4edda
style M fill:#f8d7da
Materiały:
- Cypress CI/CD Documentation - Oficjalny przewodnik po CI/CD
- GitHub Actions for Cypress - Oficjalna akcja GitHub Actions
- Cypress Docker Images - Gotowe obrazy Docker dla CI