Pytania rekrutacyjne Cypress

40 pytań — 20 odpowiedzi dostępnych od razu

Przed rozmową o testach end-to-end warto umieć nie tylko napisać scenariusz, ale też wyjaśnić, dlaczego jest stabilny i jak go utrzymać. Ten darmowy podgląd Cypress pokazuje przykładowe pytania z puli 40 zagadnień rekrutacyjnych.

Zakres obejmuje selektory, interakcje, asercje, własne komendy oraz testowanie API. Materiał pomaga powtórzyć praktyczne decyzje dotyczące automatyzacji testów w aplikacjach webowych.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 40 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 20 odpowiedzi

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:

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:

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:

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:

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:

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:

Selektory i Interakcje

Jak Cypress znajduje elementy na stronie (selektory)?

Odpowiedź w 30 sekund:

Cypress używa komend takich jak cy.get(), cy.contains() i cy.find() do lokalizowania elementów. Obsługuje selektory CSS, atrybuty data, teksty oraz oferuje specjalne metody jak .parent(), .children(), czy .siblings(). Najpopularniejsze podejście to używanie atrybutów data-cy lub data-test dla stabilności testów.

Odpowiedź w 2 minuty:

Cypress oferuje bogaty zestaw metod do znajdowania elementów na stronie. Podstawową komendą jest cy.get(), która przyjmuje selektory CSS, identyfikatory, klasy czy atrybuty. Komenda cy.contains() pozwala znaleźć element po zawartym w nim tekście, co jest szczególnie przydatne przy testowaniu interfejsów użytkownika.

Do nawigacji po drzewie DOM Cypress udostępnia metody traversal takie jak .find() (szukanie potomków), .parent() (rodzic elementu), .children() (bezpośrednie dzieci), .siblings() (rodzeństwo), .first(), .last(), .eq() (wybór po indeksie) oraz .filter() i .not() do filtrowania wyników.

Zaleca się używanie dedykowanych atrybutów testowych jak data-cy, data-test czy data-testid, ponieważ są one niezależne od stylu i struktury aplikacji. Unikanie selektorów bazujących na klasach CSS czy strukturze DOM czyni testy bardziej odpornymi na zmiany w kodzie produkcyjnym.

Cypress automatycznie powtarza zapytania (retry) do momentu znalezienia elementu lub przekroczenia timeoutu, co eliminuje potrzebę dodawania explicite waitów w większości przypadków. Wszystkie komendy są automatycznie kolejkowane i wykonywane asynchronicznie, co upraszcza pisanie testów.

Przykład kodu:

describe('Znajdowanie elementów w Cypress', () => {
  beforeEach(() => {
    cy.visit('https://example.com/login');
  });

  it('używa różnych selektorów do znajdowania elementów', () => {
    // Selektor CSS po ID
    cy.get('#username').type('jan.kowalski');

    // Selektor po klasie
    cy.get('.btn-primary').click();

    // Selektor po atrybucie data-cy (zalecane)
    cy.get('[data-cy="password-input"]').type('haslo123');

    // Selektor po typie i atrybucie
    cy.get('input[name="email"]').type('jan@example.com');

    // Znajdowanie po tekście
    cy.contains('Zaloguj się').click();
    cy.contains('button', 'Wyślij').click(); // Precyzyjniejsze

    // Kombinacja get i contains
    cy.get('.navbar').contains('Profil').click();
  });

  it('używa metod traversal do nawigacji DOM', () => {
    // Znajdowanie potomków
    cy.get('.user-list').find('li').should('have.length', 5);

    // Wybór pierwszego i ostatniego elementu
    cy.get('.items').children().first().should('have.text', 'Pierwszy');
    cy.get('.items').children().last().should('have.text', 'Ostatni');

    // Wybór elementu po indeksie (0-based)
    cy.get('ul li').eq(2).should('contain', 'Trzeci element');

    // Nawigacja do rodzica
    cy.get('[data-cy="child-element"]').parent().should('have.class', 'parent-container');

    // Znajdowanie rodzeństwa
    cy.get('.active-item').siblings().should('have.length', 4);

    // Filtrowanie elementów
    cy.get('li').filter('.completed').should('have.length', 3);
    cy.get('li').not('.disabled').click({ multiple: true });
  });

  it('używa aliasów dla często używanych elementów', () => {
    // Utworzenie aliasu
    cy.get('[data-cy="search-input"]').as('searchBox');
    cy.get('.results-list').as('results');

    // Użycie aliasu
    cy.get('@searchBox').type('Cypress');
    cy.get('@results').should('be.visible');
    cy.get('@searchBox').clear().type('Testing');
  });

  it('łączy selektory dla precyzyjnego targetowania', () => {
    // Selektor wielopoziomowy
    cy.get('form#login-form input[type="text"]').first().type('user');

    // Kombinacja atrybutów
    cy.get('[data-cy="submit"][type="submit"]').click();

    // Nested selektory
    cy.get('.modal').within(() => {
      cy.get('[data-cy="modal-title"]').should('contain', 'Potwierdź');
      cy.get('[data-cy="confirm-btn"]').click();
    });
  });

  it('obsługuje dynamiczne selektory', () => {
    const userId = '12345';

    // Interpolacja w selektorze
    cy.get(`[data-user-id="${userId}"]`).click();
    cy.get(`#user-${userId}`).should('be.visible');

    // Dynamiczne wyszukiwanie po tekście
    const buttonText = 'Zapisz zmiany';
    cy.contains('button', buttonText).click();
  });
});

Diagram:

graph TD
    A[Cypress Selektory] --> B[Podstawowe Komendy]
    A --> C[Metody Traversal]
    A --> D[Best Practices]

    B --> B1[cy.get - selektory CSS]
    B --> B2[cy.contains - tekst]
    B --> B3[cy.find - potomkowie]

    C --> C1[.parent - rodzic]
    C --> C2[.children - dzieci]
    C --> C3[.siblings - rodzeństwo]
    C --> C4[.first/.last - pierwszy/ostatni]
    C --> C5[.eq - po indeksie]
    C --> C6[.filter/.not - filtrowanie]

    D --> D1[Używaj data-cy]
    D --> D2[Unikaj selektorów CSS]
    D --> D3[Stosuj aliasy]
    D --> D4[Within dla kontekstu]

    style A fill:#f9f,stroke:#333,stroke-width:4px
    style D1 fill:#bfb,stroke:#333,stroke-width:2px

Materiały:

Czym jest atrybut data-cy i dlaczego jest zalecany?

Odpowiedź w 30 sekund:

data-cy to niestandardowy atrybut HTML używany wyłącznie do celów testowych w Cypress. Jest zalecany, ponieważ oddziela logikę testową od implementacji UI - zmiany w klasach CSS, stylach czy strukturze DOM nie psują testów. Zapewnia stabilne i czytelne selektory niezależne od warstwy prezentacji.

Odpowiedź w 2 minuty:

Atrybut data-cy (lub alternatywnie data-test, data-testid) to specjalny atrybut HTML wprowadzony jako best practice w testowaniu aplikacji z Cypress. Jego głównym celem jest wyraźne oddzielenie warstwy testowej od implementacji interfejsu użytkownika. W przeciwieństwie do klas CSS czy identyfikatorów, które mogą się zmieniać podczas refaktoryzacji stylów lub logiki biznesowej, data-cy służy wyłącznie testom i pozostaje stabilny.

Stosowanie data-cy przynosi kilka kluczowych korzyści. Po pierwsze, czyni testy odpornymi na zmiany w kodzie produkcyjnym - zmiana nazwy klasy CSS nie wymaga aktualizacji testów. Po drugie, poprawia czytelność testów - cy.get('[data-cy="submit-button"]') jest bardziej zrozumiałe niż cy.get('.btn.btn-primary.mt-3'). Po trzecie, stanowi jasny kontrakt między deweloperami a testerami - każdy element z data-cy jest świadomie oznaczony jako testowany.

W praktyce zaleca się dodawanie atrybutów data-cy do wszystkich interaktywnych elementów (przyciski, pola formularzy, linki) oraz kluczowych elementów strukturalnych (nagłówki sekcji, kontenery modali). Wartości atrybutów powinny być opisowe i stabilne, preferując kebab-case dla spójności. W środowisku produkcyjnym można je usuwać podczas budowania aplikacji, aby zmniejszyć rozmiar HTML.

Warto zauważyć, że konwencja nazewnictwa może się różnić między projektami - niektóre zespoły używają data-test, data-testid czy data-qa. Najważniejsze jest zachowanie konsekwencji w całym projekcie i skonfigurowanie odpowiednich zasad w ESLint czy innych narzędziach do code review.

Przykład kodu:

// ========================================
// Przykład komponentu React z data-cy
// ========================================
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    // Logika logowania
  };

  return (
    <form onSubmit={handleSubmit} data-cy="login-form">
      <h2 data-cy="form-title">Zaloguj się</h2>

      {error && (
        <div className="alert alert-danger" data-cy="error-message">
          {error}
        </div>
      )}

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          className="form-control"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          data-cy="email-input"
          placeholder="Wpisz email"
        />
      </div>

      <div className="form-group">
        <label htmlFor="password">Hasło</label>
        <input
          id="password"
          type="password"
          className="form-control"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          data-cy="password-input"
          placeholder="Wpisz hasło"
        />
      </div>

      <button
        type="submit"
        className="btn btn-primary"
        data-cy="submit-button"
      >
        Zaloguj
      </button>

      <a href="/forgot-password" data-cy="forgot-password-link">
        Zapomniałeś hasła?
      </a>
    </form>
  );
}

// ========================================
// Test Cypress używający data-cy
// ========================================
describe('Formularz logowania', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('wyświetla wszystkie elementy formularza', () => {
    // Sprawdzenie widoczności kluczowych elementów
    cy.get('[data-cy="login-form"]').should('be.visible');
    cy.get('[data-cy="form-title"]').should('contain', 'Zaloguj się');
    cy.get('[data-cy="email-input"]').should('be.visible');
    cy.get('[data-cy="password-input"]').should('be.visible');
    cy.get('[data-cy="submit-button"]').should('be.visible');
    cy.get('[data-cy="forgot-password-link"]').should('be.visible');
  });

  it('pozwala użytkownikowi się zalogować', () => {
    // Wypełnienie formularza
    cy.get('[data-cy="email-input"]').type('jan@example.com');
    cy.get('[data-cy="password-input"]').type('SecurePass123');
    cy.get('[data-cy="submit-button"]').click();

    // Weryfikacja przekierowania
    cy.url().should('include', '/dashboard');
  });

  it('wyświetla błąd przy nieprawidłowych danych', () => {
    cy.get('[data-cy="email-input"]').type('wrong@example.com');
    cy.get('[data-cy="password-input"]').type('wrongpass');
    cy.get('[data-cy="submit-button"]').click();

    // Sprawdzenie komunikatu błędu
    cy.get('[data-cy="error-message"]')
      .should('be.visible')
      .and('contain', 'Nieprawidłowe dane logowania');
  });

  it('nawiguje do strony odzyskiwania hasła', () => {
    cy.get('[data-cy="forgot-password-link"]').click();
    cy.url().should('include', '/forgot-password');
  });
});

// ========================================
// Przykład komponentu Vue z data-cy
// ========================================
/*
<template>
  <div class="product-card" :data-cy="`product-${product.id}`">
    <img
      :src="product.image"
      :alt="product.name"
      :data-cy="`product-image-${product.id}`"
    />

    <h3 data-cy="product-name">{{ product.name }}</h3>

    <p data-cy="product-price" class="price">
      {{ formatPrice(product.price) }}
    </p>

    <div class="quantity-controls">
      <button
        @click="decrementQuantity"
        data-cy="decrease-quantity"
        :disabled="quantity === 0"
      >
        -
      </button>

      <span data-cy="quantity-value">{{ quantity }}</span>

      <button
        @click="incrementQuantity"
        data-cy="increase-quantity"
      >
        +
      </button>
    </div>

    <button
      @click="addToCart"
      data-cy="add-to-cart"
      class="btn-add-to-cart"
      :disabled="quantity === 0"
    >
      Dodaj do koszyka
    </button>
  </div>
</template>
*/

// Test dla komponentu produktu
describe('Karta produktu', () => {
  beforeEach(() => {
    cy.visit('/products/123');
  });

  it('pozwala dodać produkt do koszyka', () => {
    // Sprawdzenie początkowej ilości
    cy.get('[data-cy="quantity-value"]').should('have.text', '0');

    // Zwiększenie ilości
    cy.get('[data-cy="increase-quantity"]').click().click().click();
    cy.get('[data-cy="quantity-value"]').should('have.text', '3');

    // Dodanie do koszyka
    cy.get('[data-cy="add-to-cart"]').click();

    // Weryfikacja
    cy.get('[data-cy="cart-badge"]').should('contain', '3');
  });
});

// ========================================
// Custom command dla uproszczenia
// ========================================
// cypress/support/commands.js
Cypress.Commands.add('dataCy', (value) => {
  return cy.get(`[data-cy="${value}"]`);
});

// Użycie custom command
describe('Test z custom command', () => {
  it('używa uproszczonej składni', () => {
    cy.visit('/login');

    // Zamiast cy.get('[data-cy="email-input"]')
    cy.dataCy('email-input').type('test@example.com');
    cy.dataCy('password-input').type('password123');
    cy.dataCy('submit-button').click();

    cy.dataCy('welcome-message').should('be.visible');
  });
});

Diagram:

