StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
QANext.jsDevOps

E2E testy w Next.js App Router – kompletny setup Cypress + CI/CD

Cypress E2E w Next.js App Router krok po kroku. Konfiguracja, fixtures, custom commands, CI i wzorce, które ograniczają flaky testy.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
31 października 2025 11:40
Czytanie
4 min czytania
Aktualizacja
25 maja 2026 12:48

Większość zespołów wpada w tę samą pułapkę: inwestują w testy E2E, ale o ich nieszczelności dowiadują się dopiero podczas pożaru na produkcji. Ekosystem Next.js App Router — pełen Server Actions, Suspense i Parallel Routes — wymaga konfiguracji, która faktycznie chroni aplikację, a nie tylko bezużytecznie świeci na zielono w CI.

Artykuł w skrócie

  • Fixtures zamiast hardkodowanych danych — pliki JSON w cypress/fixtures/ pozwalają reużywać dane testowe w wielu testach bez duplikacji
  • Custom Commands — przenieś powtarzające się sekwencje (logowanie, wypełnianie formularzy) do commands.ts; użyj cy.session() do cache'owania sesji między testami
  • data-testid selektory — odporne na refactor; nie polegaj na klasach CSS ani tekście, który może się zmienić
  • App Router wymaga baseUrl — ustaw baseUrl w cypress.config.ts i czekaj na hydration przed interakcją z klientem
  • CI: next build + next start — testuj zbudowaną aplikację, nie next dev; tylko wtedy routing, bundling i cache zachowują się jak na produkcji
  • GitHub Actions z cypress-io/github-action@v6 — używaj parametrów start i wait-on zamiast ręcznej konfiguracji serwera
  • Artefakty przy błędach — uploaduj screenshoty tylko if: failure(), nagrania if: always(), żeby nie śmiecić storage przy zielonych testach
Uwaga

Testy E2E uruchamiane przeciwko next dev dają fałszywe poczucie bezpieczeństwa. W CI testuj build produkcyjny, ponieważ wtedy weryfikujesz routing, bundling, middleware i zachowanie cache w warunkach zbliżonych do wdrożenia.

Krok 1: Instalacja i konfiguracja bazowa

Code
npm install -D cypress
npx cypress open

Wybierz E2E Testing → Chrome → Cypress wygeneruje strukturę plików.

Code
// cypress.config.ts
import { defineConfig } from 'cypress'
 
export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
    supportFile: 'cypress/support/e2e.ts',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false, // włącz w CI
    screenshotOnRunFailure: true,
    defaultCommandTimeout: 10000,
    retries: {
      runMode: 2, // retries w CI
      openMode: 0, // brak retries lokalnie
    },
  },
})

TypeScript – tsconfig dla Cypress

Code
// cypress/tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["es2017", "dom", "dom.iterable"],
    "types": ["cypress", "node"],
    "baseUrl": "..",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"]
}

Skrypty w package.json

Code
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "test:e2e": "start-server-and-test dev http://localhost:3000 cy:run",
    "test:e2e:open": "start-server-and-test dev http://localhost:3000 cy:open",
    "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run"
  }
}

start-server-and-test uruchamia serwer, czeka aż będzie dostępny i odpala testy:

Code
npm install -D start-server-and-test

Krok 2: Struktura projektu

Code
cypress/
├── e2e/
│   ├── auth/
│   │   ├── login.cy.ts
│   │   └── register.cy.ts
│   ├── products/
│   │   ├── product-list.cy.ts
│   │   └── product-detail.cy.ts
│   └── checkout/
│       └── checkout-flow.cy.ts
├── fixtures/
│   ├── user.json
│   └── api-responses/
│       ├── products.json
│       ├── login-success.json
│       └── login-error.json
├── support/
│   ├── commands.ts
│   ├── e2e.ts
│   └── types.d.ts
└── tsconfig.json

Grupuj testy według feature'ów, nie typów stron. W złożonych projektach nawigacja po auth/, products/, checkout/ jest o rząd wielkości szybsza niż szukanie testu w płaskiej liście plików.

Krok 3: Fixtures – dane testowe

Fixtures to statyczne pliki JSON z danymi testowymi. Trzymaj je w cypress/fixtures/ — izolują testy od stanu backendu i eliminują niedeterministyczne zachowania.

Code
// cypress/fixtures/user.json
{
  "validUser": {
    "email": "test@example.com",
    "password": "Test1234!",
    "name": "Jan Testowy"
  },
  "invalidUser": {
    "email": "wrong@example.com",
    "password": "bad"
  }
}
Code
// cypress/fixtures/api-responses/products.json
{
  "products": [
    {
      "id": "1",
      "name": "Laptop ThinkPad X1",
      "price": 599900,
      "category": "laptopy",
      "inStock": true
    },
    {
      "id": "2",
      "name": "Monitor Dell 27\"",
      "price": 149900,
      "category": "monitory",
      "inStock": false
    }
  ]
}

