Post

Cypress

Cypress

Pengenalan

Cypress telah merevolusi cara kita melakukan end-to-end testing. Dengan pendekatan yang berbeda dari Selenium, Cypress menawarkan developer experience yang superior dan test execution yang lebih cepat dan reliable.


Definisi Cypress

Apa itu Cypress?

Cypress adalah modern end-to-end testing framework yang dibangun untuk web applications modern. Cypress menjalankan dalam browser yang sama dengan aplikasi yang sedang ditest, memberikan kontrol penuh dan visibility yang luar biasa.

Definisi Formal:

1
2
3
Cypress adalah JavaScript-based end-to-end testing framework yang 
menyediakan comprehensive API untuk testing web applications dengan 
mudah, cepat, dan reliabel.

Sejarah Singkat Cypress

1
2
3
4
5
6
7
8
9
10
11
12
CYPRESS TIMELINE
═════════════════════════════════════════════════════════════

2014: Cypress project dimulai (Brian Mann)
2015: Cypress v0.1 beta release
2018: Cypress v3.0 (major release)
2020: Cypress v5.0 dengan peningkatan signifikan
2021: Cypress v8.0 (WebSocket support, debugging improvements)
2022: Cypress v9.0+ (TypeScript support, component testing)
2023: Cypress v12.0+ (Major version milestone)
2024: Cypress continues dengan fitur-fitur modern
2025: Cypress ecosystem terus berkembang

Core Characteristics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
✅ CYPRESS CHARACTERISTICS:

1. JavaScript-Based
   ├─ Same language as web applications
   ├─ Easy for developers to write tests
   ├─ npm ecosystem support
   └─ TypeScript support

2. Modern Architecture
   ├─ Runs in the same process as app
   ├─ Direct DOM access
   ├─ No browser plugins needed
   └─ True automation

3. Developer-Friendly
   ├─ Beautiful test runner UI
   ├─ Time travel debugging
   ├─ Real browser (Chrome, Firefox, Edge, Safari)
   └─ Fast execution

4. Comprehensive
   ├─ E2E testing
   ├─ Component testing (Cypress Components)
   ├─ Visual regression testing
   ├─ Network stubbing & mocking
   └─ API testing

Posisi dalam Testing Landscape

Testing Pyramid dengan Cypress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TESTING PYRAMID - Cypress Position
═════════════════════════════════════════════════════════════

            / \             E2E Testing (Cypress ⭐)
           /   \            Tests end-to-end workflows
          / E2E \           Slower, comprehensive
         /───────\          
        /         \         Integration Testing
       /Integration\        Tests component interactions
      /             \       Medium speed & complexity
     /───────────────\
    /                 \     Component/Unit Tests
   /    Component      \    Fastest, isolated 
  /─────────────────────\    
 ╰───────────────────────╯  

Cypress Scope:
  ✓ E2E Testing (Primary)
  ✓ Component Testing (Secondary)
  ✗ Unit Testing (Use Jest instead)
  ✗ Load Testing (Use dedicated tools)

Comparison: Testing Tools Position

ToolTypeLanguageSetupSpeed
JestUnitJS/TSEasyVery Fast
Cypress ⭐E2EJS/TSEasyFast
SeleniumE2EMultiMediumMedium
PlaywrightE2EMultiMediumVery Fast
TestCafeE2EJSMediumFast
WebdriverIOE2EJSMediumMedium

CYPRESS STRENGTH: Developer experience + E2E coverage + Speed


Keunggulan Cypress

1. 🏗️ Arsitektur Unik

Cypress Architecture (Modern)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CYPRESS ARCHITECTURE - Revolutionary Approach
═════════════════════════════════════════════════════════════

┌──────────────────────────────────────────────────────────┐
│ Your Test Code (JavaScript/TypeScript)                   │
│ describe('Login', () => {                                │
│   it('should login successfully', () => { ... })         │
│ })                                                        │
└────────────────────┬─────────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────────┐
│ Cypress Core (Node.js Process)                           │
│ ├─ Proxy Server                                          │
│ ├─ DOM Access                                            │
│ ├─ State Management                                      │
│ └─ Command Queue                                         │
└────────────────────┬─────────────────────────────────────┘
                     │
┌────────────────────▼─────────────────────────────────────┐
│ Real Browser (Chrome, Firefox, Edge, Safari)            │
│ ├─ Actual Application                                    │
│ ├─ Real DOM                                              │
│ └─ Real Network Calls                                    │
└──────────────────────────────────────────────────────────┘

KEY DIFFERENCE FROM SELENIUM:
  Selenium:  Test Code → WebDriver → Browser
  Cypress:   Test Code → Proxy → Browser (Same Process!)

Keunggulan Cypress Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
✅ CYPRESS ARCHITECTURE BENEFITS:

1. Direct DOM Access
   ├─ Can read/write DOM directly
   ├─ No network latency
   ├─ Instant element interaction
   └─ Better debuggability

2. Network Control
   ├─ Intercept all network requests
   ├─ Stub/mock responses
   ├─ Control delays
   ├─ Simulate errors
   └─ Full visibility

3. Same Process Execution
   ├─ No WebDriver overhead
   ├─ Faster execution
   ├─ Better error messages
   ├─ True debugging
   └─ State preservation