graph LR
    A[Strategie Selektorów] --> B[Złe Praktyki]
    A --> C[Dobre Praktyki]

    B --> B1[.btn.primary - Klasy CSS<br/>Zmieniają się często]
    B --> B2[#submit-btn-123 - ID<br/>Może być dynamiczne]
    B --> B3[div > form > button:nth-child - Struktura<br/>Krucha przy zmianach]

    C --> C1[data-cy='submit-btn'<br/>✓ Stabilny<br/>✓ Czytelny<br/>✓ Niezależny od UI]

    C1 --> D[Korzyści]
    D --> D1[Odporność na refaktoring]
    D --> D2[Jasny kontrakt test-dev]
    D --> D3[Łatwa konserwacja]
    D --> D4[Lepsza dokumentacja]

    style B1 fill:#fbb,stroke:#333,stroke-width:2px
    style B2 fill:#fbb,stroke:#333,stroke-width:2px
    style B3 fill:#fbb,stroke:#333,stroke-width:2px
    style C1 fill:#bfb,stroke:#333,stroke-width:2px

Materiały:

Jak wykonać kliknięcie, wpisanie tekstu i inne interakcje?

Odpowiedź w 30 sekund:

Cypress oferuje intuicyjne komendy do interakcji: .click() dla kliknięć, .type() dla wpisywania tekstu, .clear() dla czyszczenia pól, .check() i .uncheck() dla checkboxów, .select() dla dropdownów. Wszystkie komendy są automatycznie retry'owane i czekają na możliwość wykonania akcji. Obsługują również opcje modyfikatorów jak force, multiple czy delay.

Odpowiedź w 2 minuty:

Cypress dostarcza bogaty zestaw komend do symulowania interakcji użytkownika z aplikacją webową. Najczęściej używane to .click() do klikania elementów, .type() do wprowadzania tekstu, .clear() do czyszczenia pól tekstowych oraz .dblclick() do podwójnego kliknięcia. Każda z tych komend automatycznie czeka, aż element stanie się widoczny, interaktywny i nie będzie zakryty przez inne elementy.

Komenda .type() jest szczególnie zaawansowana - obsługuje znaki specjalne (np. {enter}, {backspace}, {selectall}), modyfikatory klawiatury ({ctrl}, {shift}, {alt}), oraz opcje jak delay do kontrolowania prędkości pisania czy force do wymuszenia akcji nawet gdy element jest zakryty. Można również symulować zachowania użytkownika jak zaznaczanie tekstu i jego zastępowanie.

Dla formularzy dostępne są dedykowane komendy: .check() i .uncheck() dla checkboxów i radio buttonów, .select() dla elementów select, oraz .focus() i .blur() dla zarządzania fokusem. Cypress automatycznie przewija stronę do elementu przed wykonaniem akcji i sprawdza, czy element nie jest disabled.

Wszystkie interakcje można dostosować za pomocą opcji - na przykład .click({ force: true }) wymusza kliknięcie pomijając sprawdzanie widoczności, .click({ multiple: true }) klika wszystkie znalezione elementy, a .type('text', { delay: 100 }) spowalnia pisanie. Istnieje również możliwość symulowania kliknięć w określone punkty elementu używając pozycji jak 'topLeft', 'center', czy współrzędnych x, y.

Przykład kodu:

describe('Interakcje użytkownika w Cypress', () => {
  beforeEach(() => {
    cy.visit('https://example.com');
  });

  // ========================================
  // PODSTAWOWE INTERAKCJE
  // ========================================
  it('wykonuje podstawowe kliknięcia', () => {
    // Proste kliknięcie
    cy.get('[data-cy="submit-btn"]').click();

    // Podwójne kliknięcie
    cy.get('[data-cy="edit-title"]').dblclick();

    // Prawy przycisk myszy (menu kontekstowe)
    cy.get('[data-cy="file-item"]').rightclick();

    // Kliknięcie w określone miejsce elementu
    cy.get('[data-cy="canvas"]').click('topLeft');
    cy.get('[data-cy="canvas"]').click('center');
    cy.get('[data-cy="canvas"]').click(150, 200); // x, y w pikselach

    // Kliknięcie z modyfikatorami
    cy.get('[data-cy="item"]').click({ ctrlKey: true }); // Ctrl+Click
    cy.get('[data-cy="item"]').click({ shiftKey: true }); // Shift+Click
    cy.get('[data-cy="item"]').click({ altKey: true });   // Alt+Click
  });

  it('korzysta z opcji force dla zakrytych elementów', () => {
    // Czasami element jest zakryty przez modal, overlay itp.
    // Force pomija sprawdzanie widoczności
    cy.get('[data-cy="hidden-button"]').click({ force: true });

    // Kliknięcie wielu elementów
    cy.get('.checkbox-item').click({ multiple: true });
  });

  // ========================================
  // WPISYWANIE TEKSTU
  // ========================================
  it('wpisuje tekst w różnych scenariuszach', () => {
    // Proste wpisanie
    cy.get('[data-cy="username"]').type('jan.kowalski');

    // Czyszczenie i wpisanie nowej wartości
    cy.get('[data-cy="email"]')
      .clear()
      .type('nowy@email.com');

    // Wpisywanie z opóźnieniem (symulacja wolnego pisania)
    cy.get('[data-cy="search"]').type('Cypress Testing', { delay: 100 });

    // Wpisywanie bez czyszczenia poprzedniej wartości
    cy.get('[data-cy="message"]').type('Dodatkowy tekst');
  });

  it('używa znaków specjalnych podczas pisania', () => {
    cy.get('[data-cy="comment"]').type('To jest komentarz');

    // Zaznaczenie całego tekstu i zastąpienie
    cy.get('[data-cy="comment"]')
      .type('{selectall}')
      .type('Nowy tekst');

    // Backspace i Delete
    cy.get('[data-cy="input"]')
      .type('Błądny tekst{backspace}{backspace}{backspace}{backspace}');

    // Enter (submit formularza)
    cy.get('[data-cy="search-box"]')
      .type('Wyszukiwana fraza{enter}');

    // Tab (przejście do następnego pola)
    cy.get('[data-cy="first-name"]')
      .type('Jan{tab}')
      .get('[data-cy="last-name"]') // Focus przeszedł automatycznie
      .type('Kowalski');

    // Kombinacje z Ctrl/Cmd
    cy.get('[data-cy="editor"]')
      .type('Zaznaczony tekst')
      .type('{selectall}')
      .type('{ctrl}c'); // Ctrl+C (kopiowanie)

    // Strzałki
    cy.get('[data-cy="text"]')
      .type('Test')
      .type('{leftarrow}{leftarrow}') // Kursor 2 znaki w lewo
      .type('XX'); // Wstawi XX w środku
  });

  it('obsługuje znaki specjalne wymagające escape', () => {
    // Klamry muszą być podwojone, bo są znakami specjalnymi
    cy.get('[data-cy="code-input"]').type('function() {{}}');

    // Wpisywanie adresu email
    cy.get('[data-cy="email"]').type('user@example.com');

    // Wpisywanie URL
    cy.get('[data-cy="url"]').type('https://example.com/path?param=value');
  });

  // ========================================
  // CHECKBOXY I RADIO BUTTONY
  // ========================================
  it('obsługuje checkboxy', () => {
    // Zaznaczenie checkboxa
    cy.get('[data-cy="terms-checkbox"]').check();
    cy.get('[data-cy="terms-checkbox"]').should('be.checked');

    // Odznaczenie checkboxa
    cy.get('[data-cy="newsletter"]').uncheck();
    cy.get('[data-cy="newsletter"]').should('not.be.checked');

    // Zaznaczenie wielu checkboxów naraz
    cy.get('.permission-checkbox').check({ multiple: true });

    // Zaznaczenie checkboxa po wartości
    cy.get('input[type="checkbox"]').check('option1');
    cy.get('input[type="checkbox"]').check(['option2', 'option3']);
  });

  it('obsługuje radio buttony', () => {
    // Radio button - tylko check, nie ma uncheck
    cy.get('[data-cy="payment-credit-card"]').check();
    cy.get('[data-cy="payment-credit-card"]').should('be.checked');

    // Zaznaczenie radio po wartości
    cy.get('input[name="size"]').check('medium');

    // Weryfikacja że inne radio zostało odznaczone
    cy.get('[data-cy="payment-paypal"]').should('not.be.checked');
  });

  // ========================================
  // DROPDOWN (SELECT)
  // ========================================
  it('obsługuje elementy select', () => {
    // Wybór po tekście widocznym dla użytkownika
    cy.get('[data-cy="country-select"]').select('Polska');

    // Wybór po wartości (value)
    cy.get('[data-cy="country-select"]').select('PL');

    // Wybór po indeksie (0-based)
    cy.get('[data-cy="month-select"]').select(5); // Czerwiec

    // Wybór wielu opcji (dla multiselect)
    cy.get('[data-cy="languages"]').select(['pl', 'en', 'de']);

    // Weryfikacja wybranej opcji
    cy.get('[data-cy="country-select"]')
      .should('have.value', 'PL');
  });

  // ========================================
  // FOCUS I BLUR
  // ========================================
  it('zarządza fokusem elementów', () => {
    // Ustawienie fokusa
    cy.get('[data-cy="username"]').focus();
    cy.get('[data-cy="username"]').should('have.focus');

    // Wpisanie i utrata fokusa (blur)
    cy.get('[data-cy="email"]')
      .type('test@example.com')
      .blur();

    // Blur może wywołać walidację
    cy.get('[data-cy="validation-error"]').should('not.exist');
  });

  // ========================================
  // HOVER (najechanie myszą)
  // ========================================
  it('symuluje najechanie myszą', () => {
    // Cypress nie ma natywnej komendy .hover()
    // Używamy .trigger() do wywołania zdarzeń myszy
    cy.get('[data-cy="menu-item"]').trigger('mouseover');

    // Sprawdzenie czy submenu się pokazało
    cy.get('[data-cy="submenu"]').should('be.visible');

    // Zjechanie z elementu
    cy.get('[data-cy="menu-item"]').trigger('mouseout');
  });

  // ========================================
  // SCROLLOWANIE
  // ========================================
  it('przewija stronę i elementy', () => {
    // Przewinięcie do elementu (automatyczne przy większości akcji)
    cy.get('[data-cy="footer"]').scrollIntoView();
    cy.get('[data-cy="footer"]').should('be.visible');

    // Przewinięcie do pozycji
    cy.scrollTo('bottom');
    cy.scrollTo('top');
    cy.scrollTo('center');

    // Przewinięcie do współrzędnych
    cy.scrollTo(0, 500); // x, y

    // Przewinięcie w kontenerze z overflow
    cy.get('[data-cy="scrollable-list"]').scrollTo('bottom');
  });

  // ========================================
  // ZŁOŻONE SCENARIUSZE
  // ========================================
  it('symuluje wypełnienie kompleksowego formularza', () => {
    // Dane osobowe
    cy.get('[data-cy="first-name"]').type('Jan');
    cy.get('[data-cy="last-name"]').type('Kowalski');
    cy.get('[data-cy="email"]').type('jan.kowalski@example.com');

    // Data urodzenia
    cy.get('[data-cy="birth-day"]').select('15');
    cy.get('[data-cy="birth-month"]').select('05');
    cy.get('[data-cy="birth-year"]').select('1990');

    // Płeć (radio)
    cy.get('[data-cy="gender-male"]').check();

    // Zainteresowania (checkboxy)
    cy.get('[data-cy="interest-sport"]').check();
    cy.get('[data-cy="interest-music"]').check();
    cy.get('[data-cy="interest-travel"]').check();

    // Kraj
    cy.get('[data-cy="country"]').select('Polska');

    // Zgody
    cy.get('[data-cy="terms"]').check();
    cy.get('[data-cy="privacy"]').check();

    // Komentarz
    cy.get('[data-cy="message"]')
      .type('To jest testowy komentarz.{enter}Nowa linia tekstu.');

    // Submit
    cy.get('[data-cy="submit-form"]').click();

    // Weryfikacja
    cy.url().should('include', '/success');
  });
});

Diagram:

graph TD
    A[Interakcje Cypress] --> B[Kliknięcia]
    A --> C[Wprowadzanie Tekstu]
    A --> D[Elementy Formularzy]
    A --> E[Zaawansowane]

    B --> B1[.click]
    B --> B2[.dblclick]
    B --> B3[.rightclick]
    B --> B4[Opcje: force, multiple, position]

    C --> C1[.type - wpisywanie]
    C --> C2[.clear - czyszczenie]
    C --> C3[Znaki specjalne: enter, tab, backspace]
    C --> C4[Modyfikatory: ctrl, shift, alt]

    D --> D1[.check/.uncheck - checkbox]
    D --> D2[.select - dropdown]
    D --> D3[.focus/.blur - fokus]

    E --> E1[.trigger - zdarzenia]
    E --> E2[.scrollIntoView - scroll]
    E --> E3[.invoke - metody DOM]

    style A fill:#f9f,stroke:#333,stroke-width:4px
    style B1 fill:#bbf,stroke:#333,stroke-width:2px
    style C1 fill:#bbf,stroke:#333,stroke-width:2px
    style D1 fill:#bbf,stroke:#333,stroke-width:2px

Materiały:

Jak obsługiwać formularze w Cypress?

Odpowiedź w 30 sekund:

Formularze w Cypress obsługuje się przez wypełnianie pól komendą .type(), wybieranie opcji przez .select(), zaznaczanie checkboxów przez .check(), a następnie submit przez .click() na przycisku lub .submit() na formularzu. Cypress automatycznie czeka na gotowość elementów i weryfikuje możliwość interakcji. Można też używać .invoke('val', value) do bezpośredniego ustawiania wartości.

Odpowiedź w 2 minuty:

Obsługa formularzy w Cypress jest intuicyjna i przypomina rzeczywiste interakcje użytkownika. Podstawowy workflow polega na lokalizowaniu pól formularza (preferowane przez atrybuty data-cy), wypełnianiu ich odpowiednimi danymi używając .type(), .select(), .check(), a następnie wysłaniu formularza przez kliknięcie przycisku submit lub wywołanie .submit() bezpośrednio na elemencie <form>.

Cypress oferuje kilka strategii wypełniania formularzy. Najprostsza to sekwencyjne wypełnianie każdego pola z użyciem .type() dla tekstów, .select() dla dropdownów i .check() dla checkboxów. Dla bardziej zaawansowanych scenariuszy można użyć .invoke('val', value) do bezpośredniego ustawienia wartości bez symulowania pisania, co jest szybsze ale pomija walidację onChange. Można też łączyć te podejścia w zależności od potrzeb.

Ważnym aspektem jest testowanie walidacji formularzy. Cypress pozwala sprawdzać komunikaty błędów, disabled state przycisków submit, wymagane pola oraz walidację po stronie klienta. Można testować zarówno walidację inline (podczas wypełniania) jak i po wysłaniu formularza. Przydatne są asercje .should('have.attr', 'required') czy .should('have.class', 'is-invalid').

Dla formularzy z dynamicznymi polami (dodawanie/usuwanie sekcji) Cypress radzi sobie dobrze dzięki automatycznemu retry. Można również testować upload plików używając .selectFile(), czyszczenie formularzy, zapisywanie drafts oraz przywracanie wartości. Best practice to tworzenie custom commands dla często wypełnianych formularzy (np. cy.fillLoginForm(email, password)) aby zwiększyć reużywalność i czytelność testów.

Przykład kodu:

describe('Obsługa formularzy w Cypress', () => {

  // ========================================
  // PODSTAWOWE WYPEŁNIANIE FORMULARZA
  // ========================================
  it('wypełnia i wysyła prosty formularz kontaktowy', () => {
    cy.visit('/contact');

    // Wypełnienie wszystkich pól
    cy.get('[data-cy="name-input"]').type('Jan Kowalski');
    cy.get('[data-cy="email-input"]').type('jan@example.com');
    cy.get('[data-cy="phone-input"]').type('+48 123 456 789');
    cy.get('[data-cy="subject-select"]').select('Zapytanie ogólne');
    cy.get('[data-cy="message-textarea"]')
      .type('To jest treść wiadomości testowej.{enter}Nowa linia.');

    // Zaznaczenie zgody
    cy.get('[data-cy="consent-checkbox"]').check();

    // Wysłanie formularza przez kliknięcie przycisku
    cy.get('[data-cy="submit-button"]').click();

    // Weryfikacja sukcesu
    cy.get('[data-cy="success-message"]')
      .should('be.visible')
      .and('contain', 'Dziękujemy za wiadomość');
  });

  // ========================================
  // FORMULARZ REJESTRACJI Z WALIDACJĄ
  // ========================================
  it('testuje walidację formularza rejestracji', () => {
    cy.visit('/register');

    // Próba wysłania pustego formularza
    cy.get('[data-cy="register-button"]').click();

    // Sprawdzenie komunikatów błędów
    cy.get('[data-cy="username-error"]')
      .should('be.visible')
      .and('contain', 'Nazwa użytkownika jest wymagana');

    cy.get('[data-cy="email-error"]')
      .should('be.visible')
      .and('contain', 'Email jest wymagany');

    // Wypełnienie nieprawidłowym emailem
    cy.get('[data-cy="email-input"]').type('nieprawidlowy-email');
    cy.get('[data-cy="email-input"]').blur();

    cy.get('[data-cy="email-error"]')
      .should('contain', 'Podaj prawidłowy adres email');

    // Poprawienie emaila
    cy.get('[data-cy="email-input"]')
      .clear()
      .type('poprawny@example.com');

    // Błąd powinien zniknąć
    cy.get('[data-cy="email-error"]').should('not.exist');

    // Test zgodności haseł
    cy.get('[data-cy="password-input"]').type('SecurePass123!');
    cy.get('[data-cy="confirm-password-input"]').type('DifferentPass');
    cy.get('[data-cy="confirm-password-input"]').blur();

    cy.get('[data-cy="password-match-error"]')
      .should('contain', 'Hasła muszą być identyczne');

    // Poprawienie hasła
    cy.get('[data-cy="confirm-password-input"]')
      .clear()
      .type('SecurePass123!');

    // Wypełnienie pozostałych wymaganych pól
    cy.get('[data-cy="username-input"]').type('jankowalski');
    cy.get('[data-cy="terms-checkbox"]').check();

    // Teraz submit powinien działać
    cy.get('[data-cy="register-button"]').should('not.be.disabled');
    cy.get('[data-cy="register-button"]').click();

    // Weryfikacja przekierowania
    cy.url().should('include', '/welcome');
  });

  // ========================================
  // FORMULARZ Z DYNAMICZNYMI POLAMI
  // ========================================
  it('obsługuje dynamiczne dodawanie i usuwanie pól', () => {
    cy.visit('/job-application');

    // Początkowe dane
    cy.get('[data-cy="applicant-name"]').type('Jan Kowalski');

    // Sprawdzenie że jest tylko jedna sekcja doświadczenia
    cy.get('[data-cy^="experience-"]').should('have.length', 1);

    // Wypełnienie pierwszego doświadczenia
    cy.get('[data-cy="experience-0-company"]').type('Firma ABC');
    cy.get('[data-cy="experience-0-position"]').type('Frontend Developer');
    cy.get('[data-cy="experience-0-years"]').select('2-3 lata');

    // Dodanie kolejnego doświadczenia
    cy.get('[data-cy="add-experience-button"]').click();

    // Sprawdzenie że jest już 2 sekcje
    cy.get('[data-cy^="experience-"]').should('have.length', 2);

    // Wypełnienie drugiego doświadczenia
    cy.get('[data-cy="experience-1-company"]').type('Firma XYZ');
    cy.get('[data-cy="experience-1-position"]').type('Junior Developer');
    cy.get('[data-cy="experience-1-years"]').select('1-2 lata');

    // Usunięcie pierwszego doświadczenia
    cy.get('[data-cy="remove-experience-0"]').click();

    // Weryfikacja że został tylko jeden element
    cy.get('[data-cy^="experience-"]').should('have.length', 1);

    // Weryfikacja że pozostał właściwy element
    cy.get('[data-cy="experience-0-company"]').should('have.value', 'Firma XYZ');
  });

  // ========================================
  // UPLOAD PLIKÓW
  // ========================================
  it('obsługuje upload plików', () => {
    cy.visit('/upload-document');

    // Upload jednego pliku
    cy.get('[data-cy="file-input"]').selectFile('cypress/fixtures/cv.pdf');

    // Weryfikacja nazwy pliku
    cy.get('[data-cy="file-name"]').should('contain', 'cv.pdf');

    // Upload wielu plików
    cy.get('[data-cy="multiple-files-input"]').selectFile([
      'cypress/fixtures/photo1.jpg',
      'cypress/fixtures/photo2.jpg'
    ]);

    // Weryfikacja ilości plików
    cy.get('[data-cy="uploaded-files-list"] li').should('have.length', 2);

    // Upload przez drag & drop
    cy.get('[data-cy="drop-zone"]').selectFile('cypress/fixtures/document.docx', {
      action: 'drag-drop'
    });
  });

  // ========================================
  // BEZPOŚREDNIE USTAWIANIE WARTOŚCI
  // ========================================
  it('używa invoke do szybkiego wypełniania formularza', () => {
    cy.visit('/profile-edit');

    // Bezpośrednie ustawienie wartości (szybsze, ale pomija zdarzenia)
    cy.get('[data-cy="bio-textarea"]').invoke('val', 'Moja biografia testowa');

    // Wywołanie zdarzenia input aby uruchomić walidację
    cy.get('[data-cy="bio-textarea"]').trigger('input');

    // Weryfikacja licznika znaków
    cy.get('[data-cy="char-counter"]').should('contain', '23');

    // Kombinacja invoke i type dla konkretnych scenariuszy
    cy.get('[data-cy="tags-input"]')
      .invoke('val', 'JavaScript, React') // Wstępne dane
      .type(', TypeScript{enter}'); // Dodanie kolejnego taga
  });

  // ========================================
  // CUSTOM COMMAND DLA FORMULARZY
  // ========================================
  // W pliku cypress/support/commands.js:
  /*
  Cypress.Commands.add('fillLoginForm', (email, password) => {
    cy.get('[data-cy="email-input"]').clear().type(email);
    cy.get('[data-cy="password-input"]').clear().type(password);
  });

  Cypress.Commands.add('submitForm', (formSelector) => {
    cy.get(formSelector).within(() => {
      cy.get('[type="submit"]').click();
    });
  });
  */

  it('używa custom commands dla czytelności', () => {
    cy.visit('/login');

    // Użycie custom command
    cy.fillLoginForm('test@example.com', 'password123');
    cy.submitForm('[data-cy="login-form"]');

    // Weryfikacja
    cy.url().should('include', '/dashboard');
  });

  // ========================================
  // FORMULARZ Z AUTOUZUPEŁNIANIEM
  // ========================================
  it('obsługuje autocomplete/autosuggest', () => {
    cy.visit('/search');

    // Wpisanie tekstu wywołującego sugestie
    cy.get('[data-cy="search-input"]').type('Cypre');

    // Czekanie na pojawienie się sugestii
    cy.get('[data-cy="suggestions-list"]').should('be.visible');

    // Sprawdzenie czy pierwsza sugestia zawiera wpisany tekst
    cy.get('[data-cy="suggestion-0"]').should('contain', 'Cypress');

    // Kliknięcie w sugestię
    cy.get('[data-cy="suggestion-0"]').click();

    // Weryfikacja że wartość została wstawiona
    cy.get('[data-cy="search-input"]').should('have.value', 'Cypress Testing');
  });

  // ========================================
  // RESETOWANIE I CZYSZCZENIE FORMULARZA
  // ========================================
  it('testuje czyszczenie formularza', () => {
    cy.visit('/feedback');

    // Wypełnienie formularza
    cy.get('[data-cy="name"]').type('Jan Kowalski');
    cy.get('[data-cy="email"]').type('jan@example.com');
    cy.get('[data-cy="rating"]').select('5');
    cy.get('[data-cy="comment"]').type('Świetna aplikacja!');

    // Kliknięcie przycisku reset/clear
    cy.get('[data-cy="clear-button"]').click();

    // Weryfikacja że pola zostały wyczyszczone
    cy.get('[data-cy="name"]').should('have.value', '');
    cy.get('[data-cy="email"]').should('have.value', '');
    cy.get('[data-cy="rating"]').should('have.value', '');
    cy.get('[data-cy="comment"]').should('have.value', '');
  });

  // ========================================
  // FORMULARZ WIELOSTRONICOWY (WIZARD)
  // ========================================
  it('przechodzi przez formularz wielostronicowy', () => {
    cy.visit('/registration-wizard');

    // Krok 1: Dane osobowe
    cy.get('[data-cy="current-step"]').should('contain', 'Krok 1 z 3');
    cy.get('[data-cy="first-name"]').type('Jan');
    cy.get('[data-cy="last-name"]').type('Kowalski');
    cy.get('[data-cy="next-button"]').click();

    // Krok 2: Dane kontaktowe
    cy.get('[data-cy="current-step"]').should('contain', 'Krok 2 z 3');
    cy.get('[data-cy="email"]').type('jan@example.com');
    cy.get('[data-cy="phone"]').type('123456789');
    cy.get('[data-cy="next-button"]').click();

    // Krok 3: Podsumowanie
    cy.get('[data-cy="current-step"]').should('contain', 'Krok 3 z 3');
    cy.get('[data-cy="summary-name"]').should('contain', 'Jan Kowalski');
    cy.get('[data-cy="summary-email"]').should('contain', 'jan@example.com');

    // Możliwość powrotu
    cy.get('[data-cy="back-button"]').click();
    cy.get('[data-cy="current-step"]').should('contain', 'Krok 2 z 3');

    // Powrót do kroku 3 i submit
    cy.get('[data-cy="next-button"]').click();
    cy.get('[data-cy="submit-button"]').click();

    // Weryfikacja sukcesu
    cy.url().should('include', '/success');
  });
});

Diagram:

graph TD
    A[Strategie Testowania Formularzy] --> B[Wypełnianie]
    A --> C[Walidacja]
    A --> D[Submit]
    A --> E[Edge Cases]

    B --> B1[.type - symulacja użytkownika]
    B --> B2[.invoke - bezpośrednie wartości]
    B --> B3[.select - dropdowny]
    B --> B4[.check - checkboxy]
    B --> B5[.selectFile - upload]

    C --> C1[Walidacja inline]
    C --> C2[Komunikaty błędów]
    C --> C3[Disabled state]
    C --> C4[Required fields]

    D --> D1[.click na button]
    D --> D2[.submit na form]
    D --> D3[Enter w polu]

    E --> E1[Dynamiczne pola]
    E --> E2[Autocomplete]
    E --> E3[Multi-step wizards]
    E --> E4[Czyszczenie formularza]

    style A fill:#f9f,stroke:#333,stroke-width:4px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#bfb,stroke:#333,stroke-width:2px

Materiały:

Jak pracować z elementami dynamicznymi i oczekiwać na ich pojawienie się?

Odpowiedź w 30 sekund:

Cypress automatycznie retry'uje komendy przez 4 sekundy (domyślny timeout), więc elementy dynamiczne są obsługiwane out-of-the-box. Można dostosować timeout przez opcję { timeout: 10000 }. Dla bardziej złożonych scenariuszy używa się .should() z callback, cy.wait() dla requesta API, lub cy.waitUntil() z pluginu dla custom warunków.

Odpowiedź w 2 minuty:

Cypress został zaprojektowany z myślą o aplikacjach Single Page Application (SPA), które dynamicznie ładują i zmieniają content. W przeciwieństwie do innych narzędzi testowych, Cypress ma wbudowany mechanizm automatycznego retry dla większości komend - jeśli element nie istnieje lub nie spełnia warunku, Cypress będzie ponawiać próby przez określony czas (domyślnie 4 sekundy) zanim rzuci błąd.

Ten built-in retry działa dla komend query (.get(), .find(), .contains()) oraz asercji (.should()). Oznacza to, że w większości przypadków nie musisz explicite czekać - wystarczy napisać cy.get('[data-cy="dynamic-element"]').should('be.visible') i Cypress będzie sprawdzać warunek aż się spełni. Możesz dostosować timeout dla konkretnej komendy przez opcję { timeout: 10000 } lub globalnie w konfiguracji.

Dla elementów pojawiających się po zapytaniach API best practice to użycie cy.intercept() do przechwycenia requestu i cy.wait('@alias') do oczekiwania na odpowiedź. To daje pewność, że czekasz na konkretne zdarzenie, a nie arbitralny czas. Możesz również używać cy.waitUntil() z cypress-wait-until plugin dla bardziej zaawansowanych warunków oczekiwania.

Ważne jest unikanie cy.wait(1000) z hardcoded czasem - to anti-pattern prowadzący do niestabilnych testów (flaky tests). Zamiast tego zawsze czekaj na konkretny warunek: pojawienie się elementu, zmianę tekstu, wykonanie requestu API. Cypress oferuje również metody jak .should('have.length.greaterThan', 0) dla dynamicznych list czy .should('not.exist') dla elementów które powinny zniknąć.

Przykład kodu:

describe('Praca z elementami dynamicznymi w Cypress', () => {

  // ========================================
  // AUTOMATYCZNE RETRY (BUILT-IN)
  // ========================================
  it('automatycznie czeka na pojawienie się elementu', () => {
    cy.visit('/dashboard');

    // Element pojawia się po załadowaniu danych - Cypress czeka automatycznie
    cy.get('[data-cy="user-profile"]').should('be.visible');

    // Kliknięcie przycisku który pokazuje modal
    cy.get('[data-cy="open-modal"]').click();

    // Modal pojawia się z animacją - Cypress czeka automatycznie
    cy.get('[data-cy="modal"]').should('be.visible');
    cy.get('[data-cy="modal-title"]').should('contain', 'Witaj');
  });

  // ========================================
  // DOSTOSOWANIE TIMEOUTU
  // ========================================
  it('używa custom timeout dla wolno ładujących się elementów', () => {
    cy.visit('/reports');

    // Element może się ładować do 15 sekund
    cy.get('[data-cy="complex-chart"]', { timeout: 15000 })
      .should('be.visible');

    // Alternatywnie - timeout w should()
    cy.get('[data-cy="data-table"]')
      .should('be.visible', { timeout: 10000 });
  });

  // ========================================
  // CZEKANIE NA REQUESTY API
  // ========================================
  it('czeka na konkretne zapytanie API', () => {
    // Przechwycenie requestu przed wizytą
    cy.intercept('GET', '/api/users*').as('getUsers');

    cy.visit('/users');

    // Czekanie na zakończenie requestu
    cy.wait('@getUsers').then((interception) => {
      // Weryfikacja odpowiedzi
      expect(interception.response.statusCode).to.equal(200);
      expect(interception.response.body).to.have.length.greaterThan(0);
    });

    // Teraz elementy na pewno są załadowane
    cy.get('[data-cy="user-list"] li').should('have.length.greaterThan', 0);
  });

  it('czeka na wiele requestów', () => {
    cy.intercept('GET', '/api/users').as('getUsers');
    cy.intercept('GET', '/api/settings').as('getSettings');
    cy.intercept('GET', '/api/notifications').as('getNotifications');

    cy.visit('/dashboard');

    // Czekanie na wszystkie requesty
    cy.wait(['@getUsers', '@getSettings', '@getNotifications']);

    // Wszystkie dane są załadowane
    cy.get('[data-cy="dashboard-content"]').should('be.visible');
  });

  // ========================================
  // DYNAMICZNE LISTY
  // ========================================
  it('pracuje z dynamicznie ładowaną listą', () => {
    cy.visit('/products');

    // Czekanie aż lista będzie miała przynajmniej 1 element
    cy.get('[data-cy="product-list"] .product-item')
      .should('have.length.greaterThan', 0);

    // Czekanie na konkretną ilość elementów
    cy.get('[data-cy="product-list"] .product-item')
      .should('have.length', 12);

    // Kliknięcie "Załaduj więcej"
    cy.get('[data-cy="load-more"]').click();

    // Czekanie aż liczba elementów wzrośnie
    cy.get('[data-cy="product-list"] .product-item')
      .should('have.length', 24);
  });

  // ========================================
  // INFINITE SCROLL
  // ========================================
  it('obsługuje infinite scroll', () => {
    cy.intercept('GET', '/api/posts?page=*').as('getPosts');

    cy.visit('/feed');

    // Początkowe posty załadowane
    cy.wait('@getPosts');
    cy.get('.post-item').should('have.length', 10);

    // Scroll w dół aby załadować więcej
    cy.scrollTo('bottom');

    // Czekanie na kolejny request
    cy.wait('@getPosts');

    // Sprawdzenie że liczba postów wzrosła
    cy.get('.post-item').should('have.length', 20);

    // Kolejny scroll
    cy.scrollTo('bottom');
    cy.wait('@getPosts');
    cy.get('.post-item').should('have.length', 30);
  });

  // ========================================
  // ELEMENTY ZNIKAJĄCE
  // ========================================
  it('czeka aż element zniknie', () => {
    cy.visit('/page');

    // Loader widoczny na początku
    cy.get('[data-cy="loading-spinner"]').should('be.visible');

    // Czekanie aż loader zniknie
    cy.get('[data-cy="loading-spinner"]').should('not.exist');
    // lub
    cy.get('[data-cy="loading-spinner"]').should('not.be.visible');

    // Teraz content jest widoczny
    cy.get('[data-cy="page-content"]').should('be.visible');
  });

  // ========================================
  // TOAST NOTIFICATIONS
  // ========================================
  it('obsługuje przemijające notyfikacje', () => {
    cy.visit('/dashboard');

    // Akcja wywołująca notyfikację
    cy.get('[data-cy="save-button"]').click();

    // Notyfikacja pojawia się
    cy.get('[data-cy="toast-notification"]')
      .should('be.visible')
      .and('contain', 'Zapisano pomyślnie');

    // Czekanie aż zniknie (auto-hide po 3 sekundach)
    cy.get('[data-cy="toast-notification"]', { timeout: 5000 })
      .should('not.exist');
  });

  // ========================================
  // SEARCH Z DEBOUNCE
  // ========================================
  it('obsługuje search z debounce/throttle', () => {
    cy.intercept('GET', '/api/search?q=*').as('search');

    cy.visit('/search');

    // Wpisanie zapytania
    cy.get('[data-cy="search-input"]').type('Cypress Testing');

    // Czekanie na request (debounce zwykle 300-500ms)
    cy.wait('@search');

    // Wyniki pojawiły się
    cy.get('[data-cy="search-results"]').should('be.visible');
    cy.get('.search-result-item').should('have.length.greaterThan', 0);
  });

  // ========================================
  // WARUNKOWE CZEKANIE NA ELEMENT
  // ========================================
  it('używa should() z funkcją callback', () => {
    cy.visit('/dashboard');

    // Czekanie aż element będzie miał konkretny tekst
    cy.get('[data-cy="status"]').should(($el) => {
      expect($el).to.have.text('Aktywny');
    });

    // Czekanie na konkretną klasę
    cy.get('[data-cy="badge"]').should(($badge) => {
      expect($badge).to.have.class('badge-success');
    });

    // Złożony warunek
    cy.get('[data-cy="counter"]').should(($counter) => {
      const count = parseInt($counter.text());
      expect(count).to.be.greaterThan(0);
      expect(count).to.be.lessThan(100);
    });
  });

  // ========================================
  // SKELETON LOADERS
  // ========================================
  it('czeka aż skeleton loader zostanie zastąpiony danymi', () => {
    cy.visit('/profile');

    // Skeleton loader widoczny
    cy.get('[data-cy="profile-skeleton"]').should('be.visible');
    cy.get('[data-cy="profile-data"]').should('not.exist');

    // Czekanie aż dane się załadują
    cy.get('[data-cy="profile-skeleton"]').should('not.exist');
    cy.get('[data-cy="profile-data"]').should('be.visible');

    // Weryfikacja załadowanych danych
    cy.get('[data-cy="user-name"]').should('not.be.empty');
    cy.get('[data-cy="user-avatar"]').should('have.attr', 'src');
  });

  // ========================================
  // ANIMACJE I TRANSITIONS
  // ========================================
  it('czeka na zakończenie animacji', () => {
    cy.visit('/page');

    // Kliknięcie triggera animacji
    cy.get('[data-cy="expand-section"]').click();

    // Czekanie aż element stanie się widoczny (po animacji)
    cy.get('[data-cy="expanded-content"]')
      .should('be.visible')
      .and('have.css', 'opacity', '1'); // Sprawdzenie że animacja się zakończyła

    // Alternatywnie - czekanie na klasę wskazującą koniec animacji
    cy.get('[data-cy="animated-element"]')
      .should('have.class', 'animation-complete');
  });

  // ========================================
  // UŻYCIE CYPRESS-WAIT-UNTIL (PLUGIN)
  // ========================================
  /*
  // Wymagane: npm install -D cypress-wait-until
  // W cypress/support/e2e.js: import 'cypress-wait-until';

  it('używa waitUntil dla złożonych warunków', () => {
    cy.visit('/realtime-dashboard');

    // Czekanie aż wartość licznika przekroczy 100
    cy.waitUntil(() =>
      cy.get('[data-cy="counter"]').then($el => {
        const value = parseInt($el.text());
        return value > 100;
      }),
      {
        timeout: 10000,
        interval: 500,
        errorMsg: 'Licznik nie osiągnął wartości > 100'
      }
    );

    // Czekanie na pojawienie się konkretnego tekstu w liście
    cy.waitUntil(() =>
      cy.get('[data-cy="messages-list"]').then($list => {
        return $list.text().includes('Nowa wiadomość');
      })
    );
  });
  */

  // ========================================
  // ANTI-PATTERN: UNIKAĆ!
  // ========================================
  it('ZŁE: używanie cy.wait z hardcoded czasem', () => {
    cy.visit('/page');

    // ❌ ZŁE - arbitralny timeout
    cy.wait(3000);
    cy.get('[data-cy="content"]').should('be.visible');

    // ✅ DOBRE - czekanie na konkretny warunek
    cy.get('[data-cy="content"]').should('be.visible');
  });
});

Diagram:

graph TD
    A[Elementy Dynamiczne w Cypress] --> B[Automatyczne Retry]
    A --> C[API Requests]
    A --> D[Timeouty]
    A --> E[Zaawansowane]

    B --> B1[cy.get - retry do 4s]
    B --> B2[.should - retry asercji]
    B --> B3[.find - retry potomków]

    C --> C1[cy.intercept - przechwycenie]
    C --> C2[cy.wait alias - czekanie]
    C --> C3[Weryfikacja response]

    D --> D1[timeout option lokalne]
    D --> D2[defaultCommandTimeout globalne]
    D --> D3[requestTimeout dla API]

    E --> E1[.should callback - warunki]
    E --> E2[cy.waitUntil plugin]
    E --> E3[Dynamiczne listy]
    E --> E4[not.exist - znikanie]

    F[Anti-patterns] --> F1[❌ cy.wait hardcoded]
    F --> F2[✅ Czekaj na warunki]

    style A fill:#f9f,stroke:#333,stroke-width:4px
    style B fill:#bfb,stroke:#333,stroke-width:2px
    style F1 fill:#fbb,stroke:#333,stroke-width:2px
    style F2 fill:#bfb,stroke:#333,stroke-width:2px

Materiały:

Jak obsługiwać dropdown, checkbox i radio button?

Odpowiedź w 30 sekund:

Do obsługi dropdown używa się .select() z tekstem, wartością lub indeksem opcji. Checkboxy zaznacza się przez .check() i odznacza .uncheck(), można również przekazać wartość lub tablicę wartości. Radio buttony obsługuje tylko .check() bo nie można ich odznaczyć. Wszystkie komendy automatycznie scrollują do elementu i weryfikują czy nie jest disabled.

Odpowiedź w 2 minuty:

Cypress oferuje dedykowane komendy do obsługi podstawowych elementów formularzy. Dla tradycyjnych elementów <select> komenda .select() przyjmuje tekst widoczny dla użytkownika, wartość atrybutu value, lub indeks opcji (0-based). Można również wybrać wiele opcji jednocześnie dla elementów multiselect, przekazując tablicę. Cypress automatycznie sprawdza czy element nie jest disabled i czy wybrana opcja istnieje.

Checkboxy obsługuje się przez .check() do zaznaczania i .uncheck() do odznaczania. Komendy te działają zarówno na pojedynczych elementach jak i na grupach checkboxów - można przekazać konkretną wartość lub tablicę wartości do zaznaczenia. Cypress automatycznie pomija elementy już zaznaczone przy .check() i odznaczone przy .uncheck(), dzięki czemu komendy są idempotentne. Weryfikacja stanu odbywa się przez asercje .should('be.checked') lub .should('not.be.checked').

Radio buttony mają specyficzną charakterystykę - można je tylko zaznaczyć (.check()), nie ma komendy .uncheck() ponieważ radio button odznacza się automatycznie gdy zaznaczymy inny z tej samej grupy. Najlepszą praktyką jest targetowanie radio buttonów po atrybucie name i wartości value, lub używanie dedykowanych atrybutów testowych data-cy.

Dla niestandardowych dropdownów (custom select, autocomplete, multi-select) które nie używają natywnego <select>, trzeba stosować standardowe komendy jak .click() i .type(). W takich przypadkach przydatne są komendy .within() do zawężenia kontekstu oraz czekanie na pojawienie się listy opcji. Często wymaga to kombinacji kilku kroków: kliknięcie w trigger, czekanie na listę, kliknięcie w opcję.

Przykład kodu:

describe('Obsługa dropdown, checkbox i radio button', () => {

  // ========================================
  // DROPDOWN (NATYWNY SELECT)
  // ========================================
  it('obsługuje natywny element select na różne sposoby', () => {
    cy.visit('/form');

    // Wybór po tekście widocznym dla użytkownika
    cy.get('[data-cy="country-select"]').select('Polska');

    // Wybór po wartości (value attribute)
    cy.get('[data-cy="country-select"]').select('PL');

    // Wybór po indeksie (0-based, 0 to zwykle placeholder)
    cy.get('[data-cy="month-select"]').select(5); // Czerwiec

    // Weryfikacja wybranej opcji
    cy.get('[data-cy="country-select"]')
      .should('have.value', 'PL');

    // Weryfikacja wyświetlanego tekstu
    cy.get('[data-cy="country-select"] option:selected')
      .should('have.text', 'Polska');
  });

  it('obsługuje multiselect', () => {
    cy.visit('/preferences');

    // Wybór wielu opcji naraz
    cy.get('[data-cy="languages-multiselect"]')
      .select(['pl', 'en', 'de']);

    // Weryfikacja że wszystkie są wybrane
    cy.get('[data-cy="languages-multiselect"]')
      .invoke('val')
      .should('deep.equal', ['pl', 'en', 'de']);

    // Dodanie kolejnej opcji
    cy.get('[data-cy="languages-multiselect"]')
      .select(['pl', 'en', 'de', 'fr']);
  });

  it('weryfikuje dostępne opcje w dropdown', () => {
    cy.visit('/form');

    // Sprawdzenie ilości opcji
    cy.get('[data-cy="city-select"] option')
      .should('have.length', 10);

    // Sprawdzenie konkretnych opcji
    cy.get('[data-cy="city-select"]')
      .find('option')
      .first()
      .should('have.text', '-- Wybierz miasto --');

    // Weryfikacja że opcja istnieje
    cy.get('[data-cy="city-select"]')
      .find('option')
      .contains('Warszawa')
      .should('exist');
  });

  // ========================================
  // CHECKBOXY
  // ========================================
  it('zaznacza i odznacza checkboxy', () => {
    cy.visit('/settings');

    // Zaznaczenie checkboxa
    cy.get('[data-cy="newsletter-checkbox"]').check();

    // Weryfikacja że jest zaznaczony
    cy.get('[data-cy="newsletter-checkbox"]')
      .should('be.checked');

    // Odznaczenie
    cy.get('[data-cy="newsletter-checkbox"]').uncheck();

    // Weryfikacja że nie jest zaznaczony
    cy.get('[data-cy="newsletter-checkbox"]')
      .should('not.be.checked');
  });

  it('obsługuje grupę checkboxów', () => {
    cy.visit('/permissions');

    // Zaznaczenie wszystkich checkboxów w grupie
    cy.get('[data-cy="permissions"] input[type="checkbox"]')
      .check();

    // Weryfikacja że wszystkie są zaznaczone
    cy.get('[data-cy="permissions"] input[type="checkbox"]')
      .should('be.checked');

    // Odznaczenie wszystkich
    cy.get('[data-cy="permissions"] input[type="checkbox"]')
      .uncheck();

    // Zaznaczenie konkretnych checkboxów po wartości
    cy.get('[data-cy="permissions"] input[type="checkbox"]')
      .check(['read', 'write']); // value attributes

    // Weryfikacja konkretnych stanów
    cy.get('input[value="read"]').should('be.checked');
    cy.get('input[value="write"]').should('be.checked');
    cy.get('input[value="delete"]').should('not.be.checked');
  });

  it('obsługuje checkbox z labelką klikalną', () => {
    cy.visit('/form');

    // Kliknięcie w label również zaznacza checkbox
    cy.get('label[for="terms-checkbox"]').click();

    // Weryfikacja
    cy.get('#terms-checkbox').should('be.checked');

    // Lub bezpośrednio przez data-cy na checkboxie
    cy.get('[data-cy="privacy-checkbox"]').check();
  });

  it('weryfikuje disabled state checkboxa', () => {
    cy.visit('/form');

    // Sprawdzenie że checkbox jest disabled
    cy.get('[data-cy="admin-only-checkbox"]')
      .should('be.disabled');

    // Próba zaznaczenia disabled checkboxa zakończy się błędem
    // cy.get('[data-cy="admin-only-checkbox"]').check(); // ❌ Rzuci błąd

    // Force pominie sprawdzanie disabled (niezalecane)
    cy.get('[data-cy="admin-only-checkbox"]')
      .check({ force: true });
  });

  // ========================================
  // RADIO BUTTONY
  // ========================================
  it('obsługuje radio buttony', () => {
    cy.visit('/survey');

    // Zaznaczenie radio buttona
    cy.get('[data-cy="satisfaction-good"]').check();

    // Weryfikacja że jest zaznaczony
    cy.get('[data-cy="satisfaction-good"]')
      .should('be.checked');

    // Zaznaczenie innego radio z tej samej grupy
    cy.get('[data-cy="satisfaction-excellent"]').check();

    // Sprawdzenie że poprzedni został automatycznie odznaczony
    cy.get('[data-cy="satisfaction-good"]')
      .should('not.be.checked');
    cy.get('[data-cy="satisfaction-excellent"]')
      .should('be.checked');
  });

  it('wybiera radio button po wartości', () => {
    cy.visit('/survey');

    // Wybór radio po nazwie grupy i wartości
    cy.get('input[name="payment-method"]')
      .check('credit-card');

    // Weryfikacja
    cy.get('input[name="payment-method"]:checked')
      .should('have.value', 'credit-card');

    // Zmiana wyboru
    cy.get('input[name="payment-method"]')
      .check('paypal');

    cy.get('input[name="payment-method"]:checked')
      .should('have.value', 'paypal');
  });

  it('weryfikuje wszystkie opcje radio w grupie', () => {
    cy.visit('/quiz');

    // Sprawdzenie ilości opcji
    cy.get('input[name="question-1"]')
      .should('have.length', 4);

    // Sprawdzenie że żadna nie jest zaznaczona początkowo
    cy.get('input[name="question-1"]:checked')
      .should('not.exist');

    // Zaznaczenie odpowiedzi
    cy.get('input[name="question-1"][value="B"]').check();

    // Weryfikacja że tylko jedna jest zaznaczona
    cy.get('input[name="question-1"]:checked')
      .should('have.length', 1)
      .and('have.value', 'B');
  });

  // ========================================
  // CUSTOM DROPDOWN (niestandardowy)
  // ========================================
  it('obsługuje custom dropdown (nie natywny select)', () => {
    cy.visit('/advanced-form');

    // Kliknięcie w trigger custom dropdowna
    cy.get('[data-cy="custom-select-trigger"]').click();

    // Czekanie na pojawienie się listy opcji
    cy.get('[data-cy="custom-select-options"]')
      .should('be.visible');

    // Kliknięcie w wybraną opcję
    cy.get('[data-cy="custom-select-options"]')
      .contains('Warszawa')
      .click();

    // Weryfikacja że dropdown się zamknął
    cy.get('[data-cy="custom-select-options"]')
      .should('not.be.visible');

    // Weryfikacja wybranej wartości
    cy.get('[data-cy="custom-select-trigger"]')
      .should('contain', 'Warszawa');
  });

  it('obsługuje searchable custom dropdown', () => {
    cy.visit('/advanced-form');

    // Otwarcie dropdowna
    cy.get('[data-cy="country-dropdown"]').click();

    // Wpisanie w pole wyszukiwania
    cy.get('[data-cy="country-search-input"]')
      .type('Pol');

    // Czekanie na przefiltrowane wyniki
    cy.get('[data-cy="country-options"] li')
      .should('have.length', 1)
      .and('contain', 'Polska');

    // Kliknięcie w wynik
    cy.get('[data-cy="country-options"] li')
      .first()
      .click();
  });

  // ========================================
  // CUSTOM CHECKBOX/TOGGLE
  // ========================================
  it('obsługuje custom checkbox (toggle switch)', () => {
    cy.visit('/settings');

    // Custom checkbox może wyglądać jak toggle switch
    // Sprawdzenie stanu przed kliknięciem
    cy.get('[data-cy="notifications-toggle"]')
      .should('have.attr', 'aria-checked', 'false');

    // Kliknięcie w toggle
    cy.get('[data-cy="notifications-toggle"]').click();

    // Weryfikacja zmiany stanu
    cy.get('[data-cy="notifications-toggle"]')
      .should('have.attr', 'aria-checked', 'true');

    // Lub sprawdzenie klasy CSS
    cy.get('[data-cy="notifications-toggle"]')
      .should('have.class', 'toggle-on');
  });

  // ========================================
  // AUTOCOMPLETE (COMBOBOX)
  // ========================================
  it('obsługuje autocomplete/combobox', () => {
    cy.visit('/booking');

    // Kliknięcie w pole autocomplete
    cy.get('[data-cy="destination-autocomplete"]').click();

    // Wpisanie tekstu
    cy.get('[data-cy="destination-autocomplete"]')
      .type('Warsz');

    // Czekanie na sugestie
    cy.get('[data-cy="autocomplete-suggestions"]')
      .should('be.visible');

    // Sprawdzenie że sugestie zawierają wpisany tekst
    cy.get('[data-cy="autocomplete-suggestions"] li')
      .should('have.length.greaterThan', 0)
      .first()
      .should('contain', 'Warszawa');

    // Wybór sugestii
    cy.get('[data-cy="autocomplete-suggestions"]')
      .contains('Warszawa, Polska')
      .click();

    // Weryfikacja wybranej wartości
    cy.get('[data-cy="destination-autocomplete"]')
      .should('have.value', 'Warszawa, Polska');
  });

  // ========================================
  // MULTI-SELECT Z TAGAMI
  // ========================================
  it('obsługuje multi-select z tagami', () => {
    cy.visit('/project-settings');

    // Kliknięcie w multi-select
    cy.get('[data-cy="tags-multiselect"]').click();

    // Wybór pierwszego taga
    cy.get('[data-cy="tag-options"]')
      .contains('JavaScript')
      .click();

    // Wybór drugiego taga (dropdown pozostaje otwarty)
    cy.get('[data-cy="tag-options"]')
      .contains('React')
      .click();

    // Weryfikacja wybranych tagów
    cy.get('[data-cy="selected-tags"] .tag')
      .should('have.length', 2);

    cy.get('[data-cy="selected-tags"]')
      .should('contain', 'JavaScript')
      .and('contain', 'React');

    // Usunięcie taga
    cy.get('[data-cy="selected-tags"] .tag')
      .contains('JavaScript')
      .find('.remove-tag')
      .click();

    // Weryfikacja że został 1 tag
    cy.get('[data-cy="selected-tags"] .tag')
      .should('have.length', 1);
  });

  // ========================================
  // WITHIN DLA KONTEKSTU
  // ========================================
  it('używa within() dla skomplikowanych struktur', () => {
    cy.visit('/complex-form');

    // Praca w kontekście konkretnego fieldset
    cy.get('[data-cy="shipping-address"]').within(() => {
      cy.get('[data-cy="country"]').select('Polska');
      cy.get('[data-cy="city"]').select('Kraków');
      cy.get('[data-cy="express-delivery"]').check();
    });

    // Praca w kontekście innego fieldset
    cy.get('[data-cy="billing-address"]').within(() => {
      cy.get('[data-cy="same-as-shipping"]').check();
    });
  });
});

Diagram:

graph TD
    A[Elementy Formularzy] --> B[Natywne Elementy]
    A --> C[Custom Elementy]

    B --> B1[Select]
    B --> B2[Checkbox]
    B --> B3[Radio Button]

    B1 --> B1A[.select text/value/index]
    B1 --> B1B[Multiselect z tablicą]
    B1 --> B1C[Weryfikacja have.value]

    B2 --> B2A[.check - zaznacz]
    B2 --> B2B[.uncheck - odznacz]
    B2 --> B2C[Weryfikacja be.checked]
    B2 --> B2D[Wiele: check array]

    B3 --> B3A[.check tylko - no uncheck]
    B3 --> B3B[Auto-odznaczanie innych]
    B3 --> B3C[Po name i value]

    C --> C1[Custom Dropdown]
    C --> C2[Autocomplete]
    C --> C3[Toggle Switch]
    C --> C4[Multi-select Tags]

    C1 --> C1A[.click trigger]
    C1 --> C1B[.click opcja]
    C1 --> C1C[Weryfikacja be.visible]

    style A fill:#f9f,stroke:#333,stroke-width:4px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#fdb,stroke:#333,stroke-width:2px

Materiały:

Asercje i Oczekiwania

Jakie rodzaje asercji są dostępne w Cypress?

Odpowiedź w 30 sekund

Cypress oferuje dwa główne style asercji: BDD (Chai-BDD) z metodami should() i and() oraz TDD (Chai-Assert) z funkcją expect() i assert(). Najczęściej używa się składni BDD should(), która automatycznie korzysta z mechanizmu retry. Asercje mogą dotyczyć elementów DOM, obiektów JavaScript, odpowiedzi API oraz stanu aplikacji.

Odpowiedź w 2 minuty

Cypress integruje kilka bibliotek asercyjnych, oferując bogaty zestaw możliwości testowania. Podstawą są asercje Chai w dwóch stylach: BDD (Behavior-Driven Development) i TDD (Test-Driven Development). Styl BDD wykorzystuje metody should() i and() bezpośrednio na łańcuchu komend Cypress, co zapewnia automatyczny retry i lepszą czytelność testów. Dostępne są assertery jak be.visible, have.text, contain, have.class, have.attr i wiele innych.

Styl TDD używa funkcji expect() i assert(), które są przydatne w callbackach i przy testowaniu wartości JavaScript bezpośrednio. Te asercje nie mają wbudowanego retry, więc wykonują się natychmiast. Cypress dodaje również własne assertery specyficzne dla testowania webowego, takie jak be.checked, be.disabled, be.focused, have.value.

Dodatkowo Cypress oferuje asercje jQuery poprzez should('have.length'), should('exist') oraz asercje Sinon-Chai do testowania spyów i stubów (have.been.called, have.been.calledWith). Można również tworować własne assertery używając Chai.Assertion.addMethod().

Wybór odpowiedniego stylu asercji zależy od kontekstu: should() dla elementów DOM i komend Cypress (z retry), expect() dla logiki JavaScript w callbackach (bez retry), oraz assert() dla bardziej złożonych warunków logicznych.

Przykład kodu

describe('Rodzaje asercji w Cypress', () => {
  it('BDD style - should() z automatycznym retry', () => {
    cy.visit('https://example.com')

    // Asercje Chai-BDD dla elementów DOM
    cy.get('h1')
      .should('be.visible')
      .and('have.text', 'Witaj w aplikacji')
      .and('have.class', 'header-title')
      .and('have.css', 'color', 'rgb(0, 0, 0)')

    // Asercje dla atrybutów i właściwości
    cy.get('input[type="email"]')
      .should('have.attr', 'placeholder', 'Wpisz email')
      .and('have.value', '')
      .and('be.enabled')
      .and('not.be.disabled')

    // Asercje dla list elementów
    cy.get('.menu-item')
      .should('have.length', 5)
      .and('contain', 'Start')
  })

  it('TDD style - expect() bez retry', () => {
    cy.visit('https://example.com')

    // expect() w callbacku - dla wartości JavaScript
    cy.get('button').then(($btn) => {
      const text = $btn.text()
      expect(text).to.equal('Kliknij mnie')
      expect(text).to.have.length.greaterThan(5)
      expect($btn).to.have.class('btn-primary')
    })

    // Asercje dla obiektów i tablic
    cy.wrap({ name: 'Jan', age: 30 }).then((user) => {
      expect(user).to.have.property('name', 'Jan')
      expect(user).to.deep.equal({ name: 'Jan', age: 30 })
      expect(user.age).to.be.a('number')
    })

    cy.wrap([1, 2, 3]).then((arr) => {
      expect(arr).to.have.length(3)
      expect(arr).to.include(2)
      expect(arr).to.deep.equal([1, 2, 3])
    })
  })

  it('Asercje dla stanu aplikacji', () => {
    cy.visit('https://example.com/login')

    // URL
    cy.url()
      .should('include', '/login')
      .and('match', /\/login\?.*/)

    // Cookies
    cy.getCookie('session')
      .should('exist')
      .and('have.property', 'value')
      .and('match', /^[a-z0-9]+$/)

    // LocalStorage
    cy.window().then((win) => {
      expect(win.localStorage.getItem('theme')).to.equal('dark')
    })
  })

  it('Asercje dla API', () => {
    cy.request('GET', '/api/users/1')
      .should((response) => {
        expect(response.status).to.equal(200)
        expect(response.body).to.have.property('id', 1)
        expect(response.body.name).to.be.a('string')
        expect(response.headers).to.have.property('content-type')
      })
  })

  it('Asercje Sinon-Chai dla spyów', () => {
    cy.visit('https://example.com')

    // Spy na metodę window.alert
    cy.window().then((win) => {
      cy.spy(win, 'alert').as('alertSpy')
    })

    cy.get('#show-alert-btn').click()

    cy.get('@alertSpy')
      .should('have.been.calledOnce')
      .and('have.been.calledWith', 'Ważna wiadomość!')
  })

  it('Własne assertery', () => {
    // Dodanie własnego assertera
    Chai.Assertion.addMethod('polishPhoneNumber', function() {
      const obj = this._obj
      const regex = /^\+48\d{9}$/

      this.assert(
        regex.test(obj),
        'oczekiwano poprawnego numeru telefonu polskiego',
        'nie oczekiwano poprawnego numeru telefonu polskiego',
        '+48XXXXXXXXX',
        obj
      )
    })

    // Użycie własnego assertera
    cy.wrap('+48123456789').should('be.polishPhoneNumber')
  })

  it('Asercje negatywne', () => {
    cy.get('button')
      .should('not.be.disabled')
      .and('not.have.class', 'hidden')
      .and('not.contain', 'Anuluj')
  })
})

Diagram

graph TB
    A[Asercje w Cypress] --> B[Chai-BDD Style]
    A --> C[Chai-TDD Style]
    A --> D[Własne Assertery]

    B --> B1[should/and<br/>z retry]
    B --> B2[be.visible]
    B --> B3[have.text]
    B --> B4[have.class]
    B --> B5[have.attr]

    C --> C1[expect<br/>bez retry]
    C --> C2[assert<br/>bez retry]
    C --> C3[Użycie w then]

    B1 --> E[Elementy DOM]
    B1 --> F[Komendy Cypress]

    C1 --> G[Wartości JS]
    C1 --> H[Obiekty]
    C1 --> I[Tablice]

    D --> D1[Chai.Assertion.addMethod]
    D --> D2[Własna logika]

    J[Biblioteki] --> K[Chai]
    J --> L[jQuery]
    J --> M[Sinon-Chai]

    K --> B
    K --> C
    L --> B2
    M --> N[have.been.called]

    style A fill:#e1f5ff
    style B1 fill:#c3f0c3
    style C1 fill:#ffe6cc

Materiały

Jak działa mechanizm retry w Cypress?

Odpowiedź w 30 sekund

Mechanizm retry w Cypress automatycznie powtarza komendy i asercje przez określony czas (domyślnie 4 sekundy), czekając aż warunek zostanie spełniony. Dotyczy to komend query (np. cy.get(), cy.find()) połączonych z asercjami, ale nie dotyczy komend akcji (np. cy.click()). Dzięki retry testy są bardziej stabilne i nie wymagają ręcznego dodawania czekania.

Odpowiedź w 2 minuty

Mechanizm retry-ability jest jedną z kluczowych cech Cypress, która odróżnia go od tradycyjnych narzędzi testowych. Cypress automatycznie ponawia próby wykonania komend query i asercji, dopóki nie zakończą się sukcesem lub nie upłynie timeout (domyślnie 4000ms, konfigurowalny przez defaultCommandTimeout). Mechanizm ten rozwiązuje problem asynchroniczności aplikacji webowych bez potrzeby ręcznego dodawania waitów i sleep.

Retry działa na zasadzie inteligentnej pętli: Cypress najpierw wykonuje komendę query (np. cy.get('.button')), następnie sprawdza wszystkie powiązane asercje (np. .should('be.visible')). Jeśli którakolwiek asercja nie jest spełniona, Cypress cofa się do początku łańcucha query, czeka krótką chwilę i próbuje ponownie. Proces ten powtarza się aż do sukcesu lub timeoutu.

Ważne jest rozróżnienie między komendami, które mają retry a tymi, które go nie mają. Komendy query (get, find, contains, its) są retry-able i będą ponawiane razem z asercjami. Komendy akcji (click, type, select) wykonują się tylko raz i nie są ponawiane - jeśli akcja nie może być wykonana, test natychmiast failuje.

Mechanizm retry można kontrolować poprzez timeouty: globalnie w cypress.config.js, per-komenda przez parametr timeout, lub per-test przez Cypress.config(). Można również tworzyć własne warunki retry używając should() z funkcją callback, która zwraca asercje. To pozwala na tworzenie złożonych warunków oczekiwania bez sztywnych sleep-ów.

Przykład kodu

describe('Mechanizm retry w Cypress', () => {
  beforeEach(() => {
    cy.visit('https://example.com')
  })

  it('Automatyczny retry dla komend query + asercje', () => {
    // Element pojawi się po 2 sekundach, ale test przejdzie
    // bo Cypress będzie retry przez 4 sekundy (domyślny timeout)
    cy.get('.delayed-element')
      .should('be.visible')
      .and('contain', 'Załadowano!')

    // Cypress automatycznie czeka aż element spełni wszystkie warunki
    cy.get('.loading-spinner').should('not.exist')
    cy.get('.content').should('be.visible')
  })

  it('Łańcuch komend query - wszystkie są retry', () => {
    // Cały łańcuch jest ponnawiany dopóki ostatnia asercja nie przejdzie
    cy.get('.user-list')           // Query 1 - retry
      .find('.user-item')          // Query 2 - retry
      .first()                     // Query 3 - retry
      .should('contain', 'Jan')    // Asercja - trigger retry

    // Jeśli asercja failuje, Cypress wraca do cy.get('.user-list')
  })

  it('Komendy akcji NIE mają retry', () => {
    // Click wykonuje się tylko raz - jeśli button nie istnieje, test failuje
    cy.get('.submit-btn').click() // Akcja - brak retry!

    // Poprawne podejście - najpierw asercja (retry), potem akcja
    cy.get('.submit-btn')
      .should('be.visible')        // Retry aż będzie widoczny
      .and('be.enabled')           // Retry aż będzie aktywny
      .click()                     // Dopiero teraz klik (bez retry)
  })

  it('Konfiguracja timeoutów', () => {
    // Timeout dla konkretnej komendy - 10 sekund
    cy.get('.slow-loading-element', { timeout: 10000 })
      .should('exist')

    // Timeout dla asercji
    cy.get('.dynamic-content')
      .should('contain', 'Dane załadowane', { timeout: 8000 })
  })

  it('Własny warunek retry z callback', () => {
    // Złożony warunek - czekaj aż lista ma 5 elementów
    cy.get('.item-list').should(($list) => {
      expect($list.find('.item')).to.have.length(5)
    })

    // Czekaj aż element ma konkretny tekst (ładowany asynchronicznie)
    cy.get('.counter').should(($counter) => {
      const count = parseInt($counter.text())
      expect(count).to.be.greaterThan(0)
      expect(count).to.be.lessThan(100)
    })
  })

  it('Retry dla właściwości i atrybutów', () => {
    // Czekaj aż atrybut zmieni wartość
    cy.get('button')
      .should('have.attr', 'data-status', 'ready')

    // Czekaj aż klasa zostanie dodana
    cy.get('.notification')
      .should('have.class', 'success')

    // Czekaj aż tekst się zmieni
    cy.get('.message')
      .should('have.text', 'Operacja zakończona')
  })

  it('Retry z alias i then()', () => {
    // Alias nie powoduje retry!
    cy.get('.user-name').as('userName')

    // To NIE będzie retry - alias jest już zapisany
    cy.get('@userName').should('contain', 'Jan') // ❌ Może failować

    // Poprawne podejście - bezpośrednie query z asercją
    cy.get('.user-name').should('contain', 'Jan') // ✅ Retry działa
  })

  it('Retry dla wartości zwracanych przez then()', () => {
    // then() przerywa retry! Wartość jest "zablokowana"
    cy.get('.price').then(($price) => {
      const value = $price.text() // Wartość pobrana w tym momencie
      expect(value).to.equal('100 zł') // Brak retry jeśli fail
    })

    // Lepsze podejście - invoke z retry
    cy.get('.price')
      .invoke('text')
      .should('equal', '100 zł') // Retry działa!
  })

  it('Retry dla API responses', () => {
    // Intercept i wait mają swoje timeouty
    cy.intercept('GET', '/api/data').as('getData')

    cy.visit('/dashboard')

    // Czekaj max 10 sekund na request
    cy.wait('@getData', { timeout: 10000 })
      .its('response.statusCode')
      .should('equal', 200)
  })

  it('Kontrola retry dla złożonych scenariuszy', () => {
    // Użyj should() z callback dla złożonej logiki
    cy.get('.data-table').should(($table) => {
      // Ta funkcja będzie wywoływana wielokrotnie (retry)
      const rows = $table.find('tr')

      // Wszystkie te warunki muszą być spełnione
      expect(rows).to.have.length.greaterThan(0)
      expect(rows.first()).to.contain('Nagłówek')

      // Sprawdź zawartość pierwszego wiersza danych
      const firstDataRow = rows.eq(1)
      expect(firstDataRow.find('td')).to.have.length(3)
    })
  })

  it('Wyłączenie retry - natychmiastowa asercja', () => {
    cy.visit('/page')

    // Użyj then() + expect() dla natychmiastowej asercji (bez retry)
    cy.get('.counter').then(($el) => {
      const count = parseInt($el.text())
      expect(count).to.equal(0) // Failuje natychmiast jeśli nie 0
    })
  })

  it('Debugowanie retry', () => {
    // Dodaj log aby zobaczyć ile razy retry był wykonany
    cy.get('.dynamic-element').should(($el) => {
      console.log('Retry próba:', $el.text())
      expect($el.text()).to.contain('Gotowe')
    })
  })
})

// Globalna konfiguracja timeoutów
// cypress.config.js
// {
//   defaultCommandTimeout: 4000,      // Domyślny timeout dla komend
//   requestTimeout: 5000,             // Timeout dla cy.request()
//   responseTimeout: 30000,           // Timeout dla odpowiedzi
//   pageLoadTimeout: 60000,           // Timeout dla ładowania strony
//   execTimeout: 60000                // Timeout dla cy.exec()
// }

Diagram

sequenceDiagram
    participant T as Test
    participant C as Cypress
    participant D as DOM

    T->>C: cy.get('.button')
    C->>D: Szukaj elementu
    D-->>C: Element znaleziony

    C->>C: Sprawdź asercje
    Note over C: should('be.visible')

    alt Element niewidoczny (retry)
        C->>C: Czekaj 50ms
        C->>D: Ponów query
        D-->>C: Element znaleziony
        C->>C: Sprawdź asercje
        Note over C: Retry aż timeout<br/>lub sukces
    else Element widoczny
        C-->>T: Sukces ✓
    end

    alt Timeout przekroczony
        C-->>T: Błąd ✗
        Note over T: AssertionError:<br/>expected element<br/>to be visible
    end

    Note over C,D: Komendy Query:<br/>get, find, contains<br/>→ RETRY ✓

    Note over C,D: Komendy Akcji:<br/>click, type, select<br/>→ BRAK RETRY ✗

Materiały

Czym jest implicit waiting w Cypress?

Odpowiedź w 30 sekund

Implicit waiting (czekanie niejawne) to wbudowany w Cypress mechanizm automatycznego oczekiwania na elementy i warunki, bez potrzeby ręcznego dodawania wait() lub sleep(). Cypress domyślnie czeka do 4 sekund na każdą komendę query i asercję, automatycznie ponnawiając próby aż warunek zostanie spełniony. Dzięki temu testy są bardziej niezawodne i nie zawierają sztywnych opóźnień.

Odpowiedź w 2 minuty

Implicit waiting to fundamentalna cecha architektury Cypress, która automatycznie czeka na elementy DOM, asercje i warunki aplikacji bez wymagania od programisty ręcznego dodawania czekania. W przeciwieństwie do tradycyjnych narzędzi jak Selenium, gdzie trzeba używać explicit wait lub implicit wait z konkretnymi wartościami czasowymi, Cypress inteligentnie zarządza oczekiwaniem automatycznie.

Mechanizm działa na kilku poziomach. Po pierwsze, wszystkie komendy query (jak cy.get(), cy.find(), cy.contains()) automatycznie czekają na pojawienie się elementu w DOM przez czas określony w defaultCommandTimeout (domyślnie 4000ms). Po drugie, asercje są automatycznie ponawiane dopóki nie zostaną spełnione lub nie upłynie timeout. Po trzecie, komendy akcji czekają na możliwość interakcji z elementem (czy jest widoczny, czy nie jest zasłonięty, czy nie jest disabled).

Cypress automatycznie czeka również na zakończenie animacji CSS przed wykonaniem akcji, na routing przejścia między stronami, na zakończenie requestów sieciowych (gdy używamy cy.wait() z aliasem), oraz na stabilizację DOM po zmianach. To wszystko dzieje się "za kulisami" bez dodatkowego kodu.

Implicit waiting eliminuje problemy typowe dla testów E2E: flaky testy wynikające z race conditions, niepotrzebne sleep() które spowalniają testy, oraz trudne do debugowania timeouty. Programista może skupić się na logice testu, a Cypress zadba o synchronizację z aplikacją. W rzadkich przypadkach, gdy potrzebujemy bardziej precyzyjnej kontroli, możemy użyć explicit waiting poprzez cy.wait(alias) lub własne funkcje retry w should().

Przykład kodu

describe('Implicit waiting w Cypress', () => {
  it('Automatyczne czekanie na elementy DOM', () => {
    cy.visit('https://example.com')

    // Cypress czeka do 4 sekund aż element się pojawi
    // Nie trzeba dodawać cy.wait() ani sleep()
    cy.get('.lazy-loaded-content')
      .should('be.visible')

    // Nawet jeśli element pojawi się po 3 sekundach, test przejdzie
    cy.get('.delayed-button').click()
  })

  it('Czekanie na warunki asercji', () => {
    cy.visit('https://example.com/counter')

    cy.get('#start-btn').click()

    // Cypress automatycznie czeka aż licznik osiągnie 10
    // Ponawia sprawdzenie co ~50ms przez 4 sekundy
    cy.get('.counter-value')
      .should('have.text', '10')

    // Bez implicit waiting musielibyśmy napisać:
    // cy.wait(5000) // ❌ Złe - sztywne czekanie
    // cy.get('.counter-value').should('have.text', '10')
  })

  it('Czekanie na widoczność i interakcję', () => {
    cy.visit('https://example.com')

    // Cypress automatycznie czeka aż:
    // 1. Element istnieje w DOM
    // 2. Element jest widoczny
    // 3. Element nie jest disabled
    // 4. Element nie jest zasłonięty przez inny element
    // 5. Animacje CSS się zakończą
    cy.get('button.submit')
      .click() // Click wykona się tylko gdy wszystkie warunki spełnione
  })

  it('Czekanie na zmiany stanu', () => {
    cy.visit('https://example.com/form')

    cy.get('input[name="email"]').type('test@example.com')
    cy.get('form').submit()

    // Automatyczne czekanie na pojawienie się komunikatu
    cy.get('.success-message')
      .should('be.visible')
      .and('contain', 'Email wysłany')

    // Czekanie aż klasa zostanie dodana
    cy.get('.form-container')
      .should('have.class', 'submitted')
  })

  it('Czekanie na zakończenie animacji', () => {
    cy.visit('https://example.com')

    cy.get('.menu-toggle').click()

    // Cypress czeka aż animacja slidowania menu się zakończy
    // przed wykonaniem click na elemencie wewnątrz
    cy.get('.menu-item')
      .contains('Profil')
      .click() // Wykona się po zakończeniu animacji
  })

  it('Czekanie na routing i nawigację', () => {
    cy.visit('https://example.com')

    cy.get('a[href="/dashboard"]').click()

    // Cypress automatycznie czeka na:
    // - Załadowanie nowej strony
    // - Event 'load'
    // - Stabilizację DOM
    cy.url().should('include', '/dashboard')
    cy.get('.dashboard-header').should('be.visible')
  })

  it('Czekanie na requesty sieciowe', () => {
    cy.intercept('GET', '/api/users').as('getUsers')

    cy.visit('https://example.com/users')

    // Explicit wait dla konkretnego requestu
    cy.wait('@getUsers')
      .its('response.statusCode')
      .should('equal', 200)

    // Teraz implicit wait dla elementów UI
    cy.get('.user-list .user-item')
      .should('have.length.greaterThan', 0)
  })

  it('Czekanie na wielokrotne zmiany', () => {
    cy.visit('https://example.com/live-data')

    // Cypress będzie sprawdzać warunek co ~50ms
    // aż zostanie spełniony lub timeout (4s)
    cy.get('.status-indicator')
      .should(($el) => {
        const status = $el.text()
        expect(status).to.be.oneOf(['Ready', 'Active', 'Complete'])
        expect($el).to.have.class('status-ok')
      })
  })

  it('Dostosowanie timeoutu dla wolniejszych operacji', () => {
    cy.visit('https://example.com')

    // Zwiększenie timeoutu dla konkretnej operacji
    cy.get('.slow-loading-chart', { timeout: 10000 })
      .should('be.visible')

    // Lub dla asercji
    cy.get('.data-table')
      .should('contain', 'Dane załadowane', { timeout: 15000 })
  })

  it('Implicit wait vs explicit wait', () => {
    cy.visit('https://example.com')

    // ❌ ZŁE - sztywne czekanie (explicit)
    cy.wait(3000) // Zawsze czeka 3s, nawet jeśli element jest już gotowy
    cy.get('.content').should('be.visible')

    // ✅ DOBRE - implicit waiting
    cy.get('.content').should('be.visible') // Czeka tylko tyle ile potrzeba

    // ✅ DOBRE - explicit wait na konkretny request
    cy.intercept('GET', '/api/data').as('getData')
    cy.wait('@getData') // Czeka konkretnie na ten request
  })

  it('Czekanie na localStorage i cookies', () => {
    cy.visit('https://example.com/login')

    cy.get('input[name="username"]').type('user')
    cy.get('input[name="password"]').type('pass')
    cy.get('button[type="submit"]').click()

    // Implicit wait dla cookie
    cy.getCookie('session')
      .should('exist')
      .and('have.property', 'value')

    // Wait dla localStorage (przez własną komendę)
    cy.window().its('localStorage.token')
      .should('exist')
  })

  it('Czekanie w łańcuchu komend', () => {
    cy.visit('https://example.com/products')

    // Każdy krok ma implicit waiting
    cy.get('.product-list')          // Czeka na listę
      .find('.product-item')         // Czeka na elementy
      .first()                       // Czeka aż first() jest dostępny
      .find('.add-to-cart')          // Czeka na przycisk
      .should('be.visible')          // Czeka aż widoczny
      .and('not.be.disabled')        // Czeka aż aktywny
      .click()                       // Wykonuje click

    cy.get('.cart-badge')
      .should('contain', '1')        // Czeka aż licznik się zaktualizuje
  })

  it('Czekanie z warunkami alternatywnymi', () => {
    cy.visit('https://example.com')

    // Czekaj na jeden z możliwych stanów
    cy.get('.notification').should(($notification) => {
      const text = $notification.text()
      // Akceptujemy kilka możliwych stanów
      expect(text).to.match(/Sukces|Ostrzeżenie|Info/)
    })
  })

  it('Brak czekania dla then() - snapshot wartości', () => {
    cy.visit('https://example.com')

    // then() przerywa retry - wartość jest "zamrożona"
    cy.get('.price').then(($price) => {
      const value = parseFloat($price.text())
      // Ten expect NIE ma implicit waiting!
      expect(value).to.be.greaterThan(0) // Failuje natychmiast jeśli false
    })

    // Aby zachować implicit waiting, użyj invoke() + should()
    cy.get('.price')
      .invoke('text')
      .then(parseFloat)
      .should('be.greaterThan', 0) // MA implicit waiting
  })
})

// Przykład konfiguracji globalnych timeoutów
// cypress.config.js
/*
export default {
  e2e: {
    defaultCommandTimeout: 4000,    // Domyślne czekanie dla komend query
    requestTimeout: 5000,            // Timeout dla cy.request()
    responseTimeout: 30000,          // Timeout dla odpowiedzi sieciowych
    pageLoadTimeout: 60000,          // Timeout dla cy.visit()
    execTimeout: 60000,              // Timeout dla cy.exec()
  }
}
*/

// Własna komenda z implicit waiting
Cypress.Commands.add('waitForText', (selector, text, timeout = 4000) => {
  // Ta komenda używa wbudowanego implicit waiting
  cy.get(selector, { timeout }).should('contain', text)
})

// Użycie
// cy.waitForText('.message', 'Załadowano', 8000)

Diagram

graph TB
    subgraph "Cypress Implicit Waiting"
        A[Wykonaj komendę] --> B{Element<br/>istnieje?}
        B -->|NIE| C[Czekaj 50ms]
        C --> B
        B -->|TAK| D{Asercje<br/>spełnione?}
        D -->|NIE| E{Timeout<br/>przekroczony?}
        E -->|NIE| C
        E -->|TAK| F[BŁĄD ✗]
        D -->|TAK| G[SUKCES ✓]
    end

    subgraph "Tradycyjne narzędzia (np. Selenium)"
        H[Wykonaj komendę] --> I{Element<br/>istnieje?}
        I -->|NIE| J[BŁĄD ✗]
        I -->|TAK| K[SUKCES ✓]

        L[Sleep 3000ms] --> M[Wykonaj komendę]
        M --> N[...]
    end

    subgraph "Automatyczne czekanie na"
        O[Elementy DOM]
        P[Warunki asercji]
        Q[Widoczność]
        R[Zakończenie animacji]
        S[Brak zasłonięcia]
        T[Stan enabled]
    end

    style G fill:#90EE90
    style F fill:#FFB6C6
    style A fill:#E1F5FF
    style H fill:#FFE6CC

Materiały

Jak używać should() vs expect() w asercjach?

Odpowiedź w 30 sekund

should() to metoda łańcuchowa Cypress z automatycznym retry, używana głównie dla elementów DOM i komend Cypress. expect() to funkcja Chai bez retry, używana w callbackach then() dla wartości JavaScript. Używaj should() domyślnie dla testowania UI (retry działa), a expect() wewnątrz then() dla asercji na konkretnych wartościach (bez retry, natychmiastowe sprawdzenie).

Odpowiedź w 2 minuty

Główna różnica między should() a expect() leży w mechanizmie retry i sposobie użycia. should() jest metodą Cypress, która działa na łańcuchu komend i automatycznie korzysta z mechanizmu retry-ability. Oznacza to, że asercje z should() są sprawdzane wielokrotnie przez określony czas (domyślnie 4 sekundy), co czyni je idealnymi do testowania dynamicznych elementów DOM i stanu aplikacji. Składnia to cy.get('.element').should('be.visible').

expect() natomiast to funkcja z biblioteki Chai, która wykonuje asercję natychmiast, bez retry. Jest używana głównie w callbackach funkcji then(), gdzie pracujemy z "zamrożonymi" wartościami JavaScript. Gdy użyjemy then(), przerywamy łańcuch retry Cypress i otrzymujemy snapshot wartości w danym momencie. expect() jest wtedy naturalnym wyborem do weryfikacji tej wartości. Składnia to expect(value).to.equal(10).

Wybór między nimi zależy od kontekstu: dla elementów DOM i komend Cypress używaj should() z retry, dla logiki JavaScript w callbackach używaj expect() bez retry. Można również używać should() z funkcją callback, która zawiera expect() - wtedy callback jest wywoływany wielokrotnie (retry), ale wewnętrzne expect() failuje natychmiast jeśli warunek nie jest spełniony w danej iteracji.

Cypress oferuje też alias and() dla should(), który poprawia czytelność przy łańcuchu wielu asercji. Możliwe jest również mieszanie stylów: should() dla warunków DOM, a expect() dla dodatkowej logiki wewnątrz callback. Kluczowa zasada: jeśli potrzebujesz retry (czekanie na zmianę stanu), użyj should(). Jeśli testujesz wartość która już jest znana i nie zmieni się, użyj expect().

Przykład kodu

describe('should() vs expect() w Cypress', () => {
  beforeEach(() => {
    cy.visit('https://example.com')
  })

  it('should() - z automatycznym retry', () => {
    // should() dla elementów DOM - ma retry
    cy.get('.notification')
      .should('be.visible')              // Czeka aż będzie widoczny
      .and('have.class', 'success')      // and() to alias dla should()
      .and('contain', 'Zapisano')        // Czeka aż tekst się pojawi

    // should() dla właściwości elementu - ma retry
    cy.get('input#email')
      .should('have.value', '')          // Czeka aż value będzie puste
      .and('be.enabled')                 // Czeka aż będzie aktywny
      .and('have.attr', 'type', 'email') // Sprawdza atrybut

    // should() dla URL - ma retry
    cy.url()
      .should('include', '/dashboard')   // Czeka na zmianę URL
      .and('match', /\/dashboard\?.*/)   // Walidacja regex
  })

  it('expect() - bez retry, natychmiastowa asercja', () => {
    // expect() w callback then() - NIE ma retry
    cy.get('.price').then(($price) => {
      const value = parseFloat($price.text())
      expect(value).to.be.a('number')
      expect(value).to.be.greaterThan(0)
      expect(value).to.be.lessThan(1000)
    })

    // expect() dla obiektów JavaScript
    cy.window().then((win) => {
      expect(win.localStorage.getItem('theme')).to.equal('dark')
      expect(win.location.pathname).to.include('/home')
    })

    // expect() dla tablic i obiektów
    cy.wrap({ name: 'Jan', age: 30 }).then((user) => {
      expect(user).to.have.property('name')
      expect(user.age).to.equal(30)
      expect(user).to.deep.equal({ name: 'Jan', age: 30 })
    })
  })

  it('Kiedy używać should() - elementy DOM i retry', () => {
    // ✅ DOBRE - should() dla elementów które mogą się zmienić
    cy.get('.loading-spinner')
      .should('not.exist')               // Czeka aż zniknie

    cy.get('.data-loaded')
      .should('be.visible')              // Czeka aż się pojawi

    cy.get('.counter')
      .should('have.text', '5')          // Czeka aż licznik osiągnie 5

    // ✅ DOBRE - should() dla komendy Cypress
    cy.getCookie('session')
      .should('exist')
      .and('have.property', 'value')
  })

  it('Kiedy używać expect() - wartości JavaScript bez retry', () => {
    // ✅ DOBRE - expect() dla przetworzonych wartości
    cy.get('.items').then(($items) => {
      const count = $items.length
      const texts = $items.map((i, el) => el.textContent).get()

      expect(count).to.equal(3)
      expect(texts).to.include('Item 1')
      expect(texts).to.deep.equal(['Item 1', 'Item 2', 'Item 3'])
    })

    // ✅ DOBRE - expect() dla obliczeń
    cy.get('.price-total').then(($total) => {
      const total = parseFloat($total.text())
      const expectedTotal = 100 * 1.23 // VAT 23%
      expect(total).to.be.closeTo(expectedTotal, 0.01)
    })
  })

  it('should() z callback - łączenie retry + expect()', () => {
    // should() z funkcją callback - callback jest retry-owany
    cy.get('.status-list').should(($list) => {
      // Ten blok wykonuje się wielokrotnie (retry)
      const items = $list.find('.status-item')

      // expect() wewnątrz jest sprawdzany przy każdej próbie
      expect(items).to.have.length(3)

      // Możemy robić złożone sprawdzenia
      const statuses = items.map((i, el) => el.textContent).get()
      expect(statuses).to.include('Active')
    })
  })

  it('Porównanie: ta sama asercja w obu stylach', () => {
    // Styl 1: should() - z retry
    cy.get('.message')
      .should('contain', 'Sukces')       // Czeka aż tekst się pojawi

    // Styl 2: expect() w then() - bez retry
    cy.get('.message').then(($msg) => {
      expect($msg.text()).to.include('Sukces') // Sprawdza natychmiast
    })

    // Styl 3: should() z callback + expect() - retry
    cy.get('.message').should(($msg) => {
      expect($msg.text()).to.include('Sukces') // Callback retry-owany
    })
  })

  it('Łączenie should() i expect()', () => {
    cy.get('form')
      .should('be.visible')              // should() dla DOM
      .find('input')
      .should('have.length', 3)          // should() dla liczby elementów
      .then(($inputs) => {
        // Przełączenie na expect() dla logiki JS
        const values = $inputs.map((i, el) => el.value).get()
        expect(values).to.deep.equal(['', '', ''])
        expect(values.every(v => v.length === 0)).to.be.true
      })
  })

  it('should() dla asynchronicznych zmian', () => {
    cy.get('#load-data-btn').click()

    // should() czeka na asynchroniczną zmianę
    cy.get('.data-container')
      .should('not.have.class', 'loading')    // Czeka aż loading zniknie
      .and('have.class', 'loaded')            // Czeka aż loaded się pojawi
      .find('.data-row')
      .should('have.length.greaterThan', 0)   // Czeka aż dane się załadują
  })

  it('expect() dla wartości statycznych', () => {
    // Dla wartości które już znamy i się nie zmienią
    const config = { apiUrl: 'https://api.example.com', timeout: 5000 }

    cy.wrap(config).then((cfg) => {
      expect(cfg.apiUrl).to.be.a('string')
      expect(cfg.timeout).to.equal(5000)
      expect(cfg).to.have.all.keys('apiUrl', 'timeout')
    })
  })

  it('Błędy przy niepoprawnym użyciu', () => {
    // ❌ ZŁE - expect() bez then() nie ma sensu
    // cy.get('.element').expect('be.visible') // Syntax error!

    // ❌ ZŁE - should() w then() - traci retry
    cy.get('.element').then(($el) => {
      // To nie zadziała - should() potrzebuje być w łańcuchu Cypress
      // $el.should('be.visible') // Błąd!

      // Użyj expect() zamiast tego
      expect($el).to.be.visible
    })

    // ✅ DOBRE - should() poza then()
    cy.get('.element').should('be.visible')

    // ✅ DOBRE - expect() wewnątrz then()
    cy.get('.element').then(($el) => {
      expect($el).to.be.visible
    })
  })

  it('Negacja w obu stylach', () => {
    // should() z negacją - retry działa
    cy.get('.error-message')
      .should('not.exist')
      .should('not.be.visible')
      .should('not.have.class', 'active')

    // expect() z negacją - natychmiast
    cy.get('.button').then(($btn) => {
      expect($btn).to.not.have.class('disabled')
      expect($btn.text()).to.not.equal('')
    })
  })

  it('Własne komunikaty błędów', () => {
    // should() - komunikat w trzecim parametrze (rzadko używane)
    cy.get('.element').should('be.visible') // Domyślny komunikat

    // expect() - własny komunikat w drugim parametrze
    cy.get('.price').then(($price) => {
      const value = parseFloat($price.text())
      expect(value, 'Cena powinna być dodatnia').to.be.greaterThan(0)
      expect(value, 'Cena nie może przekraczać 1000').to.be.lessThan(1000)
    })
  })

  it('Multiple asercje - różne style', () => {
    // Styl 1: Łańcuch should()
    cy.get('.user-card')
      .should('be.visible')
      .and('have.class', 'active')
      .and('contain', 'Jan Kowalski')

    // Styl 2: should() z callback - wszystko w jednym
    cy.get('.user-card').should(($card) => {
      expect($card).to.be.visible
      expect($card).to.have.class('active')
      expect($card.text()).to.include('Jan Kowalski')
    })

    // Styl 3: Osobne should() (każde retry osobno)
    cy.get('.user-card').should('be.visible')
    cy.get('.user-card').should('have.class', 'active')
    cy.get('.user-card').should('contain', 'Jan Kowalski')
  })

  it('Testowanie API responses', () => {
    cy.request('/api/users/1').then((response) => {
      // expect() dla struktury response
      expect(response.status).to.equal(200)
      expect(response.body).to.have.property('id', 1)
      expect(response.body.name).to.be.a('string')
      expect(response.headers['content-type']).to.include('application/json')
    })

    // Alternatywnie z should()
    cy.request('/api/users/1')
      .its('status').should('equal', 200)
      .its('body').should('have.property', 'id', 1)
  })
})

// Podsumowanie: Kiedy czego używać
/*
┌─────────────────────────────────────────────────────────────┐
│ UŻYJ should()                                               │
├─────────────────────────────────────────────────────────────┤
│ • Elementy DOM (cy.get, cy.find)                            │
│ • Komendy Cypress (cy.url, cy.getCookie)                    │
│ • Kiedy potrzebujesz retry (czekanie na zmianę)             │
│ • Asynchroniczne operacje                                   │
│ • Gdy warunek może się zmienić w czasie                     │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ UŻYJ expect()                                               │
├─────────────────────────────────────────────────────────────┤
│ • Wewnątrz then() dla wartości JavaScript                   │
│ • Dla "zamrożonych" wartości (snapshot)                     │
│ • Obliczenia i transformacje danych                         │
│ • Testowanie struktur (obiekty, tablice)                    │
│ • Gdy wartość jest statyczna i znana                        │
└─────────────────────────────────────────────────────────────┘
*/

Diagram

graph TB
    A[Potrzebujesz asercji] --> B{Element DOM<br/>czy wartość JS?}

    B -->|Element DOM| C[Użyj should]
    B -->|Wartość JS| D{W callback<br/>then?}

    D -->|TAK| E[Użyj expect]
    D -->|NIE| F[Użyj should + wrap]

    C --> G{Potrzebujesz<br/>retry?}
    G -->|TAK| H[cy.get.should<br/>✓ Retry]
    G -->|NIE| I[cy.get.then + expect<br/>✗ Bez retry]

    E --> J[then $el =><br/>expect $el]

    subgraph "should() - Retry ✓"
        H --> K[Próba 1]
        K -->|Fail| L[Czekaj 50ms]
        L --> M[Próba 2]
        M -->|Fail| L
        M -->|Pass| N[Sukces]
    end

    subgraph "expect() - Natychmiast ✗"
        J --> O[Sprawdź raz]
        O -->|Pass| P[Sukces]
        O -->|Fail| Q[Błąd]
    end

    R[Przykłady should] --> R1[.should 'be.visible']
    R --> R2[.should 'have.text']
    R --> R3[.should callback]

    S[Przykłady expect] --> S1[expect x .to.equal]
    S --> S2[expect arr .to.include]
    S --> S3[expect obj .to.have.property]

    style H fill:#90EE90
    style J fill:#FFE6CC
    style N fill:#90EE90
    style Q fill:#FFB6C6

Materiały

Jak sprawdzić widoczność, tekst i atrybuty elementu?

Odpowiedź w 30 sekund

W Cypress sprawdzamy widoczność przez should('be.visible') lub should('not.be.visible'), tekst przez should('have.text', 'exact') lub should('contain', 'partial'), a atrybuty przez should('have.attr', 'name', 'value') lub invoke('attr', 'name'). Wszystkie te asercje korzystają z automatycznego retry, więc czekają na pojawienie się oczekiwanego stanu.

Odpowiedź w 2 minuty

Cypress oferuje bogaty zestaw asserterów do testowania różnych aspektów elementów DOM. Widoczność sprawdzamy głównie przez be.visible, który weryfikuje czy element istnieje w DOM, ma niezerowe wymiary, nie ma display: none ani visibility: hidden, i nie jest zasłonięty przez inny element. Dla ukrytych elementów używamy not.be.visible lub not.exist.

Tekst elementu można sprawdzać na kilka sposobów w zależności od potrzeb. have.text wymaga dokładnego dopasowania całego tekstu (łącznie z białymi znakami), contain sprawdza czy tekst zawiera podany fragment (częściowe dopasowanie), a have.html weryfikuje zawartość HTML. Można też używać wyrażeń regularnych przez match. Metoda invoke('text') pozwala na pobranie tekstu i dalsze operacje.

Atrybuty sprawdzamy przez have.attr podając nazwę atrybutu i opcjonalnie oczekiwaną wartość, lub przez invoke('attr', 'name') aby pobrać wartość do dalszej analizy. Dla klas CSS używamy dedykowanego have.class, dla wartości inputów have.value, a dla właściwości have.prop. Ważne jest rozróżnienie między atrybutami HTML (attr) a właściwościami DOM (prop).

Wszystkie te asercje działają z mechanizmem retry Cypress, automatycznie czekając aż warunek zostanie spełniony. Można je łączyć w łańcuchy używając and(), sprawdzać wiele warunków jednocześnie w callback, oraz negować przez not. Dla bardziej złożonych scenariuszy można używać should() z funkcją callback, która pozwala na dowolną logikę walidacji.

Przykład kodu

describe('Sprawdzanie widoczności, tekstu i atrybutów', () => {
  beforeEach(() => {
    cy.visit('https://example.com')
  })

  describe('Widoczność elementów', () => {
    it('Sprawdzanie czy element jest widoczny', () => {
      // Podstawowe sprawdzenie widoczności
      cy.get('.header').should('be.visible')

      // Element istnieje w DOM i jest widoczny
      cy.get('.notification')
        .should('exist')
        .and('be.visible')

      // Negacja - element ukryty
      cy.get('.hidden-content').should('not.be.visible')

      // Element w ogóle nie istnieje w DOM
      cy.get('.non-existent').should('not.exist')
    })

    it('Różne stany widoczności', () => {
      // Element istnieje ale jest ukryty przez CSS
      cy.get('.display-none').should('exist').and('not.be.visible')

      // Element poza ekranem (nadal widoczny dla Cypress)
      cy.get('.offscreen').should('be.visible')

      // Element z opacity: 0 (nadal widoczny)
      cy.get('.transparent').should('be.visible')

      // Element z zerową wysokością/szerokością
      cy.get('.zero-size').should('not.be.visible')
    })

    it('Czekanie na pojawienie się elementu', () => {
      cy.get('#load-btn').click()

      // Cypress czeka aż element stanie się widoczny
      cy.get('.loaded-content')
        .should('be.visible', { timeout: 10000 })

      // Czekanie aż loader zniknie
      cy.get('.loading-spinner').should('not.exist')
    })

    it('Sprawdzanie widoczności wielu elementów', () => {
      cy.get('.menu-item')
        .should('have.length', 5)
        .and('be.visible') // Wszystkie są widoczne

      // Sprawdzenie konkretnego elementu z listy
      cy.get('.menu-item').eq(2).should('be.visible')

      // Sprawdzenie pierwszego i ostatniego
      cy.get('.menu-item').first().should('be.visible')
      cy.get('.menu-item').last().should('be.visible')
    })
  })

  describe('Sprawdzanie tekstu', () => {
    it('Dokładne dopasowanie tekstu', () => {
      // have.text - dokładne dopasowanie (bez białych znaków)
      cy.get('h1').should('have.text', 'Witaj w aplikacji')

      // Tekst z wieloma spacjami/enterami jest normalizowany
      cy.get('.title').should('have.text', 'Tytuł strony')
    })

    it('Częściowe dopasowanie tekstu', () => {
      // contain - zawiera fragment tekstu
      cy.get('.description')
        .should('contain', 'Lorem ipsum')
        .and('contain', 'dolor sit amet')

      // Wielkość liter ma znaczenie
      cy.get('.info').should('contain', 'Info')
    })

    it('Tekst z wyrażeniami regularnymi', () => {
      // match - wyrażenie regularne
      cy.get('.email')
        .invoke('text')
        .should('match', /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i)

      // Walidacja formatu numeru telefonu
      cy.get('.phone')
        .invoke('text')
        .should('match', /^\+48\s?\d{3}\s?\d{3}\s?\d{3}$/)
    })

    it('Pobieranie i analiza tekstu', () => {
      // invoke('text') - pobiera tekst do dalszej analizy
      cy.get('.price')
        .invoke('text')
        .then((text) => {
          const price = parseFloat(text.replace('zł', '').trim())
          expect(price).to.be.greaterThan(0)
          expect(price).to.be.lessThan(1000)
        })
    })

    it('Sprawdzanie tekstu z białymi znakami', () => {
      // Tekst dokładnie taki jak w HTML (z białymi znakami)
      cy.get('.formatted')
        .invoke('text')
        .should('equal', '  Tekst z spacjami  ')

      // Tekst po trim()
      cy.get('.formatted')
        .invoke('text')
        .invoke('trim')
        .should('equal', 'Tekst z spacjami')
    })

    it('Sprawdzanie pustego tekstu', () => {
      cy.get('.empty-div').should('have.text', '')
      cy.get('.empty-div').should('be.empty')

      // Input bez wartości
      cy.get('input#name').should('have.value', '')
    })

    it('Tekst wieloliniowy', () => {
      cy.get('.multiline')
        .should('contain', 'Linia 1')
        .and('contain', 'Linia 2')
        .and('contain', 'Linia 3')
    })

    it('Sprawdzanie HTML content', () => {
      // have.html - zawartość HTML
      cy.get('.rich-content')
        .should('have.html', '<strong>Ważne</strong> informacje')

      // Zawiera tag HTML
      cy.get('.formatted-text').should('contain.html', '<em>')
    })

    it('Tekst elementów z listy', () => {
      // Sprawdzenie tekstu każdego elementu
      cy.get('.item').each(($item, index) => {
        expect($item.text()).to.include('Item')
      })

      // Konkretny tekst w konkretnym elemencie
      cy.get('.item').eq(0).should('have.text', 'Item 1')
      cy.get('.item').eq(1).should('have.text', 'Item 2')
    })
  })

  describe('Sprawdzanie atrybutów', () => {
    it('Podstawowe sprawdzanie atrybutów', () => {
      // have.attr - sprawdzenie wartości atrybutu
      cy.get('a.link')
        .should('have.attr', 'href', '/page')
        .and('have.attr', 'target', '_blank')

      // Sprawdzenie czy atrybut istnieje (bez wartości)
      cy.get('input').should('have.attr', 'required')

      // Sprawdzenie że atrybut nie istnieje
      cy.get('button').should('not.have.attr', 'disabled')
    })

    it('Pobieranie wartości atrybutu', () => {
      // invoke('attr') - pobiera wartość atrybutu
      cy.get('img')
        .invoke('attr', 'src')
        .should('include', '/images/')
        .and('match', /\.(jpg|png|gif)$/)

      // Analiza wartości atrybutu
      cy.get('a').invoke('attr', 'href').then((href) => {
        expect(href).to.be.a('string')
        expect(href.startsWith('http')).to.be.true
      })
    })

    it('Atrybuty data-*', () => {
      // Sprawdzanie custom data attributes
      cy.get('.product')
        .should('have.attr', 'data-id', '123')
        .and('have.attr', 'data-category', 'electronics')
        .and('have.attr', 'data-price', '99.99')

      // Pobieranie i parsowanie data-*
      cy.get('.product')
        .invoke('attr', 'data-price')
        .then(parseFloat)
        .should('be.greaterThan', 50)
    })

    it('Sprawdzanie klas CSS', () => {
      // have.class - sprawdzenie pojedynczej klasy
      cy.get('.button')
        .should('have.class', 'btn')
        .and('have.class', 'btn-primary')
        .and('have.class', 'active')

      // Negacja
      cy.get('.button').should('not.have.class', 'disabled')

      // Sprawdzenie czy ma którykolwiek z klas
      cy.get('.alert').should(($el) => {
        const classes = $el.attr('class')
        expect(classes).to.match(/success|warning|error/)
      })
    })

    it('Sprawdzanie wartości inputów', () => {
      // have.value - dla input, select, textarea
      cy.get('input#email')
        .should('have.value', '')

      cy.get('input#email').type('test@example.com')
      cy.get('input#email')
        .should('have.value', 'test@example.com')

      // Checkbox
      cy.get('input[type="checkbox"]')
        .should('not.be.checked')
        .check()
        .should('be.checked')
    })

    it('Sprawdzanie właściwości vs atrybutów', () => {
      // Atrybut HTML (początkowa wartość)
      cy.get('input#name')
        .should('have.attr', 'value', 'Jan')

      // Właściwość DOM (aktualna wartość)
      cy.get('input#name')
        .should('have.prop', 'value', 'Jan')

      // Po wpisaniu tekstu
      cy.get('input#name').clear().type('Anna')

      // Atrybut się nie zmienił
      cy.get('input#name').should('have.attr', 'value', 'Jan')

      // Ale właściwość tak
      cy.get('input#name').should('have.prop', 'value', 'Anna')
    })

    it('Sprawdzanie wielu atrybutów jednocześnie', () => {
      cy.get('button#submit').should(($btn) => {
        expect($btn).to.have.attr('type', 'submit')
        expect($btn).to.have.attr('class')
        expect($btn).to.have.class('btn-primary')
        expect($btn.attr('disabled')).to.be.undefined
      })
    })

    it('Atrybuty boolean', () => {
      // Atrybuty boolean (disabled, required, readonly)
      cy.get('input#required')
        .should('have.attr', 'required')

      cy.get('button').should('not.have.attr', 'disabled')

      // Sprawdzenie przez właściwość (zwraca boolean)
      cy.get('input#required')
        .should('have.prop', 'required', true)
    })

    it('Style CSS', () => {
      // have.css - sprawdzenie computed style
      cy.get('.header')
        .should('have.css', 'background-color', 'rgb(255, 0, 0)')
        .and('have.css', 'font-size', '24px')
        .and('have.css', 'display', 'flex')

      // Pobieranie wartości CSS
      cy.get('.element')
        .invoke('css', 'color')
        .should('equal', 'rgb(0, 0, 0)')
    })
  })

  describe('Kombinacje sprawdzeń', () => {
    it('Widoczność + tekst + atrybuty razem', () => {
      cy.get('.alert')
        .should('be.visible')                         // Widoczność
        .and('have.class', 'alert-success')           // Klasa
        .and('contain', 'Operacja zakończona')        // Tekst
        .and('have.attr', 'role', 'alert')            // Atrybut
        .and('have.css', 'background-color')          // Style
    })

    it('Callback z wieloma sprawdzeniami', () => {
      cy.get('.user-card').should(($card) => {
        // Widoczność
        expect($card).to.be.visible

        // Tekst
        expect($card.text()).to.include('Jan Kowalski')

        // Atrybuty
        expect($card).to.have.attr('data-user-id')
        expect($card).to.have.class('active')

        // Dzieci
        expect($card.find('.avatar')).to.exist
        expect($card.find('.name')).to.have.text('Jan Kowalski')
      })
    })

    it('Dynamiczne czekanie na zmiany', () => {
      cy.get('#start-process').click()

      // Czekaj aż status się zmieni
      cy.get('.status')
        .should('not.have.text', 'Nieaktywny')
        .and('have.text', 'Aktywny')
        .and('have.class', 'status-active')

      // Czekaj aż progress osiągnie 100%
      cy.get('.progress-bar')
        .should('have.attr', 'aria-valuenow', '100')
        .and('have.text', '100%')
    })
  })
})

Diagram

graph TB
    A[Element DOM] --> B[Widoczność]
    A --> C[Tekst]
    A --> D[Atrybuty]

    B --> B1[should 'be.visible']
    B --> B2[should 'not.be.visible']
    B --> B3[should 'exist']
    B --> B4[should 'not.exist']

    C --> C1[should 'have.text']
    C --> C2[should 'contain']
    C --> C3[should 'match' regex]
    C --> C4[invoke 'text']
    C --> C5[should 'have.html']

    D --> D1[should 'have.attr']
    D --> D2[invoke 'attr']
    D --> D3[should 'have.class']
    D --> D4[should 'have.value']
    D --> D5[should 'have.prop']
    D --> D6[should 'have.css']

    subgraph "Widoczność - Warunki"
        E1[Istnieje w DOM]
        E2[Niezerowe wymiary]
        E3[Nie display:none]
        E4[Nie visibility:hidden]
        E5[Nie zasłonięty]
    end

    B1 --> E1
    B1 --> E2
    B1 --> E3
    B1 --> E4
    B1 --> E5

    subgraph "Tekst - Typy"
        F1[Dokładne: have.text]
        F2[Częściowe: contain]
        F3[Regex: match]
        F4[HTML: have.html]
    end

    C1 --> F1
    C2 --> F2
    C3 --> F3
    C5 --> F4

    subgraph "Atrybuty - Rodzaje"
        G1[HTML: href, src, id]
        G2[Data-*: data-id]
        G3[Klasy: class]
        G4[Style: css]
        G5[Właściwości: prop]
    end

    D1 --> G1
    D1 --> G2
    D3 --> G3
    D6 --> G4
    D5 --> G5

    style A fill:#E1F5FF
    style B1 fill:#C3F0C3
    style C1 fill:#FFE6CC
    style D1 fill:#FFD9E6

Materiały

Jak testować stan aplikacji (URL, cookies, localStorage)?

Odpowiedź w 30 sekund

Do testowania URL używamy cy.url() z asercjami should('include') lub should('eq'). Cookies sprawdzamy przez cy.getCookie('name') i cy.getCookies(), a ustawiamy przez cy.setCookie(). LocalStorage testujemy przez cy.window().its('localStorage') lub dedykowane komendy cy.clearLocalStorage(). Wszystkie te operacje mają wbudowany retry i działają asynchronicznie.

Odpowiedź w 2 minuty

Cypress oferuje bogate API do testowania stanu aplikacji na różnych poziomach. URL testujemy przez komendę cy.url(), która zwraca aktualny adres i automatycznie czeka na zmiany routingu. Możemy sprawdzać pełny URL przez should('eq'), fragment przez should('include'), lub używać wyrażeń regularnych przez should('match'). Dodatkowo cy.location() daje dostęp do wszystkich części URL (pathname, search, hash, protocol).

Cookies są testowane przez dedykowane komendy: cy.getCookie('name') pobiera pojedyncze cookie, cy.getCookies() zwraca wszystkie cookies dla danej domeny, cy.setCookie() ustawia cookie, a cy.clearCookie() i cy.clearCookies() je usuwają. Można sprawdzać wartości, daty wygaśnięcia, flagi secure/httpOnly oraz domain i path. Cookies są automatycznie zachowywane między testami w tym samym it(), ale czyszczone między różnymi testami.

LocalStorage i sessionStorage testujemy przez obiekt window. Używamy cy.window().its('localStorage.key') do sprawdzenia wartości, lub cy.window().then() do bardziej złożonych operacji. Cypress oferuje też cy.clearLocalStorage() do czyszczenia storage. Podobnie dla sessionStorage. Można również tworzyć własne komendy do wygodniejszego zarządzania storage.

Stan aplikacji to także Redux/Vuex store, który można testować przez cy.window().its('store'), zmienne globalne, query parametry URL, oraz session data. Cypress pozwala na pełną introspekcję i modyfikację stanu aplikacji w trakcie testów, co jest bardzo przydatne do setupu scenariuszy testowych czy debugowania.

Przykład kodu

describe('Testowanie stanu aplikacji', () => {
  describe('Testowanie URL', () => {
    it('Sprawdzanie pełnego URL', () => {
      cy.visit('https://example.com/products?category=electronics')

      // Pełny URL
      cy.url().should('eq', 'https://example.com/products?category=electronics')

      // Częściowe dopasowanie
      cy.url().should('include', '/products')
      cy.url().should('include', 'category=electronics')

      // Wyrażenie regularne
      cy.url().should('match', /\/products\?.*/)
    })

    it('Sprawdzanie części URL', () => {
      cy.visit('https://example.com/users/123?tab=profile#settings')

      // cy.location() - dostęp do wszystkich części URL
      cy.location('protocol').should('eq', 'https:')
      cy.location('hostname').should('eq', 'example.com')
      cy.location('pathname').should('eq', '/users/123')
      cy.location('search').should('eq', '?tab=profile')
      cy.location('hash').should('eq', '#settings')

      // Obiekt location
      cy.location().should((loc) => {
        expect(loc.protocol).to.eq('https:')
        expect(loc.pathname).to.eq('/users/123')
        expect(loc.search).to.include('tab=profile')
        expect(loc.hash).to.eq('#settings')
      })
    })

    it('Czekanie na zmianę URL', () => {
      cy.visit('https://example.com')

      cy.get('a[href="/about"]').click()

      // Cypress czeka aż URL się zmieni
      cy.url().should('include', '/about')

      // Czekanie na konkretny parametr query
      cy.get('#filter-btn').click()
      cy.url().should('include', 'filter=active')
    })

    it('Testowanie navigacji', () => {
      cy.visit('https://example.com/page1')
      cy.url().should('include', '/page1')

      cy.visit('https://example.com/page2')
      cy.url().should('include', '/page2')

      // Powrót
      cy.go('back')
      cy.url().should('include', '/page1')

      // Dalej
      cy.go('forward')
      cy.url().should('include', '/page2')

      // Odświeżenie
      cy.reload()
      cy.url().should('include', '/page2')
    })

    it('Query parameters', () => {
      cy.visit('https://example.com/search?q=cypress&lang=pl&page=2')

      // Sprawdzenie query params
      cy.location('search')
        .should('include', 'q=cypress')
        .and('include', 'lang=pl')
        .and('include', 'page=2')

      // Parsowanie query params
      cy.location('search').then((search) => {
        const params = new URLSearchParams(search)
        expect(params.get('q')).to.equal('cypress')
        expect(params.get('lang')).to.equal('pl')
        expect(params.get('page')).to.equal('2')
      })
    })

    it('Hash/fragment', () => {
      cy.visit('https://example.com/docs#installation')

      cy.location('hash').should('eq', '#installation')

      // Kliknięcie w link z hash
      cy.get('a[href="#api"]').click()
      cy.location('hash').should('eq', '#api')
    })
  })

  describe('Testowanie Cookies', () => {
    beforeEach(() => {
      cy.visit('https://example.com')
    })

    it('Pobieranie i sprawdzanie cookie', () => {
      // Ustawienie cookie
      cy.setCookie('session_id', 'abc123')

      // Pobranie cookie
      cy.getCookie('session_id')
        .should('exist')
        .and('have.property', 'value', 'abc123')

      // Sprawdzenie właściwości cookie
      cy.getCookie('session_id').should((cookie) => {
        expect(cookie.value).to.eq('abc123')
        expect(cookie.domain).to.include('example.com')
        expect(cookie.httpOnly).to.be.false
        expect(cookie.secure).to.be.false
      })
    })

    it('Sprawdzanie nieistniejącego cookie', () => {
      cy.getCookie('non_existent').should('be.null')
    })

    it('Pobieranie wszystkich cookies', () => {
      cy.setCookie('cookie1', 'value1')
      cy.setCookie('cookie2', 'value2')
      cy.setCookie('cookie3', 'value3')

      cy.getCookies()
        .should('have.length', 3)
        .then((cookies) => {
          expect(cookies[0]).to.have.property('name', 'cookie1')
          expect(cookies[1]).to.have.property('name', 'cookie2')
          expect(cookies[2]).to.have.property('name', 'cookie3')
        })
    })

    it('Ustawianie cookie z opcjami', () => {
      cy.setCookie('auth_token', 'xyz789', {
        domain: 'example.com',
        path: '/api',
        secure: true,
        httpOnly: true,
        expiry: Math.floor(Date.now() / 1000) + 3600 // 1 godzina
      })

      cy.getCookie('auth_token').should((cookie) => {
        expect(cookie.value).to.eq('xyz789')
        expect(cookie.secure).to.be.true
        expect(cookie.httpOnly).to.be.true
        expect(cookie.path).to.eq('/api')
      })
    })

    it('Usuwanie cookies', () => {
      cy.setCookie('temp', 'temporary')
      cy.getCookie('temp').should('exist')

      // Usunięcie konkretnego cookie
      cy.clearCookie('temp')
      cy.getCookie('temp').should('be.null')

      // Usunięcie wszystkich cookies
      cy.setCookie('cookie1', 'value1')
      cy.setCookie('cookie2', 'value2')
      cy.clearCookies()
      cy.getCookies().should('be.empty')
    })

    it('Testowanie logowania przez cookies', () => {
      // Login
      cy.visit('https://example.com/login')
      cy.get('input[name="username"]').type('user')
      cy.get('input[name="password"]').type('password')
      cy.get('button[type="submit"]').click()

      // Sprawdzenie że cookie sesji zostało ustawione
      cy.getCookie('session')
        .should('exist')
        .and('have.property', 'value')
        .and('match', /^[a-z0-9]+$/i)

      // Nawigacja - cookie powinno być zachowane
      cy.visit('https://example.com/dashboard')
      cy.getCookie('session').should('exist')
    })

    it('Cookie między różnymi domenami', () => {
      // Cypress domyślnie czyści cookies przy zmianie superdomain
      cy.visit('https://example.com')
      cy.setCookie('site_cookie', 'value')

      // Ta wizyta wyczyści cookies (inna domena)
      cy.visit('https://different-site.com')

      // Powrót - cookie już nie istnieje
      cy.visit('https://example.com')
      cy.getCookie('site_cookie').should('be.null')
    })
  })

  describe('Testowanie LocalStorage', () => {
    beforeEach(() => {
      cy.visit('https://example.com')
    })

    it('Sprawdzanie wartości w localStorage', () => {
      // Ustawienie wartości przez aplikację
      cy.window().then((win) => {
        win.localStorage.setItem('theme', 'dark')
        win.localStorage.setItem('language', 'pl')
      })

      // Sprawdzenie wartości
      cy.window()
        .its('localStorage.theme')
        .should('equal', 'dark')

      cy.window()
        .its('localStorage.language')
        .should('equal', 'pl')
    })

    it('Sprawdzanie złożonych obiektów w localStorage', () => {
      const user = { id: 1, name: 'Jan', role: 'admin' }

      cy.window().then((win) => {
        win.localStorage.setItem('user', JSON.stringify(user))
      })

      cy.window().then((win) => {
        const stored = JSON.parse(win.localStorage.getItem('user'))
        expect(stored).to.deep.equal(user)
        expect(stored.name).to.equal('Jan')
        expect(stored.role).to.equal('admin')
      })
    })

    it('Czyszczenie localStorage', () => {
      cy.window().then((win) => {
        win.localStorage.setItem('key1', 'value1')
        win.localStorage.setItem('key2', 'value2')
      })

      // Czyszczenie wszystkiego
      cy.clearLocalStorage()

      cy.window().then((win) => {
        expect(win.localStorage.length).to.equal(0)
      })
    })

    it('Czyszczenie wybranych kluczy', () => {
      cy.window().then((win) => {
        win.localStorage.setItem('keep_this', 'value1')
        win.localStorage.setItem('remove_this', 'value2')
      })

      // Czyszczenie kluczy pasujących do regex
      cy.clearLocalStorage('remove_')

      cy.window().then((win) => {
        expect(win.localStorage.getItem('keep_this')).to.exist
        expect(win.localStorage.getItem('remove_this')).to.be.null
      })
    })

    it('Testowanie zapisywania przez aplikację', () => {
      cy.get('#save-settings-btn').click()

      // Sprawdzenie że aplikacja zapisała w localStorage
      cy.window()
        .its('localStorage.settings')
        .should('exist')
        .then((settings) => {
          const parsed = JSON.parse(settings)
          expect(parsed).to.have.property('notifications', true)
          expect(parsed).to.have.property('theme')
        })
    })

    it('Setup stanu przez localStorage', () => {
      // Ustawienie stanu przed testem
      cy.visit('https://example.com', {
        onBeforeLoad(win) {
          win.localStorage.setItem('auth_token', 'test-token-123')
          win.localStorage.setItem('user_id', '42')
        }
      })

      // Aplikacja powinna być już zalogowana
      cy.get('.user-menu').should('be.visible')
      cy.get('.user-id').should('contain', '42')
    })
  })

  describe('Testowanie SessionStorage', () => {
    it('Sprawdzanie sessionStorage', () => {
      cy.visit('https://example.com')

      cy.window().then((win) => {
        win.sessionStorage.setItem('temp_data', 'temporary')
      })

      cy.window()
        .its('sessionStorage.temp_data')
        .should('equal', 'temporary')

      // SessionStorage czyszczone przy nowej karcie/oknie
      cy.reload()
      cy.window()
        .its('sessionStorage.temp_data')
        .should('equal', 'temporary') // Nadal istnieje po reload
    })
  })

  describe('Testowanie stanu Redux/Vuex', () => {
    it('Sprawdzanie Redux store', () => {
      cy.visit('https://example.com') // Aplikacja z Redux

      // Dostęp do store przez window
      cy.window()
        .its('store')
        .invoke('getState')
        .should((state) => {
          expect(state.user).to.exist
          expect(state.user.isLoggedIn).to.be.false
        })
    })

    it('Modyfikacja stanu Redux', () => {
      cy.visit('https://example.com')

      // Dispatch akcji
      cy.window().then((win) => {
        win.store.dispatch({
          type: 'LOGIN_SUCCESS',
          payload: { id: 1, name: 'Test User' }
        })
      })

      // Sprawdzenie że UI się zaktualizowało
      cy.get('.user-name').should('contain', 'Test User')

      // Sprawdzenie stanu
      cy.window()
        .its('store')
        .invoke('getState')
        .its('user.isLoggedIn')
        .should('be.true')
    })
  })

  describe('Własne komendy dla stanu', () => {
    // Definicja własnych komend
    before(() => {
      // Komenda do ustawiania localStorage
      Cypress.Commands.add('setLocalStorage', (key, value) => {
        cy.window().then((win) => {
          win.localStorage.setItem(key, value)
        })
      })

      // Komenda do pobierania localStorage
      Cypress.Commands.add('getLocalStorage', (key) => {
        return cy.window().then((win) => {
          return win.localStorage.getItem(key)
        })
      })

      // Komenda do logowania przez localStorage
      Cypress.Commands.add('loginByLocalStorage', (token) => {
        cy.setLocalStorage('auth_token', token)
        cy.setLocalStorage('logged_in', 'true')
      })
    })

    it('Użycie własnych komend', () => {
      cy.visit('https://example.com')

      cy.setLocalStorage('theme', 'dark')
      cy.getLocalStorage('theme').should('equal', 'dark')

      cy.loginByLocalStorage('test-token')
      cy.reload()
      cy.get('.logged-in-indicator').should('be.visible')
    })
  })

  describe('Testowanie wszystkich stanów razem', () => {
    it('Kompletny stan aplikacji', () => {
      cy.visit('https://example.com')

      // URL
      cy.url().should('include', '/home')

      // Cookies
      cy.setCookie('preferences', 'saved')
      cy.getCookie('preferences').should('exist')

      // LocalStorage
      cy.window().then((win) => {
        win.localStorage.setItem('version', '1.0')
      })
      cy.window().its('localStorage.version').should('equal', '1.0')

      // Wykonanie akcji
      cy.get('#navigate-btn').click()

      // Sprawdzenie zmian stanu
      cy.url().should('include', '/other-page')
      cy.getCookie('preferences').should('exist') // Zachowane
      cy.window().its('localStorage.version').should('equal', '1.0') // Zachowane
    })
  })
})

Diagram

graph TB
    A[Stan Aplikacji] --> B[URL]
    A --> C[Cookies]
    A --> D[Storage]
    A --> E[App State]

    B --> B1[cy.url]
    B --> B2[cy.location]
    B --> B3[cy.visit]
    B --> B4[cy.go back/forward]

    B1 --> B1A[should include]
    B1 --> B1B[should eq]
    B1 --> B1C[should match]

    B2 --> B2A[pathname]
    B2 --> B2B[search query]
    B2 --> B2C[hash]
    B2 --> B2D[protocol]

    C --> C1[cy.getCookie]
    C --> C2[cy.getCookies]
    C --> C3[cy.setCookie]
    C --> C4[cy.clearCookie]

    C1 --> C1A[Wartość]
    C1 --> C1B[Domain]
    C1 --> C1C[Path]
    C1 --> C1D[Secure/HttpOnly]
    C1 --> C1E[Expiry]

    D --> D1[LocalStorage]
    D --> D2[SessionStorage]

    D1 --> D1A[cy.window.its localStorage]
    D1 --> D1B[cy.clearLocalStorage]
    D1 --> D1C[setItem/getItem]

    E --> E1[Redux Store]
    E --> E2[Vuex Store]
    E --> E3[Zmienne globalne]

    E1 --> E1A[window.store.getState]
    E1 --> E1B[window.store.dispatch]

    subgraph "Lifecycle"
        F1[visit] --> F2[Interakcja]
        F2 --> F3[Zmiana URL]
        F2 --> F4[Ustawienie Cookie]
        F2 --> F5[Zapis Storage]
        F3 --> F6[Asercje]
        F4 --> F6
        F5 --> F6
    end

    style A fill:#E1F5FF
    style B1 fill:#C3F0C3
    style C1 fill:#FFE6CC
    style D1A fill:#FFD9E6

Materiały

Komendy i Custom Commands

Czym są komendy Cypress i jak działają łańcuchowo?

Odpowiedź w 30 sekund: Komendy Cypress to asynchroniczne operacje zwracające obiekty podlegające dalszym operacjom w łańcuchu. Działają w kolejce i są wykonywane sekwencyjnie, co eliminuje potrzebę używania async/await. Każda komenda zwraca nowy obiekt, który można dalej modyfikować za pomocą kolejnych komend połączonych kropką.

Odpowiedź w 2 minuty: Komendy Cypress są fundamentem frameworka testowego i stanowią asynchroniczne operacje, które są dodawane do kolejki wykonania. W przeciwieństwie do tradycyjnychPromisów JavaScript, komendy Cypress wykorzystują wzorzec projektowy "chaining" (łańcuchowanie), gdzie każda komenda zwraca nowy obiekt Cypress, który może być dalej przetwarzany. To podejście sprawia, że kod testowy jest czytelny i przypomina naturalny język.

Mechanizm łańcuchowy działa poprzez automatyczne zarządzanie kolejką komend. Cypress czeka na zakończenie każdej komendy przed wykonaniem następnej, automatycznie ponawiając operacje w przypadku niepowodzenia (retry-ability). Nie ma potrzeby ręcznego zarządzania Promise'ami czy używania async/await, ponieważ Cypress obsługuje to wewnętrznie.

Istnieją dwa główne typy komend: parent commands (komendy nadrzędne), które zaczynają nowy łańcuch jak cy.get(), oraz child commands (komendy podrzędne), które wymagają poprzedniego podmiotu jak .click() czy .type(). Dodatkowo są dual commands, które mogą działać w obu rolach, jak cy.contains().

Kluczową cechą jest to, że komendy nie zwracają bezpośrednio wartości - aby uzyskać dostęp do rezultatu, używamy .then(). Dzięki temu Cypress może zarządzać czasem wykonania i automatycznie ponawiać operacje, co czyni testy bardziej odpornymi na problemy z timingiem.

Przykład kodu:

// Przykład łańcuchowania komend Cypress
describe('Łańcuchowanie komend', () => {
  it('demonstruje działanie łańcuchowe komend', () => {
    // Parent command - rozpoczyna łańcuch
    cy.visit('https://example.com')

    // Child commands - kontynuują łańcuch
    cy.get('.user-form')           // Parent: znajduje element
      .find('input[name="email"]') // Child: szuka w znalezionym elemencie
      .type('test@example.com')    // Child: wpisuje tekst
      .should('have.value', 'test@example.com') // Assertion w łańcuchu

    // Nowy łańcuch z dual command
    cy.contains('Submit')          // Może być parent lub child
      .click()                     // Child: kliknięcie
      .should('be.disabled')       // Sprawdzenie stanu po kliknięciu

    // Użycie .then() do dostępu do wartości
    cy.get('.result-message')
      .then(($element) => {
        // $element to jQuery obiekt
        const text = $element.text()
        expect(text).to.include('Sukces')

        // Możemy wykonać synchroniczne operacje
        cy.log(`Otrzymana wiadomość: ${text}`)
      })

    // Alternatywne łańcuchowanie z rozgałęzieniem
    cy.get('.items')
      .find('li')
      .should('have.length.greaterThan', 0)
      .first()                     // Bierze pierwszy element
      .should('contain', 'Item')
  })

  it('pokazuje różnicę między comandami a Promise', () => {
    // ❌ NIE DZIAŁA - komendy nie są Promise'ami
    // const button = cy.get('button') // undefined!

    // ✅ DZIAŁA - użycie .then()
    cy.get('button').then(($button) => {
      // Mamy dostęp do elementu
      const text = $button.text()
      expect(text).to.equal('Kliknij mnie')
    })

    // ✅ DZIAŁA - alias dla późniejszego użycia
    cy.get('button').as('submitButton')

    // Użycie aliasu później
    cy.get('@submitButton').click()
  })

  it('demonstruje automatyczne retry dla komend', () => {
    cy.visit('/dynamic-content')

    // Cypress automatycznie ponawia przez 4 sekundy
    cy.get('.loading-spinner')
      .should('not.exist')  // Czeka aż zniknie

    cy.get('.content')
      .should('be.visible') // Czeka aż się pojawi
      .and('contain', 'Załadowano') // Kolejny warunek w łańcuchu
  })
})

// Przykład tworzenia własnego łańcucha komend
describe('Złożone łańcuchowanie', () => {
  it('łączy wiele operacji w jeden przepływ', () => {
    cy.visit('/dashboard')

    // Długi łańcuch logiczny
    cy.get('.user-menu')
      .click()                           // Otwórz menu
      .parent()                          // Przejdź do rodzica
      .find('.dropdown-menu')            // Znajdź dropdown
      .should('be.visible')              // Sprawdź widoczność
      .within(() => {                    // Kontekst wewnątrz elementu
        cy.contains('Profil').click()    // Znajdź i kliknij
      })

    // Weryfikacja nowej strony
    cy.url().should('include', '/profile')
    cy.get('h1').should('contain', 'Mój Profil')
  })
})

Diagram:

sequenceDiagram
    participant Test as Kod Testu
    participant Queue as Kolejka Cypress
    participant Browser as Przeglądarka

    Test->>Queue: cy.visit('/page')
    Test->>Queue: cy.get('.button')
    Test->>Queue: .click()
    Test->>Queue: .should('be.visible')

    Note over Test,Queue: Wszystkie komendy dodane do kolejki

    Queue->>Browser: visit('/page')
    Browser-->>Queue: ✓ Załadowano

    Queue->>Browser: get('.button')
    Browser-->>Queue: ✗ Nie znaleziono
    Queue->>Browser: get('.button') [retry]
    Browser-->>Queue: ✓ Znaleziono

    Queue->>Browser: click()
    Browser-->>Queue: ✓ Kliknięto

    Queue->>Browser: should('be.visible')
    Browser-->>Queue: ✓ Widoczny

    Note over Queue: Wszystkie komendy wykonane sekwencyjnie

Materiały:

Jak tworzyć własne komendy (Custom Commands)?

Odpowiedź w 30 sekund: Custom Commands w Cypress tworzy się za pomocą Cypress.Commands.add() w pliku support/commands.js. Pozwalają one hermetyzować powtarzalne operacje w reużywalne funkcje. Mogą przyjmować parametry, używać argumentów opcjonalnych i być łańcuchowane z innymi komendami Cypress.

Odpowiedź w 2 minuty: Custom Commands to mechanizm rozszerzania API Cypress o własne, wielokrotnie używane komendy. Definiuje się je w pliku cypress/support/commands.js (lub w osobnych plikach importowanych w tym pliku) za pomocą Cypress.Commands.add(). Jest to idealne rozwiązanie dla operacji wykonywanych w wielu testach, jak logowanie, wypełnianie formularzy czy złożone interakcje z UI.

Istnieją trzy typy Custom Commands: parent commands (rozpoczynające nowy łańcuch), child commands (działające na poprzednim podmiocie) oraz dual commands (mogące działać w obu rolach). Każdy typ ma inne zastosowanie i różni się sposobem deklaracji, szczególnie w zakresie parametru określającego poprzedni podmiot.

Custom Commands mają dostęp do wszystkich standardowych komend Cypress i mogą zwracać wartości, które będą używane w łańcuchu. Mogą również przyjmować opcje konfiguracyjne, używać TypeScript dla type safety, i być nadpisywane za pomocą Cypress.Commands.overwrite() do modyfikacji istniejących komend.

Ważne jest, by nie nadużywać Custom Commands - powinny być używane dla prawdziwej reużywalności, nie do ukrywania złożoności czy tworzenia abstrakcji na siłę. Dla prostych operacji lepiej używać standardowych funkcji JavaScript lub helpers.

Przykład kodu:

// cypress/support/commands.js

// 1. PARENT COMMAND - rozpoczyna nowy łańcuch
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login')
  cy.get('input[name="email"]').type(email)
  cy.get('input[name="password"]').type(password)
  cy.get('button[type="submit"]').click()

  // Weryfikacja poprawnego logowania
  cy.url().should('include', '/dashboard')
  cy.getCookie('auth_token').should('exist')
})