Użycie w teście:

Code
// cypress/e2e/products/product-list.cy.ts
describe('Product List', () => {
  beforeEach(() => {
    cy.fixture('api-responses/products.json').as('productsData')
  })
 
  it('displays products from API', function () {
    cy.intercept('GET', '/api/products', this.productsData).as('getProducts')
 
    cy.visit('/products')
    cy.wait('@getProducts')
 
    cy.get("[data-testid='product-card']").should('have.length', 2)
    cy.contains('Laptop ThinkPad X1')
    cy.contains('5 999,00 zł')
  })
 
  it('shows out of stock badge', function () {
    cy.intercept('GET', '/api/products', this.productsData)
    cy.visit('/products')
 
    cy.contains('Monitor Dell 27')
      .parents("[data-testid='product-card']")
      .find("[data-testid='out-of-stock']")
      .should('be.visible')
  })
})

Krok 4: Custom Commands

Custom commands eliminują powtarzalny kod — logowanie, nawigacja, setup stanu. Im mniej duplikatu w testach, tym niższy koszt utrzymania całego zestawu.

Code
// cypress/support/commands.ts
 
// Logowanie przez UI (wolne, ale testuje faktyczny flow)
Cypress.Commands.add('loginViaUI', (email: string, password: string) => {
  cy.visit('/login')
  cy.get("[data-testid='email-input']").type(email)
  cy.get("[data-testid='password-input']").type(password)
  cy.get("[data-testid='login-button']").click()
  cy.url().should('not.include', '/login')
})
 
// Logowanie przez API (szybkie, dla testów nie dotyczących auth)
// Uwaga: działa wyłącznie z tokenami w localStorage (JWT). Przy auth opartym
// na HttpOnly cookies (NextAuth, Auth.js) użyj loginViaUI lub cy.session().
Cypress.Commands.add('loginViaAPI', (email: string, password: string) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
  }).then((response) => {
    cy.window().then((win) => {
      win.localStorage.setItem('token', response.body.token)
    })
  })
})
 
// Logowanie przez session caching (najszybsze)
Cypress.Commands.add('login', (email?: string, password?: string) => {
  const userEmail = email ?? Cypress.env('TEST_USER_EMAIL')
  const userPassword = password ?? Cypress.env('TEST_USER_PASSWORD')
 
  cy.session(
    [userEmail],
    () => {
      cy.loginViaAPI(userEmail, userPassword)
    },
    {
      validate: () => {
        // Sprawdź czy sesja jest nadal aktywna; bez tego Cypress może
        // odtwarzać wygasłą sesję bez ponownego logowania
        cy.request({ url: '/api/auth/me', failOnStatusCode: false })
          .its('status')
          .should('eq', 200)
      },
    },
  )
})
 
// Helper do sprawdzania toast notifications
Cypress.Commands.add('shouldShowToast', (message: string) => {
  cy.get("[role='status']").should('contain.text', message)
})

Type definitions

Code
// cypress/support/types.d.ts
declare namespace Cypress {
  interface Chainable {
    loginViaUI(email: string, password: string): Chainable<void>
    loginViaAPI(email: string, password: string): Chainable<void>
    login(email?: string, password?: string): Chainable<void>
    shouldShowToast(message: string): Chainable<JQuery<HTMLElement>>
  }
}

Krok 5: Testowanie App Router – specyficzne sprawy

Loading states i Suspense

Next.js App Router intensywnie korzysta z loading.tsx i Suspense. Testy muszą to uwzględniać — sprawdzają nie tylko dane końcowe, ale cały cykl życia komponentu:

Code
it('shows loading skeleton while fetching data', () => {
  cy.intercept('GET', '/api/products', (req) => {
    req.on('response', (res) => {
      res.setDelay(2000) // symuluj wolny response
    })
  })
 
  cy.visit('/products')
 
  // Sprawdź loading state
  cy.get("[data-testid='product-skeleton']").should('be.visible')
 
  // Poczekaj na dane
  cy.get("[data-testid='product-card']", { timeout: 5000 }).should(
    'have.length.at.least',
    1,
  )
 
  // Loading powinien zniknąć
  cy.get("[data-testid='product-skeleton']").should('not.exist')
})

Server Actions

Uzależnianie testów od wewnętrznych mechanizmów Next.js to prosta droga do stworzenia niestabilnego zestawu, który wymaga poprawek po każdej aktualizacji frameworka.

