To jest darmowy podgląd

To jest próbka 15 pytań z naszej pełnej kolekcji 40 pytań rekrutacyjnych. Uzyskaj pełny dostęp do wszystkich pytań ze szczegółowymi odpowiedziami i przykładami kodu.

Zobacz plany cenowe

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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

↑ Powrót na górę

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

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

Chcesz więcej pytań?

Uzyskaj dostęp do 800+ pytań z 13 technologii - JavaScript, React, TypeScript, Node.js, SQL i więcej. Wybierz plan 30-dniowy, 90-dniowy lub bezterminowy.

Wybierz plan od 49 zł