4. Object Access
   ├─ Access application state
   ├─ Call functions directly
   ├─ Modify data
   ├─ Skip unnecessary steps
   └─ Precise testing

Perbandingan dengan Selenium

AspekSeleniumCypress
ArchitectureJSON WireDirect DOM
Process IsolationSeparateSame ⭐
DOM AccessLimitedFull ⭐
Network ControlNoYes ⭐
SpeedMediumFast ⭐
DebuggingHardEasy ⭐
Network OverheadHighNone ⭐
Error MessagesGenericDetailed ⭐

2. 🎮 Test Runner UI (Beautiful & Intuitive)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CYPRESS TEST RUNNER FEATURES
═════════════════════════════════════════════════════════════

┌─────────────────────────────────────────────────────────┐
│ Cypress Test Runner                                     │
│ ┌─────────┬─────────────────────────────────────────────┤
│ │ Specs   │ Your Application & Browser                  │
│ │         │ ┌───────────────────────────────────────┐   │
│ │ ✓ Login │ │ Application runs here in real browser │  │
│ │ ✓ Cart  │ │ Watch it being tested live!           │  │
│ │ ✓ Chk.. │ │                                       │  │
│ │         │ │ Real time feedback & visualization    │   │
│ │ ┌─────┐ │ │                                       │   │
│ │ │ ... │ │ └───────────────────────────────────────┘   │
│ └─────────┴─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Command Log                     Test Info           │ │
│ │ ✓ visit()    0ms    Page loaded │ Duration: 2.5s    │ │
│ │ ✓ get()      5ms    Element found│ Status: Passed   │ │
│ │ ✓ type()     10ms   Text entered │ Browser: Chrome  │ │
│ │ ✓ click()    15ms   Button clicked                  │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

FEATURES:
  ✓ Live browser preview
  ✓ Command log dengan timing
  ✓ Step-by-step execution
  ✓ Real-time feedback
  ✓ Beautiful UI

3. ⏱️ Time Travel Debugging

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CYPRESS TIME TRAVEL DEBUGGING
═════════════════════════════════════════════════════════════

Normal Debugging:
  1. Test runs
  2. Test fails
  3. Check logs (tidak detail)
  4. Modify code
  5. Run again
  6. REPEAT ❌ Time-consuming!

Cypress Time Travel Debugging:
  1. Run test
  2. Hover over commands in log
  3. See exact state at that moment ⭐
  4. Browser shows what it looked like
  5. Modify test immediately
  6. Re-run instantly
  
BENEFIT: Dapat melihat state aplikasi di setiap step!

Example:
  Test fails at "click button"
  
  Normal: "Tapi button mana yang click? Apa valuenya?"
  Cypress: Hover over "click()" command
           → Browser shows exact moment sebelum & sesudah
           → Bisa lihat semua DOM elements
           → Instant understanding!

4. ⚡ Fast & Reliable Execution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
SPEED COMPARISON - Real World Benchmarks
═════════════════════════════════════════════════════════════

Test Case: Login to website

Selenium (Python):
  Setup:        2s
  Navigate:     3s
  Find element: 2s
  Type:         1s
  Click:        1s
  Wait:         2s
  ─────────────
  Total:        11s

Cypress (JavaScript):
  Setup:        1s
  Navigate:     1s
  Find element: 0.1s (DOM direct access)
  Type:         0.2s
  Click:        0.1s
  Wait:         0.2s
  ─────────────
  Total:        2.6s ⭐ 4x faster!

WHY FASTER?
  ✓ No network latency
  ✓ Direct DOM access
  ✓ Same process execution
  ✓ Optimized architecture
  ✓ Smart waits (auto-retry)

5. 📊 Built-in Network Stubbing & Mocking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NETWORK STUBBING - Powerful Feature
═════════════════════════════════════════════════════════════

✅ What you can do:

  cy.intercept('GET', '/api/users')
    .as('getUsers')
    .visit('/users')
    .wait('@getUsers')
    .then(({ response }) => {
      expect(response.statusCode).to.equal(200)
    })

  // Stub API response
  cy.intercept('POST', '/api/login', {
    statusCode: 200,
    body: { token: 'abc123' }
  }).as('login')

  // Simulate network error
  cy.intercept('GET', '/api/data', {
    statusCode: 500,
    body: { error: 'Server error' }
  })

  // Delay response
  cy.intercept('/api/*', (req) => {
    req.reply((res) => {
      res.delay(1000)  // Add 1 second delay
    })
  })

BENEFITS:
  ✓ Tidak perlu backend untuk test
  ✓ Test API edge cases
  ✓ Simulate failures
  ✓ Control network conditions
  ✓ Faster tests (no real API calls)

6. 📝 Excellent Documentation & Developer Experience

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
DEVELOPER EXPERIENCE
═════════════════════════════════════════════════════════════

Cypress vs Selenium:

Selenium:
  - Documentation: Good
  - Examples: Basic
  - Error messages: Generic
  - Community: Large but fragmented
  - Learning curve: Medium
  - Stack Overflow: Mixed quality

Cypress: ⭐
  - Documentation: Excellent (best-in-class)
  - Examples: Comprehensive
  - Error messages: Detailed & actionable
  - Community: Active & helpful
  - Learning curve: Easy
  - Official support: Excellent