// Użycie w teście
describe('Test logowania', () => {
  it('loguje użytkownika', () => {
    cy.login('user@example.com', 'password123')
    cy.contains('Witaj, użytkowniku!')
  })
})

// 2. CHILD COMMAND - działa na poprzednim podmiocie
Cypress.Commands.add('getById', { prevSubject: 'element' }, (subject, id) => {
  // subject to poprzedni element w łańcuchu
  return cy.wrap(subject).find(`[data-testid="${id}"]`)
})

// Użycie child command
cy.get('.container')
  .getById('user-profile')
  .should('be.visible')

// 3. DUAL COMMAND - może być parent lub child
Cypress.Commands.add('clickLink', { prevSubject: 'optional' }, (subject, label) => {
  if (subject) {
    // Jako child - szuka w subject
    return cy.wrap(subject).contains('a', label).click()
  } else {
    // Jako parent - szuka globalnie
    return cy.contains('a', label).click()
  }
})

// Użycie jako parent
cy.clickLink('Strona główna')

// Użycie jako child
cy.get('.navigation').clickLink('Kontakt')

// 4. COMMAND Z OPCJAMI KONFIGURACYJNYMI
Cypress.Commands.add('loginByAPI', (username, password, options = {}) => {
  const defaults = {
    endpoint: '/api/auth/login',
    rememberMe: false,
  }

  const config = { ...defaults, ...options }

  cy.request({
    method: 'POST',
    url: config.endpoint,
    body: {
      username,
      password,
      rememberMe: config.rememberMe,
    },
  }).then((response) => {
    // Zapisz token w localStorage
    window.localStorage.setItem('authToken', response.body.token)

    // Ustaw cookie jeśli zaznaczono "pamiętaj mnie"
    if (config.rememberMe) {
      cy.setCookie('remember_token', response.body.rememberToken)
    }
  })
})