Testuj efekt widoczny dla użytkownika — nie mechanizm:

Code
it('submits form via Server Action', () => {
  cy.login()
  cy.visit('/settings')
 
  cy.get("[name='displayName']").clear().type('Nowa Nazwa')
  cy.get("[data-testid='save-button']").click()
 
  cy.shouldShowToast('Ustawienia zapisane')
  cy.contains('Nowa Nazwa').should('be.visible')
})

Intercepcję zachowaj do zewnętrznych usług — gdy formularz wywołuje Stripe, zewnętrzne API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. Tu chodzi o konwencje plików Next.js, które zamieniają eksportowaną funkcję w gotowy plik sitemap.xml lub robots.txt. partnera albo webhook.

Parallel Routes i Intercepting Routes

Code
it('opens product modal via intercepting route', () => {
  cy.visit('/products')
  cy.get("[data-testid='product-card']").first().click()
 
  // Modal powinien się otworzyć (intercepting route)
  cy.get("[role='dialog']").should('be.visible')
  cy.get("[role='dialog']").contains('Laptop ThinkPad X1')
 
  // URL się zmienił
  cy.url().should('include', '/products/1')
 
  // Zamknij modal
  cy.get("[aria-label='Zamknij']").click()
  cy.get("[role='dialog']").should('not.exist')
  cy.url().should('eq', `${Cypress.config('baseUrl')}/products`)
})

Krok 6: Environment variables

Code
// cypress.config.ts
export default defineConfig({
  e2e: {
    // ...
    env: {
      TEST_USER_EMAIL: 'test@example.com',
      TEST_USER_PASSWORD: 'Test1234!',
      API_URL: 'http://localhost:3000/api',
    },
  },
})

Lub przez plik cypress.env.json (dodaj do .gitignore):

Code
{
  "TEST_USER_EMAIL": "test@example.com",
  "TEST_USER_PASSWORD": "secret123"
}

Krok 7: GitHub Actions CI/CD

Code
# .github/workflows/e2e.yml
name: E2E Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 15
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Build Next.js
        run: npm run build
 
      - name: Run Cypress E2E tests
        uses: cypress-io/github-action@v6
        with:
          install: false
          start: npm start
          wait-on: 'http://localhost:3000'
          wait-on-timeout: 120
          browser: chrome
          config: video=true
        env:
          CYPRESS_TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          CYPRESS_TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
 
      - name: Upload screenshots (on failure)
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots
          retention-days: 7
 
      - name: Upload videos
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cypress-videos
          path: cypress/videos
          retention-days: 7

Testy CI zawsze uruchamiaj przeciwko next build + next start. Różnica między next dev a buildem produkcyjnym to nie szczegół techniczny — to przepaść, która ukrywa błędy routingu, middleware i cache do momentu wdrożenia.

Optymalizacja czasu CI

Parallel execution z Cypress Cloud:

Code
e2e:
  runs-on: ubuntu-latest
  strategy:
    fail-fast: false
    matrix:
      containers: [1, 2, 3]
  steps:
    # ...
    - uses: cypress-io/github-action@v6
      with:
        record: true
        parallel: true
        group: 'E2E - Chrome'
      env:
        CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Bez Cypress Cloud – podział ręczny:

Code
strategy:
  matrix:
    spec:
      - 'cypress/e2e/auth/**'
      - 'cypress/e2e/products/**'
      - 'cypress/e2e/checkout/**'
steps:
  - uses: cypress-io/github-action@v6
    with:
      spec: ${{ matrix.spec }}

Krok 8: Dobre praktyki

cy.session() do cache'owania logowania — bez tego każdy test traci czas na powtórne logowanie i rośnie całkowity czas CI.

Testuj critical paths, nie każdą stronę — E2E testy są kosztowne, więc skoncentruj się na flow związanym z funkcjonowaniem biznesu: checkout, signup, core feature.

Selektory data-testid — odporne na refactor. Klasy CSS i teksty zmieniają się przy redesignie i wymagają aktualizacji dziesiątek testów.

Eliminuj cy.wait(ms) — używaj cy.wait("@alias") lub assertions z timeoutem, ponieważ hardkodowane waity to flaky testy — kosztują czas i zaufanie do zestawu.

Seeduj bazę danych przed testami — cy.task() do setupu i cleanup eliminuje wzajemne zanieczyszczanie testów:

Code
// cypress.config.ts
export default defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on('task', {
        async seedDatabase() {
          // reset DB, insert test data
          return null
        },
        async cleanDatabase() {
          // cleanup
          return null
        },
      })
    },
  },
})
Code
// W teście:
beforeEach(() => {
  cy.task('seedDatabase')
})
 