Example Error Message:

Selenium:
  "NoSuchElementException: Cannot locate element..."

Cypress:
  "cy.get() failed because element with selector 
   '.submit-button' does not exist.
   
   Tried to find:
     '.submit-button'
   
   Searched the entire DOM and it was not found.
   
   Suggestions:
   - If selector is dynamic, use cy.contains()
   - Check if element loads after delay
   - Use cy.wait() if element loads async"

MUCH BETTER! ⭐

7. 🔄 Smart Wait & Retry Logic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
AUTOMATIC WAITS & RETRIES
═════════════════════════════════════════════════════════════

Selenium:
  element = wait.until(
    EC.presence_of_element_located((By.ID, "button"))
  )
  # Manual configuration needed

Cypress: ⭐ Automatic!
  cy.get('#button')  // Auto-waits up to 4 seconds
                     // Auto-retries if not found
                     // Smart timeouts

Built-in Retry Logic:
  cy.get('.item')    // Retry 4 seconds by default
  cy.get('.item', { timeout: 10000 })  // Custom timeout
  
  // Smart waiting:
  // 1. Element not found → Retry
  // 2. Element not visible → Retry
  // 3. Element not clickable → Retry
  // 4. After timeout → Fail with helpful message

BENEFIT: No manual waits needed! ⭐

8. 💻 Modern JavaScript/TypeScript Support

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
MODERN DEVELOPMENT
═════════════════════════════════════════════════════════════

Cypress supports:
  ✓ ES6+ JavaScript
  ✓ TypeScript
  ✓ Async/await
  ✓ Promises
  ✓ Arrow functions
  ✓ Destructuring
  ✓ Modern libraries (React, Vue, Angular)

Example - TypeScript Support:

// tests/login.cy.ts
describe('Login', () => {
  it('should login successfully', () => {
    cy.visit('https://example.com')
    
    const username: string = 'testuser'
    const password: string = 'password123'
    
    cy.get('[data-testid="username"]')
      .type(username)
    cy.get('[data-testid="password"]')
      .type(password)
    cy.get('button[type="submit"]')
      .click()
    
    cy.url().should('include', '/dashboard')
  })
})

BENEFITS:
  ✓ Type safety
  ✓ Autocomplete in IDE
  ✓ Catch errors early
  ✓ Better code maintainability

Summary: Keunggulan Cypress

CYPRESS vs SELENIUM COMPARISON
FeatureSeleniumCypress ⭐
ArchitectureWebDriverDirect DOM
Setup ComplexityMediumEasy ⭐
Learning CurveMediumEasy ⭐
Execution SpeedMediumFast ⭐
Debug ExperienceHardEasy ⭐
Test Runner UINoneBeautiful ⭐
Time Travel DebuggingNoYes ⭐
Network StubbingComplexEasy ⭐
Error MessagesGenericDetailed ⭐
DocumentationGoodExcellent ⭐
Mobile SupportYesLimited
Multi-browserYes ⭐Yes
Language SupportMultipleJS/TS
CommunityVery LargeGrowing ⭐
Job MarketHigh ⭐Growing

VERDICT: Cypress for modern web apps ⭐ Selenium for legacy/cross-browser


Instalasi Cypress

Prerequisites

1
2
3
4
5
6
7
8
9
10
11
12
Sebelum install Cypress, pastikan:

✓ Node.js 12+ sudah terinstall
✓ npm atau yarn tersedia
✓ Internet connection untuk download
✓ Terminal/Command prompt access

Cek Node.js:
  node --version
  
Cek npm:
  npm --version

Step 1: Create Project Directory

1
2
3
4
5
6
# Create new directory
mkdir cypress_project
cd cypress_project

# Initialize npm project (jika belum ada package.json)
npm init -y

Step 2: Install Cypress via NPM

1
2
3
4
5
6
7
8
# Install Cypress
npm install --save-dev cypress

# Atau gunakan Yarn
yarn add --dev cypress

# Verify installation
npx cypress --version

Step 3: Open Cypress Test Runner (First Time)

1
2
3
4
5
# Open Cypress (akan generate folder structure)
npx cypress open

# Atau langsung run headless
npx cypress run

Step 4: Project Structure

Setelah cypress open, struktur folder akan dibuat:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cypress_project/
├── cypress/
│   ├── fixtures/           # Test data files
│   ├── e2e/               # E2E tests (main)
│   │   └── spec.cy.js     # Example test
│   ├── support/
│   │   ├── e2e.js         # E2E support
│   │   └── commands.js    # Custom commands
│   └── screenshots/       # Auto-generated
│   └── videos/            # Auto-generated
├── node_modules/
├── package.json
├── cypress.config.js      # Configuration
└── .gitignore

Step 5: Configure Cypress (cypress.config.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    // Base URL untuk testing
    baseUrl: 'http://localhost:3000',
    
    // Default timeout (ms)
    defaultCommandTimeout: 4000,
    
    // Request timeout
    requestTimeout: 5000,
    
    // Response timeout
    responseTimeout: 5000,
    
    // Setup hooks
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
  
  // Component testing
  component: {
    devServer: {
      framework: 'react',
      bundler: 'webpack',
    },
  },
})

Step 6: Create First Test