// Użycie z opcjami
cy.loginByAPI('admin', 'admin123', {
  rememberMe: true,
  endpoint: '/api/v2/login',
})

// 5. COMMAND DO WYPEŁNIANIA FORMULARZY
Cypress.Commands.add('fillForm', (formData) => {
  Object.entries(formData).forEach(([field, value]) => {
    if (typeof value === 'boolean') {
      // Dla checkboxów
      const action = value ? 'check' : 'uncheck'
      cy.get(`[name="${field}"]`)[action]()
    } else if (Array.isArray(value)) {
      // Dla select multiple
      cy.get(`[name="${field}"]`).select(value)
    } else {
      // Dla zwykłych inputów
      cy.get(`[name="${field}"]`).clear().type(value)
    }
  })
})

// Użycie
cy.fillForm({
  firstName: 'Jan',
  lastName: 'Kowalski',
  email: 'jan@example.com',
  newsletter: true,
  country: 'Poland',
})

// 6. OVERWRITE - nadpisanie istniejącej komendy
Cypress.Commands.overwrite('type', (originalFn, subject, text, options) => {
  // Dodaj opóźnienie przed każdym wpisywaniem
  if (options && options.sensitive) {
    // Nie loguj wrażliwych danych
    options.log = false
    Cypress.log({
      name: 'type',
      message: '***',
    })
  }

  // Wywołaj oryginalną komendę
  return originalFn(subject, text, options)
})

