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
| Tool | Type | Language | Setup | Speed |
|---|---|---|---|---|
| Jest | Unit | JS/TS | Easy | Very Fast |
| Cypress ⭐ | E2E | JS/TS | Easy | Fast |
| Selenium | E2E | Multi | Medium | Medium |
| Playwright | E2E | Multi | Medium | Very Fast |
| TestCafe | E2E | JS | Medium | Fast |
| WebdriverIO | E2E | JS | Medium | Medium |
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
| Aspek | Selenium | Cypress |
|---|---|---|
| Architecture | JSON Wire | Direct DOM |
| Process Isolation | Separate | Same ⭐ |
| DOM Access | Limited | Full ⭐ |
| Network Control | No | Yes ⭐ |
| Speed | Medium | Fast ⭐ |
| Debugging | Hard | Easy ⭐ |
| Network Overhead | High | None ⭐ |
| Error Messages | Generic | Detailed ⭐ |
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
| Feature | Selenium | Cypress ⭐ |
|---|---|---|
| Architecture | WebDriver | Direct DOM |
| Setup Complexity | Medium | Easy ⭐ |
| Learning Curve | Medium | Easy ⭐ |
| Execution Speed | Medium | Fast ⭐ |
| Debug Experience | Hard | Easy ⭐ |
| Test Runner UI | None | Beautiful ⭐ |
| Time Travel Debugging | No | Yes ⭐ |
| Network Stubbing | Complex | Easy ⭐ |
| Error Messages | Generic | Detailed ⭐ |
| Documentation | Good | Excellent ⭐ |
| Mobile Support | Yes | Limited |
| Multi-browser | Yes ⭐ | Yes |
| Language Support | Multiple | JS/TS |
| Community | Very Large | Growing ⭐ |
| Job Market | High ⭐ | 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
Navigation Commands
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', () => { ... })
})
6. Use Page Object Model (Optional but Recommended)
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
- Install Cypress:
npm install --save-dev cypress - Run Examples:
npm testdan explore - Write Tests: Practice with own website
- Learn Advanced: Fixtures, intercepts, custom commands
- CI/CD Integration: Add to GitHub Actions, Jenkins
- Component Testing: Cypress Component Testing
Resources
Happy Cypress Testing! 🚀