1
2
# Create test file
touch cypress/e2e/example.cy.js
1
2
3
4
5
6
7
// cypress/e2e/example.cy.js
describe('Example Tests', () => {
  it('should visit example.com', () => {
    cy.visit('https://example.com')
    cy.title().should('include', 'Example')
  })
})

Step 7: Run Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Open Cypress Test Runner (Interactive)
npx cypress open

# Run all tests headless (CI/CD)
npx cypress run

# Run specific test file
npx cypress run --spec "cypress/e2e/example.cy.js"

# Run with specific browser
npx cypress run --browser chrome
npx cypress run --browser firefox
npx cypress run --browser edge

# Run with video recording
npx cypress run --record

Complete Setup Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# setup-cypress.sh (macOS/Linux) atau setup-cypress.bat (Windows)

#!/bin/bash

# Create project
mkdir cypress_project
cd cypress_project

# Initialize npm
npm init -y

# Install Cypress
npm install --save-dev cypress

# Add scripts to package.json
npm set-script test "cypress open"
npm set-script test:headless "cypress run"

# Create test directories
mkdir -p cypress/e2e
mkdir -p cypress/fixtures
mkdir -p cypress/support

# Open Cypress
npm test

package.json Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "name": "cypress_project",
  "version": "1.0.0",
  "description": "Cypress E2E Testing",
  "scripts": {
    "test": "cypress open",
    "test:headless": "cypress run",
    "test:headed": "cypress run --headed",
    "test:chrome": "cypress run --browser chrome",
    "test:firefox": "cypress run --browser firefox",
    "test:watch": "cypress open --dev"
  },
  "devDependencies": {
    "cypress": "^13.0.0"
  }
}

Struktur File Test

Test File Anatomy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// cypress/e2e/login.cy.js

// 1. DESCRIBE - Describe the feature/module
describe('Login Feature', () => {
  
  // 2. BEFORE HOOKS - Setup sebelum tests
  before(() => {
    // Run once before all tests in suite
    console.log('Setting up test suite')
  })
  
  beforeEach(() => {
    // Run before each test
    cy.visit('https://example.com/login')
  })
  
  // 3. IT - Individual test case
  it('should login with valid credentials', () => {
    // Arrange
    const username = 'testuser'
    const password = 'password123'
    
    // Act
    cy.get('[data-testid="username"]').type(username)
    cy.get('[data-testid="password"]').type(password)
    cy.get('button[type="submit"]').click()
    
    // Assert
    cy.url().should('include', '/dashboard')
    cy.get('.welcome-message').should('be.visible')
  })
  
  it('should show error for invalid credentials', () => {
    cy.get('[data-testid="username"]').type('wrong')
    cy.get('[data-testid="password"]').type('wrong')
    cy.get('button[type="submit"]').click()
    
    cy.get('.error-message').should('contain', 'Invalid credentials')
  })
  
  it('should handle empty fields', () => {
    cy.get('button[type="submit"]').click()
    
    cy.get('[data-testid="username"]')
      .should('have.attr', 'aria-invalid', 'true')
  })
  
  // 4. AFTER HOOKS - Cleanup setelah tests
  afterEach(() => {
    // Run after each test
    // Useful for cleanup
  })
  
  after(() => {
    // Run once after all tests
    console.log('Tearing down test suite')
  })
})

Hooks Detailed Explanation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
CYPRESS HOOKS - Test Lifecycle
═════════════════════════════════════════════════════════════

Test Execution Order:

before()
  ↓
beforeEach()
  ↓
it('test 1', () => { ... })
  ↓
afterEach()
  ↓
beforeEach()
  ↓
it('test 2', () => { ... })
  ↓
afterEach()
  ↓
after()

USAGE:

before()      - Setup resources once (login, seeding)
beforeEach()  - Reset before each test (visit page)
afterEach()   - Cleanup after each test
after()       - Final cleanup

Example:

before(() => {
  // Login once (expensive operation)
  cy.visit('/login')
  cy.get('#username').type('user')
  cy.get('#password').type('pass')
  cy.get('button').click()
})

beforeEach(() => {
  // Before each test, go to dashboard
  cy.visit('/dashboard')
})

afterEach(() => {
  // After each test, clear cookies
  cy.clearCookies()
})

after(() => {
  // Final cleanup
  console.log('All tests completed')
})

Describe Blocks (Nested)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
describe('Shopping Cart', () => {
  
  describe('Adding Items', () => {
    it('should add item to cart', () => {
      // Test code
    })
    
    it('should update quantity', () => {
      // Test code
    })
  })
  
  describe('Removing Items', () => {
    it('should remove item from cart', () => {
      // Test code
    })
    
    it('should clear entire cart', () => {
      // Test code
    })
  })
  
  describe('Checkout', () => {
    it('should show checkout page', () => {
      // Test code
    })
    
    it('should process payment', () => {
      // Test code
    })
  })
})

// Output:
// Shopping Cart
//   Adding Items
//     ✓ should add item to cart
//     ✓ should update quantity
//   Removing Items
//     ✓ should remove item from cart
//     ✓ should clear entire cart
//   Checkout
//     ✓ should show checkout page
//     ✓ should process payment