afterEach(() => {
  cy.task('cleanDatabase')
})

Werdykt Labu

Skonfiguruj środowisko raz, a dokładnie. Solidnym fundamentem będzie Cypress zintegrowany z Next.js App Router, wsparty przez TypeScript, pliki fixtures, własne komendy i żelazną dyscyplinę CI/CD (Continuous Integration / Continuous Deployment) to praktyka automatycznego integrowania, testowania i wdrażania kodu. CI uruchamia testy i build przy każdej zmianie, CD automatyzuje dostarczenie zmiany na środowisko docelowe — razem skracają drogę od commita do produkcji i wyłapują błędy wcześnie. w GitHub Actions.

Kluczem do sukcesu nie jest masowa produkcja testów, ale ich bezwzględna stabilność. Pięć scenariuszy testujących checkout i rejestrację na buildzie produkcyjnym daje lepszą ochronę niż pięćdziesiąt kruchych testów opartych o detale implementacji HTTP. Dane pokazują wprost: zestawy testowe, które sypią się przy każdej aktualizacji frameworka, są ostatecznie porzucane, a aplikacja zostaje bez tarczy.

Teraz przyszła pora na wdrożenie całego planu. Zacznij od jednej, absolutnie krytycznej ścieżki użytkownika, a potem testuj efekt widoczny na ekranie. Potem rozbudowowa testów powinna odbywać się wyłącznie tam, gdzie awaria na produkcji uderza bezpośrednio w biznes.

  • Krok 1: Instalacja i konfiguracja bazowa1 min
  • Krok 2: Struktura projektu1 min
  • Krok 3: Fixtures – dane testowe1 min
  • Krok 4: Custom Commands1 min
  • Krok 5: Testowanie App Router – specyficzne sprawy1 min
  • Krok 6: Environment variables1 min
  • Krok 7: GitHub Actions CI/CD1 min
  • Krok 8: Dobre praktyki1 min
  • Werdykt Labu1 min

Często zadawane pytania

Źródła i dokumentacjaZweryfikowano: 13 maja 2026

Materiały wykorzystane do weryfikacji artykułu „E2E testy w Next.js App Router – kompletny setup Cypress + CI/CD”:

Next.js testing with Cypress, Cypress Best Practices, Cypress cy.session(), Cypress GitHub Action.

Seria

Testowanie frontendu w Next.js
Część 2 / 3
  1. 1Cypress vs Playwright – który wybrać do projektu Next.js?
  2. E2E testy w Next.js App Router – kompletny setup Cypress + CI/CD
  3. 3Cypress Component Testing w React i Next.js — kiedy naprawdę ma sens
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Moje artykułyWięcej o mnie

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Cypress Component Testing w React i Next.js — kiedy naprawdę ma sens
Cypress Component Testing w React i Next.js — kiedy naprawdę ma sens

Cypress Component Testing w React i Next.js bez marketingowej mgły. Kiedy daje przewagę nad RTL, jak go skonfigurować i gdzie kończą się jego możliwości.

Maciej Sala

Maciej Sala

Founder Strivelab

6 października 2025
Cypress vs Playwright – który wybrać do projektu Next.js?
Cypress vs Playwright – który wybrać do projektu Next.js?

Cypress czy Playwright do projektu Next.js? Różnice w DX, CI, browser support, component testing i realne kryteria wyboru bez fanbojstwa.

Maciej Sala

Maciej Sala

Founder Strivelab

26 listopada 2025
App Router czy Pages Router — co wybrać?
App Router czy Pages Router — co wybrać?

App Router czy Pages Router w Next.js 16? Konkretne różnice, koszty migracji i praktyczne kryteria wyboru dla nowych oraz istniejących projektów.

Maciej Sala

Maciej Sala

Founder Strivelab

23 grudnia 2025
Poprzedni wpisGoogle Analytics 4 w Next.js App Router — konfiguracja z gtag i @next/third-partiesJak poprawnie wdrożyć GA4 w Next.js App Router: gtag, @next/third-parties, page_view przy client-side navigation, consent mode v2 i custom events bez chaosu w danych.
Maciej Sala

Maciej Sala

Founder Strivelab

18 października 2025
Następny wpisAsync/await to pułapka — kiedy Promise.all() uratuje Twoją wydajnośćSekwencyjne await potrafią niepotrzebnie wydłużyć czas odpowiedzi aplikacji. Zobacz, kiedy użyć Promise.all(), kiedy Promise.allSettled(), a kiedy ograniczyć równoległość.
Maciej Sala

Maciej Sala

Founder Strivelab

2 listopada 2025