// Użycie nadpisanej komendy
cy.get('[name="password"]').type('secretPassword', { sensitive: true })

// 7. COMMAND Z ALIASEM I RETRY
Cypress.Commands.add('waitForResource', (resourceName, timeout = 10000) => {
  const started = Date.now()

  return cy.window().then((win) => {
    return new Cypress.Promise((resolve, reject) => {
      const check = () => {
        const elapsed = Date.now() - started

        if (win[resourceName]) {
          resolve(win[resourceName])
        } else if (elapsed > timeout) {
          reject(new Error(`Zasób ${resourceName} nie załadował się w ${timeout}ms`))
        } else {
          setTimeout(check, 100)
        }
      }

      check()
    })
  })
})

// Użycie
cy.waitForResource('myGlobalLibrary').then((library) => {
  expect(library.version).to.exist
})

// 8. COMMAND DO MOCKOWANIA DATY
Cypress.Commands.add('setDate', (date) => {
  cy.clock(new Date(date).getTime())
})

// Użycie
cy.setDate('2024-01-15')
cy.visit('/calendar') // Strona zobaczy zamrożoną datę

Przykład TypeScript definition:

// cypress/support/index.d.ts
declare namespace Cypress {
  interface Chainable {
    /**
     * Loguje użytkownika przez formularz
     * @param email - Email użytkownika
     * @param password - Hasło użytkownika
     * @example cy.login('user@example.com', 'password123')
     */
    login(email: string, password: string): Chainable<void>