Test Data Organization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// cypress/fixtures/users.json
{
  "validUser": {
    "username": "standard_user",
    "password": "secret_sauce"
  },
  "lockedUser": {
    "username": "locked_out_user",
    "password": "secret_sauce"
  },
  "invalidUser": {
    "username": "wrong_user",
    "password": "wrong_password"
  }
}

// cypress/e2e/login.cy.js
describe('Login with Test Data', () => {
  
  beforeEach(() => {
    cy.fixture('users').as('userData')
  })
  
  it('should login with valid user', function() {
    cy.visit('https://www.saucedemo.com')
    
    const user = this.userData.validUser
    cy.get('#user-name').type(user.username)
    cy.get('#password').type(user.password)
    cy.get('#login-button').click()
    
    cy.url().should('include', '/inventory')
  })
})

Perintah Dasar Cypress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cy.visit() - Navigate to URL
cy.visit('https://www.example.com')
cy.visit('/dashboard')  // If baseUrl configured
cy.visit('/', { method: 'POST' })  // With method

// cy.url() - Get current URL
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')

// cy.go() - Navigate back/forward
cy.go('back')
cy.go('forward')
cy.go(-1)
cy.go(1)

// cy.reload() - Refresh page
cy.reload()
cy.reload('on-failure')

Finding Elements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// cy.get() - Find element by selector (MOST COMMON)
cy.get('#username')              // By ID
cy.get('.submit-button')         // By class
cy.get('[data-testid="button"]') // By attribute
cy.get('input[type="password"]') // Complex selector

// cy.contains() - Find by text content
cy.contains('Login')
cy.contains('button', 'Submit')
cy.contains('a', /sign up/i)     // With regex

// cy.getByTestId() - Custom command
cy.getByTestId('username')       // If configured

// cy.find() - Find within element
cy.get('.form').find('input')

// cy.eq() - Get element by index
cy.get('.item').eq(0)            // First item
cy.get('.item').eq(-1)           // Last item

// cy.first() / cy.last()
cy.get('.item').first()
cy.get('.item').last()

// cy.filter() - Filter elements
cy.get('li').filter('.active')

// cy.parent() / cy.parents()
cy.get('.child').parent()
cy.get('.child').parents('.container')

// cy.siblings()
cy.get('.item').siblings()

// cy.next() / cy.prev()
cy.get('.item').next()
cy.get('.item').prev()

Interaction Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// cy.type() - Type text into input
cy.get('#username').type('testuser')
cy.get('#password').type('password123')

// cy.clear() - Clear input value
cy.get('#search').clear()
cy.get('#search').clear().type('new value')

// cy.click() - Click element
cy.get('#submit-button').click()
cy.get('button').click()
cy.get('.checkbox').click({ force: true })  // Force click

// cy.dblclick() - Double click
cy.get('#item').dblclick()

// cy.rightclick() - Right click
cy.get('#menu').rightclick()

// cy.check() - Check checkbox/radio
cy.get('#agree-terms').check()
cy.get('input[type="radio"][value="yes"]').check()

// cy.uncheck() - Uncheck checkbox
cy.get('#newsletter').uncheck()

// cy.select() - Select dropdown option
cy.get('#country').select('USA')
cy.get('#country').select(['USA', 'Canada'])

// cy.submit() - Submit form
cy.get('form').submit()

// cy.focus() / cy.blur()
cy.get('#email').focus()
cy.get('#email').blur()

// cy.hover()
cy.get('#dropdown').hover()

// cy.type with special keys
cy.get('#input').type('{enter}')
cy.get('#input').type('{backspace}')
cy.get('#input').type('{tab}')
cy.get('#input').type('{ctrl}a')

Assertion Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// cy.should() - Primary assertion method
cy.get('#message').should('be.visible')
cy.get('#error').should('not.be.visible')
cy.get('#username').should('have.value', 'testuser')
cy.get('#submit').should('not.be.disabled')

// cy.expect() - Alternative (less common)
expect(true).to.be.true

// cy.and() - Chaining assertions
cy.get('#username')
  .should('have.value', 'user')
  .and('be.visible')
  .and('have.attr', 'type', 'text')

// Common assertions:
// ─────────────────────────────────

// Visibility
.should('be.visible')
.should('not.be.visible')
.should('exist')
.should('not.exist')

// Value & Text
.should('have.value', 'expected')
.should('have.text', 'Click me')
.should('contain', 'partial text')

// Classes & Attributes
.should('have.class', 'active')
.should('have.attr', 'href', '/dashboard')
.should('have.id', 'main-form')

// State
.should('be.enabled')
.should('be.disabled')
.should('be.checked')
.should('have.length', 3)

// DOM Structure
.should('have.descendants', 'li')
.should('be.empty')

Waiting Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// cy.wait() - Wait for specific time
cy.wait(2000)  // Wait 2 seconds

// cy.wait() for intercepted request
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')  // Wait for API call

// cy.waitFor() - Wait for condition
cy.get('#loading').should('not.exist')  // Auto-waits

// Default waiting (implicit)
cy.get('#element')  // Auto-waits 4 seconds

// Custom timeout
cy.get('#slow-element', { timeout: 10000 })

Advanced Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// cy.each() - Iterate over elements
cy.get('.item').each(($el, index) => {
  cy.wrap($el).should('contain', `Item ${index}`)
})

// cy.invoke() - Call method on element
cy.get('#toggle').invoke('show')
cy.get('form').invoke('attr', 'method')

// cy.exec() - Execute system command
cy.exec('npm run build')

// cy.task() - Execute custom task
cy.task('db:seed')

// cy.request() - Make HTTP request
cy.request('GET', '/api/users')
cy.request('POST', '/api/login', {
  username: 'user',
  password: 'pass'
})

// cy.debug() - Debug point
cy.get('#element').debug()

// cy.log() - Log message
cy.log('Starting login test')

// cy.screenshot() - Take screenshot
cy.screenshot()
cy.screenshot('login-page')

Studi Kasus: Live Coding SauceDemo Login

Project Setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Create project
mkdir cypress-saucedemo
cd cypress-saucedemo

# Initialize npm
npm init -y

# Install Cypress
npm install --save-dev cypress

# Create test file
mkdir -p cypress/e2e
touch cypress/e2e/saucedemo-login.cy.js

# Create configuration
touch cypress.config.js

Configuration File

File: cypress.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'https://www.saucedemo.com',
    defaultCommandTimeout: 5000,
    requestTimeout: 5000,
    responseTimeout: 5000,
    viewportWidth: 1280,
    viewportHeight: 720,
    
    setupNodeEvents(on, config) {
      // Implement node event listeners here
    },
  },
})

Test Fixtures

File: cypress/fixtures/saucedemo-users.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "validUser": {
    "username": "standard_user",
    "password": "secret_sauce"
  },
  "lockedUser": {
    "username": "locked_out_user",
    "password": "secret_sauce"
  },
  "problemUser": {
    "username": "problem_user",
    "password": "secret_sauce"
  },
  "invalidUser": {
    "username": "invalid_user",
    "password": "wrong_password"
  },
  "emptyCredentials": {
    "username": "",
    "password": ""
  }
}

Custom Commands

File: cypress/support/commands.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Custom command for login
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/')
  cy.get('#user-name').type(username)
  cy.get('#password').type(password)
  cy.get('#login-button').click()
})

// Custom command to verify login success
Cypress.Commands.add('verifyLoginSuccess', () => {
  cy.url().should('include', '/inventory')
  cy.get('.inventory_list').should('be.visible')
  cy.get('.inventory_item').should('have.length.greaterThan', 0)
})

// Custom command to verify login error
Cypress.Commands.add('verifyLoginError', (errorMessage) => {
  cy.get('[data-test="error"]').should('be.visible')
  cy.get('[data-test="error"]').should('contain', errorMessage)
})

Main Test File

File: cypress/e2e/saucedemo-login.cy.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/**
 * SauceDemo Login Tests
 * 
 * Test Cases:
 * 1. Valid login
 * 2. Invalid credentials
 * 3. Locked out user
 * 4. Empty fields
 * 5. Logout functionality
 */