    /**
     * Znajduje element po data-testid wewnątrz poprzedniego elementu
     * @param id - Wartość data-testid
     * @example cy.get('.container').getById('user-profile')
     */
    getById(id: string): Chainable<JQuery<HTMLElement>>

    /**
     * Wypełnia formularz na podstawie obiektu
     * @param formData - Obiekt z danymi formularza
     */
    fillForm(formData: Record<string, string | boolean | string[]>): Chainable<void>
  }
}

Diagram:

graph TD
    A[Test File] -->|używa| B[Custom Command]
    B -->|zdefiniowana w| C[commands.js]
    C -->|importowana przez| D[support/index.js]
    D -->|ładowana przez| E[Cypress]

    B --> F{Typ Command}
    F -->|Parent| G[Rozpoczyna łańcuch]
    F -->|Child| H[Kontynuuje łańcuch]
    F -->|Dual| I[Parent lub Child]

    B --> J[Może używać]
    J --> K[Standardowe komendy cy.*]
    J --> L[Inne Custom Commands]
    J --> M[JavaScript/TypeScript]

    style B fill:#f9f,stroke:#333
    style C fill:#bfb,stroke:#333

Materiały:

21. Jak używać cy.wrap() i cy.invoke()?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. Czym jest cy.task() i kiedy go używać?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
23. Jak korzystać z cy.intercept() do mockowania API?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
24. Jak używać cy.fixture() do ładowania danych testowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Testowanie API

25. Jak testować endpointy API za pomocą cy.request()?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
26. Jak mockować odpowiedzi serwera za pomocą cy.intercept()?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. Jak czekać na zakończenie żądań sieciowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
28. Jak testować autentykację i tokeny JWT?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Organizacja Testów

29. Jak organizować testy za pomocą describe(), context() i it()?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. Jak używać hooków before(), beforeEach(), after(), afterEach()?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
31. Jak pomijać lub uruchamiać wybrane testy (skip, only)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
32. Jak tagować i filtrować testy?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
33. Jak obsługiwać dane testowe i fixtures?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Zaawansowane Techniki

34. Jak testować aplikacje z iframe?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
35. Jak obsługiwać nowe okna i taby przeglądarki?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
36. Jak wykonywać testy wizualne (visual testing)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
37. Jak testować upload i download plików?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Jak konfigurować testy dla różnych środowisk?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

CI/CD i Raportowanie

39. Jak zintegrować Cypress z CI/CD (GitHub Actions, Jenkins)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
40. Jak generować raporty i nagrania wideo z testów?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 40 pytań rekrutacyjnych z Cypress oraz pozostałych technologii.

Zobacz plany cenowe