describe('SauceDemo Login Feature', () => {
  
  // Load test data
  beforeEach(() => {
    cy.fixture('saucedemo-users').as('users')
  })
  
  // ========== TEST CASE 1: Valid Login ==========
  describe('Valid Login', () => {
    
    it('TC-001: should login successfully with valid credentials', function() {
      // ARRANGE
      const user = this.users.validUser
      cy.log(`Logging in as: ${user.username}`)
      
      // ACT
      cy.visit('/')
      cy.get('#user-name').should('be.visible').type(user.username)
      cy.get('#password').should('be.visible').type(user.password)
      cy.get('#login-button').should('be.enabled').click()
      
      // ASSERT
      cy.url().should('include', '/inventory')
      cy.get('.inventory_list').should('be.visible')
      cy.get('.inventory_item').should('have.length', 6)
      cy.get('.title').should('contain', 'Products')
      
      // Log success
      cy.log('✓ Login successful')
    })
    
    it('TC-002: should display products after login', function() {
      const user = this.users.validUser
      
      // Login
      cy.login(user.username, user.password)  // Using custom command
      
      // Verify products displayed
      cy.get('.inventory_item').each(($item) => {
        cy.wrap($item).find('.inventory_item_name').should('be.visible')
        cy.wrap($item).find('.inventory_item_price').should('be.visible')
        cy.wrap($item).find('button').should('be.visible')
      })
    })
  })
  
  // ========== TEST CASE 2: Invalid Login ==========
  describe('Invalid Login', () => {
    
    it('TC-003: should show error for invalid credentials', function() {
      const user = this.users.invalidUser
      
      // ACT
      cy.visit('/')
      cy.get('#user-name').type(user.username)
      cy.get('#password').type(user.password)
      cy.get('#login-button').click()
      
      // ASSERT
      cy.verifyLoginError('Username and password do not match')
    })
    
    it('TC-004: should show error for locked out user', function() {
      const user = this.users.lockedUser
      
      cy.visit('/')
      cy.get('#user-name').type(user.username)
      cy.get('#password').type(user.password)
      cy.get('#login-button').click()
      
      cy.get('[data-test="error"]').should('contain', 'locked out')
    })
  })
  
  // ========== TEST CASE 3: Empty Fields ==========
  describe('Form Validation', () => {
    
    it('TC-005: should show error when username is empty', () => {
      cy.visit('/')
      cy.get('#password').type('secret_sauce')
      cy.get('#login-button').click()
      
      cy.get('[data-test="error"]').should('contain', 'Username')
    })
    
    it('TC-006: should show error when password is empty', () => {
      cy.visit('/')
      cy.get('#user-name').type('standard_user')
      cy.get('#login-button').click()
      
      cy.get('[data-test="error"]').should('contain', 'Password')
    })
    
    it('TC-007: should show error when both fields empty', () => {
      cy.visit('/')
      cy.get('#login-button').click()
      
      cy.get('[data-test="error"]').should('contain', 'Username')
    })
  })
  
  // ========== TEST CASE 4: UI/UX Tests ==========
  describe('Login Page UI', () => {
    
    it('TC-008: should display login page elements', () => {
      cy.visit('/')
      
      // Check page title
      cy.get('[data-test="login-container"]').should('be.visible')
      
      // Check input fields
      cy.get('#user-name').should('have.attr', 'placeholder', 'Username')
      cy.get('#password').should('have.attr', 'placeholder', 'Password')
      
      // Check button
      cy.get('#login-button').should('have.text', 'LOGIN')
      cy.get('#login-button').should('not.be.disabled')
    })
    
    it('TC-009: should clear error message when typing', function() {
      const user = this.users.invalidUser
      
      cy.visit('/')
      cy.get('#user-name').type(user.username)
      cy.get('#password').type(user.password)
      cy.get('#login-button').click()
      
      // Error should be visible
      cy.get('[data-test="error"]').should('be.visible')
      
      // Clear and type again
      cy.get('#user-name').clear().type('standard_user')
      
      // Error message should disappear
      cy.get('[data-test="error"]').should('not.exist')
    })
  })
  
  // ========== TEST CASE 5: Post-Login ==========
  describe('Post-Login Functionality', () => {
    
    beforeEach(function() {
      // Login before each test
      const user = this.users.validUser
      cy.login(user.username, user.password)
    })
    
    it('TC-010: should display user menu', () => {
      cy.get('.bm-burger-button').should('be.visible')
      cy.get('.bm-burger-button').click()
      
      cy.get('.bm-menu').should('be.visible')
      cy.get('a#logout_sidebar_link').should('contain', 'Logout')
    })
    
    it('TC-011: should logout successfully', () => {
      // Open menu
      cy.get('.bm-burger-button').click()
      cy.get('a#logout_sidebar_link').click()
      
      // Verify back at login page
      cy.url().should('eq', 'https://www.saucedemo.com/')
      cy.get('#user-name').should('be.visible')
    })
    
    it('TC-012: should add item to cart', () => {
      // Find first product add button
      cy.get('.inventory_item').first()
        .find('button')
        .should('contain', 'Add to cart')
        .click()
      
      // Verify button changed
      cy.get('.inventory_item').first()
        .find('button')
        .should('contain', 'Remove')
      
      // Verify cart badge updated
      cy.get('.shopping_cart_badge').should('have.text', '1')
    })
  })
  
  // ========== TEST CASE 6: Integration Test ==========
  describe('Full Login & Purchase Flow', () => {
    
    it('TC-013: should complete login and add multiple items', function() {
      const user = this.users.validUser
      
      // STEP 1: Login
      cy.log('STEP 1: Login')
      cy.login(user.username, user.password)
      cy.verifyLoginSuccess()
      
      // STEP 2: Add first item
      cy.log('STEP 2: Add first product to cart')
      cy.get('.inventory_item').eq(0).find('button').click()
      cy.get('.shopping_cart_badge').should('have.text', '1')
      
      // STEP 3: Add second item
      cy.log('STEP 3: Add second product to cart')
      cy.get('.inventory_item').eq(1).find('button').click()
      cy.get('.shopping_cart_badge').should('have.text', '2')
      
      // STEP 4: Go to cart
      cy.log('STEP 4: Navigate to shopping cart')
      cy.get('.shopping_cart_link').click()
      cy.url().should('include', '/cart')
      
      // STEP 5: Verify items in cart
      cy.log('STEP 5: Verify items in cart')
      cy.get('.cart_item').should('have.length', 2)
      
      cy.log('✓ Full flow test passed')
    })
  })
})

Run Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Interactive test runner
npm test

# Or use Cypress open command
npx cypress open

# Headless run
npm run test:headless

# Run specific test
npx cypress run --spec "cypress/e2e/saucedemo-login.cy.js"

# Run with specific browser
npx cypress run --browser chrome
npx cypress run --browser firefox

# Run with video
npx cypress run --record --key your-key

Expected Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SauceDemo Login Feature
  Valid Login
    ✓ TC-001: should login successfully with valid credentials (2.5s)
    ✓ TC-002: should display products after login (2.1s)
  Invalid Login
    ✓ TC-003: should show error for invalid credentials (1.8s)
    ✓ TC-004: should show error for locked out user (1.7s)
  Form Validation
    ✓ TC-005: should show error when username is empty (1.5s)
    ✓ TC-006: should show error when password is empty (1.5s)
    ✓ TC-007: should show error when both fields empty (1.6s)
  Login Page UI
    ✓ TC-008: should display login page elements (1.2s)
    ✓ TC-009: should clear error message when typing (1.9s)
  Post-Login Functionality
    ✓ TC-010: should display user menu (1.8s)
    ✓ TC-011: should logout successfully (2.0s)
    ✓ TC-012: should add item to cart (1.9s)
  Full Login & Purchase Flow
    ✓ TC-013: should complete login and add multiple items (5.2s)

===============================================
  13 passing (28.7s)
===============================================

Best Practices

Cypress Best Practices

1. Use Data Attributes for Selectors

1
2
3
4
5
6
7
8
9
10
11
// ❌ BAD: Fragile selectors
cy.get('button.btn.btn-primary.submit')
cy.get('div > form > input')

// ✅ GOOD: Stable test IDs
cy.get('[data-testid="submit-button"]')
cy.get('[data-testid="username-input"]')

// HTML should have:
<button data-testid="submit-button">Submit</button>
<input data-testid="username-input" />

2. Don’t Test Implementation Details

1
2
3
4
5
6
7
8
// ❌ BAD: Testing internal state
cy.window().then(win => {
  expect(win.app.state.user.logged_in).to.be.true
})

// ✅ GOOD: Test user behavior
cy.get('.dashboard').should('be.visible')
cy.url().should('include', '/dashboard')

3. Avoid Hard Waits

1
2
3
4
5
6
7
8
9
// ❌ BAD: Hard wait
cy.wait(3000)
cy.get('#element').should('exist')

// ✅ GOOD: Cypress auto-waits
cy.get('#element')  // Auto-waits up to 4 seconds

// ✅ GOOD: Custom timeout if needed
cy.get('#slow-element', { timeout: 10000 })

4. Use Fixtures for Test Data

1
2
3
4
5
6
7
8
// ❌ BAD: Hardcoded data
const user = 'testuser'
const pass = 'testpass123'

// ✅ GOOD: Use fixtures
cy.fixture('users').then(users => {
  cy.login(users.standard.username, users.standard.password)
})

5. Organize Tests with Describe Blocks

1
2
3
4
5
6
// ✅ GOOD: Logical organization
describe('Shopping Cart', () => {
  describe('Adding Items', () => { ... })
  describe('Removing Items', () => { ... })
  describe('Checkout', () => { ... })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Page Objects
class LoginPage {
  visit() {
    cy.visit('/login')
  }
  
  fillUsername(username) {
    cy.get('[data-testid="username"]').type(username)
    return this
  }
  
  fillPassword(password) {
    cy.get('[data-testid="password"]').type(password)
    return this
  }
  
  submit() {
    cy.get('[data-testid="submit"]').click()
    return this
  }
  
  getErrorMessage() {
    return cy.get('[data-testid="error"]')
  }
}

// Usage in tests
const loginPage = new LoginPage()

it('should login', () => {
  loginPage
    .visit()
    .fillUsername('user')
    .fillPassword('pass')
    .submit()
  
  cy.url().should('include', '/dashboard')
})

7. Use Custom Commands

1
2
3
4
5
6
7
8
9
10
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/')
  cy.get('#email').type(email)
  cy.get('#password').type(password)
  cy.get('button[type="submit"]').click()
})

// Usage
cy.login('user@example.com', 'password123')

8. Mock External Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
it('should handle API errors', () => {
  // Stub API to return error
  cy.intercept('POST', '/api/login', {
    statusCode: 500,
    body: { error: 'Server error' }
  }).as('loginError')
  
  cy.visit('/')
  cy.get('#username').type('user')
  cy.get('#password').type('pass')
  cy.get('#login-button').click()
  
  cy.wait('@loginError')
  cy.get('[data-testid="error"]').should('contain', 'error')
})

Kesimpulan

Key Takeaways

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
✅ CYPRESS ADVANTAGES:
   ✓ Modern architecture (Direct DOM access)
   ✓ Beautiful test runner
   ✓ Time travel debugging
   ✓ Fast & reliable
   ✓ Network stubbing
   ✓ Excellent documentation
   ✓ Easy to learn & use
   ✓ JavaScript/TypeScript support

✅ WHEN TO USE CYPRESS:
   ✓ Modern web applications
   ✓ JavaScript/React/Vue/Angular apps
   ✓ E2E testing focus
   ✓ Team of developers
   ✓ Faster feedback loops

✅ CYPRESS SETUP:
   ✓ npm install cypress
   ✓ npx cypress open
   ✓ Write tests in cypress/e2e/
   ✓ Run tests interactively or headless

✅ BASIC COMMANDS:
   ✓ cy.visit()     - Navigate
   ✓ cy.get()       - Find element
   ✓ cy.type()      - Type text
   ✓ cy.click()     - Click
   ✓ cy.should()    - Assert
   ✓ cy.intercept() - Mock network

✅ BEST PRACTICES:
   ✓ Use data attributes
   ✓ Test user behavior, not implementation
   ✓ Avoid hard waits
   ✓ Use fixtures for data
   ✓ Organize with describe blocks
   ✓ Create custom commands
   ✓ Mock external dependencies

Next Steps

  1. Install Cypress: npm install --save-dev cypress
  2. Run Examples: npm test dan explore
  3. Write Tests: Practice with own website
  4. Learn Advanced: Fixtures, intercepts, custom commands
  5. CI/CD Integration: Add to GitHub Actions, Jenkins
  6. Component Testing: Cypress Component Testing

Resources


Happy Cypress Testing! 🚀

This post is licensed under CC BY 4.0 by the author.