cypress-on-rails 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +50 -0
- data/CHANGELOG.md +319 -98
- data/README.md +93 -1
- data/RELEASING.md +200 -0
- data/Rakefile +1 -4
- data/cypress-on-rails.gemspec +1 -0
- data/docs/BEST_PRACTICES.md +678 -0
- data/docs/DX_IMPROVEMENTS.md +163 -0
- data/docs/PLAYWRIGHT_GUIDE.md +554 -0
- data/docs/RELEASE.md +124 -0
- data/docs/TROUBLESHOOTING.md +351 -0
- data/docs/VCR_GUIDE.md +499 -0
- data/lib/cypress_on_rails/configuration.rb +24 -0
- data/lib/cypress_on_rails/railtie.rb +7 -0
- data/lib/cypress_on_rails/server.rb +197 -0
- data/lib/cypress_on_rails/state_reset_middleware.rb +58 -0
- data/lib/cypress_on_rails/version.rb +1 -1
- data/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb +12 -0
- data/lib/generators/cypress_on_rails/templates/spec/cypress/e2e/rails_examples/using_factory_bot.cy.js +2 -2
- data/lib/generators/cypress_on_rails/templates/spec/cypress/e2e/rails_examples/using_scenarios.cy.js +1 -1
- data/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js +1 -1
- data/lib/tasks/cypress.rake +33 -0
- data/rakelib/release.rake +80 -0
- data/rakelib/task_helpers.rb +23 -0
- data/rakelib/update_changelog.rake +63 -0
- metadata +31 -2
@@ -0,0 +1,678 @@
|
|
1
|
+
# Best Practices Guide
|
2
|
+
|
3
|
+
This guide provides recommended patterns and practices for using cypress-playwright-on-rails effectively.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
- [Project Structure](#project-structure)
|
7
|
+
- [Test Organization](#test-organization)
|
8
|
+
- [Data Management](#data-management)
|
9
|
+
- [Performance Optimization](#performance-optimization)
|
10
|
+
- [CI/CD Integration](#cicd-integration)
|
11
|
+
- [Security Considerations](#security-considerations)
|
12
|
+
- [Debugging Strategies](#debugging-strategies)
|
13
|
+
- [Common Patterns](#common-patterns)
|
14
|
+
|
15
|
+
## Project Structure
|
16
|
+
|
17
|
+
### Recommended Directory Layout
|
18
|
+
```
|
19
|
+
├── e2e/ # All E2E test related files
|
20
|
+
│ ├── cypress/ # Cypress tests
|
21
|
+
│ │ ├── e2e/ # Test specs
|
22
|
+
│ │ │ ├── auth/ # Grouped by feature
|
23
|
+
│ │ │ ├── users/
|
24
|
+
│ │ │ └── products/
|
25
|
+
│ │ ├── fixtures/ # Test data
|
26
|
+
│ │ ├── support/ # Helpers and commands
|
27
|
+
│ │ └── downloads/ # Downloaded files during tests
|
28
|
+
│ ├── playwright/ # Playwright tests
|
29
|
+
│ │ ├── e2e/ # Test specs
|
30
|
+
│ │ └── support/ # Helpers
|
31
|
+
│ ├── app_commands/ # Shared Ruby commands
|
32
|
+
│ │ ├── scenarios/ # Complex test setups
|
33
|
+
│ │ └── helpers/ # Ruby helper modules
|
34
|
+
│ └── shared/ # Shared utilities
|
35
|
+
└── config/
|
36
|
+
└── initializers/
|
37
|
+
└── cypress_on_rails.rb # Configuration
|
38
|
+
```
|
39
|
+
|
40
|
+
### Separating Concerns
|
41
|
+
```ruby
|
42
|
+
# e2e/app_commands/helpers/test_data.rb
|
43
|
+
module TestData
|
44
|
+
def self.standard_user
|
45
|
+
{
|
46
|
+
name: 'John Doe',
|
47
|
+
email: 'john@example.com',
|
48
|
+
role: 'user'
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.admin_user
|
53
|
+
{
|
54
|
+
name: 'Admin User',
|
55
|
+
email: 'admin@example.com',
|
56
|
+
role: 'admin'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# e2e/app_commands/scenarios/standard_setup.rb
|
62
|
+
require_relative '../helpers/test_data'
|
63
|
+
|
64
|
+
User.create!(TestData.standard_user)
|
65
|
+
User.create!(TestData.admin_user)
|
66
|
+
```
|
67
|
+
|
68
|
+
## Test Organization
|
69
|
+
|
70
|
+
### Group Related Tests
|
71
|
+
```js
|
72
|
+
// e2e/cypress/e2e/users/registration.cy.js
|
73
|
+
describe('User Registration', () => {
|
74
|
+
context('Valid Input', () => {
|
75
|
+
it('registers with email', () => {
|
76
|
+
// Test implementation
|
77
|
+
});
|
78
|
+
|
79
|
+
it('registers with social login', () => {
|
80
|
+
// Test implementation
|
81
|
+
});
|
82
|
+
});
|
83
|
+
|
84
|
+
context('Invalid Input', () => {
|
85
|
+
it('shows error for duplicate email', () => {
|
86
|
+
// Test implementation
|
87
|
+
});
|
88
|
+
});
|
89
|
+
});
|
90
|
+
```
|
91
|
+
|
92
|
+
### Use Page Objects
|
93
|
+
```js
|
94
|
+
// e2e/cypress/support/pages/LoginPage.js
|
95
|
+
class LoginPage {
|
96
|
+
visit() {
|
97
|
+
cy.visit('/login');
|
98
|
+
}
|
99
|
+
|
100
|
+
fillEmail(email) {
|
101
|
+
cy.get('[data-cy=email]').type(email);
|
102
|
+
}
|
103
|
+
|
104
|
+
fillPassword(password) {
|
105
|
+
cy.get('[data-cy=password]').type(password);
|
106
|
+
}
|
107
|
+
|
108
|
+
submit() {
|
109
|
+
cy.get('[data-cy=submit]').click();
|
110
|
+
}
|
111
|
+
|
112
|
+
login(email, password) {
|
113
|
+
this.visit();
|
114
|
+
this.fillEmail(email);
|
115
|
+
this.fillPassword(password);
|
116
|
+
this.submit();
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
export default new LoginPage();
|
121
|
+
|
122
|
+
// Usage in test
|
123
|
+
import LoginPage from '../../support/pages/LoginPage';
|
124
|
+
|
125
|
+
it('user can login', () => {
|
126
|
+
LoginPage.login('user@example.com', 'password');
|
127
|
+
cy.url().should('include', '/dashboard');
|
128
|
+
});
|
129
|
+
```
|
130
|
+
|
131
|
+
### Data-Test Attributes
|
132
|
+
```erb
|
133
|
+
<!-- app/views/users/new.html.erb -->
|
134
|
+
<form data-cy="registration-form">
|
135
|
+
<input
|
136
|
+
type="email"
|
137
|
+
name="user[email]"
|
138
|
+
data-cy="email-input"
|
139
|
+
data-test-id="user-email"
|
140
|
+
/>
|
141
|
+
<button
|
142
|
+
type="submit"
|
143
|
+
data-cy="submit-button"
|
144
|
+
data-test-id="register-submit"
|
145
|
+
>
|
146
|
+
Register
|
147
|
+
</button>
|
148
|
+
</form>
|
149
|
+
```
|
150
|
+
|
151
|
+
```js
|
152
|
+
// Prefer data-cy or data-test-id over classes/IDs
|
153
|
+
cy.get('[data-cy=email-input]').type('test@example.com');
|
154
|
+
// NOT: cy.get('#email').type('test@example.com');
|
155
|
+
```
|
156
|
+
|
157
|
+
## Data Management
|
158
|
+
|
159
|
+
### Factory Bot Best Practices
|
160
|
+
```ruby
|
161
|
+
# spec/factories/users.rb
|
162
|
+
FactoryBot.define do
|
163
|
+
factory :user do
|
164
|
+
sequence(:email) { |n| "user#{n}@example.com" }
|
165
|
+
name { Faker::Name.name }
|
166
|
+
confirmed_at { Time.current }
|
167
|
+
|
168
|
+
trait :admin do
|
169
|
+
role { 'admin' }
|
170
|
+
end
|
171
|
+
|
172
|
+
trait :with_posts do
|
173
|
+
transient do
|
174
|
+
posts_count { 3 }
|
175
|
+
end
|
176
|
+
|
177
|
+
after(:create) do |user, evaluator|
|
178
|
+
create_list(:post, evaluator.posts_count, user: user)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
### Scenario Patterns
|
186
|
+
```ruby
|
187
|
+
# e2e/app_commands/scenarios/e_commerce_setup.rb
|
188
|
+
class ECommerceSetup
|
189
|
+
def self.run(options = {})
|
190
|
+
ActiveRecord::Base.transaction do
|
191
|
+
# Create categories
|
192
|
+
categories = create_categories
|
193
|
+
|
194
|
+
# Create products
|
195
|
+
products = create_products(categories)
|
196
|
+
|
197
|
+
# Create users
|
198
|
+
users = create_users
|
199
|
+
|
200
|
+
# Create orders
|
201
|
+
create_orders(users, products) if options[:with_orders]
|
202
|
+
|
203
|
+
{ categories: categories, products: products, users: users }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def self.create_categories
|
210
|
+
['Electronics', 'Clothing', 'Books'].map do |name|
|
211
|
+
Category.create!(name: name)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.create_products(categories)
|
216
|
+
# Implementation
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Usage in test
|
221
|
+
ECommerceSetup.run(with_orders: true)
|
222
|
+
```
|
223
|
+
|
224
|
+
### Database Cleaning Strategies
|
225
|
+
```ruby
|
226
|
+
# e2e/app_commands/clean.rb
|
227
|
+
class SmartCleaner
|
228
|
+
PRESERVE_TABLES = %w[
|
229
|
+
schema_migrations
|
230
|
+
ar_internal_metadata
|
231
|
+
spatial_ref_sys # PostGIS
|
232
|
+
].freeze
|
233
|
+
|
234
|
+
def self.clean(strategy: :transaction)
|
235
|
+
case strategy
|
236
|
+
when :transaction
|
237
|
+
DatabaseCleaner.strategy = :transaction
|
238
|
+
when :truncation
|
239
|
+
DatabaseCleaner.strategy = :truncation, {
|
240
|
+
except: PRESERVE_TABLES
|
241
|
+
}
|
242
|
+
when :deletion
|
243
|
+
DatabaseCleaner.strategy = :deletion, {
|
244
|
+
except: PRESERVE_TABLES
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
DatabaseCleaner.clean
|
249
|
+
|
250
|
+
# Reset sequences
|
251
|
+
reset_sequences if postgresql?
|
252
|
+
|
253
|
+
# Clear caches
|
254
|
+
clear_caches
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def self.postgresql?
|
260
|
+
ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.reset_sequences
|
264
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
265
|
+
ActiveRecord::Base.connection.reset_pk_sequence!(table)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.clear_caches
|
270
|
+
Rails.cache.clear
|
271
|
+
I18n.reload! if defined?(I18n)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
```
|
275
|
+
|
276
|
+
## Performance Optimization
|
277
|
+
|
278
|
+
### Minimize Database Operations
|
279
|
+
```js
|
280
|
+
// Bad: Multiple database operations
|
281
|
+
it('creates multiple users', () => {
|
282
|
+
cy.appFactories([['create', 'user', { name: 'User 1' }]]);
|
283
|
+
cy.appFactories([['create', 'user', { name: 'User 2' }]]);
|
284
|
+
cy.appFactories([['create', 'user', { name: 'User 3' }]]);
|
285
|
+
});
|
286
|
+
|
287
|
+
// Good: Batch operations
|
288
|
+
it('creates multiple users', () => {
|
289
|
+
cy.appFactories([
|
290
|
+
['create', 'user', { name: 'User 1' }],
|
291
|
+
['create', 'user', { name: 'User 2' }],
|
292
|
+
['create', 'user', { name: 'User 3' }]
|
293
|
+
]);
|
294
|
+
});
|
295
|
+
|
296
|
+
// Better: Use create_list
|
297
|
+
it('creates multiple users', () => {
|
298
|
+
cy.appFactories([
|
299
|
+
['create_list', 'user', 3]
|
300
|
+
]);
|
301
|
+
});
|
302
|
+
```
|
303
|
+
|
304
|
+
### Smart Waiting Strategies
|
305
|
+
```js
|
306
|
+
// Bad: Fixed waits
|
307
|
+
cy.wait(5000);
|
308
|
+
|
309
|
+
// Good: Wait for specific conditions
|
310
|
+
cy.get('[data-cy=loading]').should('not.exist');
|
311
|
+
cy.get('[data-cy=user-list]').should('be.visible');
|
312
|
+
|
313
|
+
// Better: Wait for API calls
|
314
|
+
cy.intercept('GET', '/api/users').as('getUsers');
|
315
|
+
cy.visit('/users');
|
316
|
+
cy.wait('@getUsers');
|
317
|
+
```
|
318
|
+
|
319
|
+
### Parallel Testing Configuration
|
320
|
+
```ruby
|
321
|
+
# config/database.yml
|
322
|
+
test:
|
323
|
+
<<: *default
|
324
|
+
database: myapp_test<%= ENV['TEST_ENV_NUMBER'] %>
|
325
|
+
|
326
|
+
# config/initializers/cypress_on_rails.rb
|
327
|
+
if ENV['PARALLEL_WORKERS'].present?
|
328
|
+
CypressOnRails.configure do |c|
|
329
|
+
worker_id = ENV['TEST_ENV_NUMBER'] || '1'
|
330
|
+
c.server_port = 5000 + worker_id.to_i
|
331
|
+
end
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
## CI/CD Integration
|
336
|
+
|
337
|
+
### GitHub Actions Example
|
338
|
+
```yaml
|
339
|
+
# .github/workflows/e2e.yml
|
340
|
+
name: E2E Tests
|
341
|
+
on: [push, pull_request]
|
342
|
+
|
343
|
+
jobs:
|
344
|
+
cypress:
|
345
|
+
runs-on: ubuntu-latest
|
346
|
+
strategy:
|
347
|
+
matrix:
|
348
|
+
browser: [chrome, firefox, edge]
|
349
|
+
|
350
|
+
services:
|
351
|
+
postgres:
|
352
|
+
image: postgres:14
|
353
|
+
env:
|
354
|
+
POSTGRES_PASSWORD: password
|
355
|
+
options: >-
|
356
|
+
--health-cmd pg_isready
|
357
|
+
--health-interval 10s
|
358
|
+
--health-timeout 5s
|
359
|
+
--health-retries 5
|
360
|
+
ports:
|
361
|
+
- 5432:5432
|
362
|
+
|
363
|
+
steps:
|
364
|
+
- uses: actions/checkout@v3
|
365
|
+
|
366
|
+
- name: Setup Ruby
|
367
|
+
uses: ruby/setup-ruby@v1
|
368
|
+
with:
|
369
|
+
bundler-cache: true
|
370
|
+
|
371
|
+
- name: Setup Node
|
372
|
+
uses: actions/setup-node@v3
|
373
|
+
with:
|
374
|
+
node-version: '18'
|
375
|
+
cache: 'yarn'
|
376
|
+
|
377
|
+
- name: Install dependencies
|
378
|
+
run: |
|
379
|
+
yarn install --frozen-lockfile
|
380
|
+
bundle install
|
381
|
+
|
382
|
+
- name: Setup database
|
383
|
+
env:
|
384
|
+
RAILS_ENV: test
|
385
|
+
DATABASE_URL: postgresql://postgres:password@localhost:5432/test
|
386
|
+
run: |
|
387
|
+
bundle exec rails db:create
|
388
|
+
bundle exec rails db:schema:load
|
389
|
+
|
390
|
+
- name: Run E2E tests
|
391
|
+
env:
|
392
|
+
RAILS_ENV: test
|
393
|
+
DATABASE_URL: postgresql://postgres:password@localhost:5432/test
|
394
|
+
run: |
|
395
|
+
bundle exec rails cypress:run
|
396
|
+
|
397
|
+
- name: Upload artifacts
|
398
|
+
uses: actions/upload-artifact@v3
|
399
|
+
if: failure()
|
400
|
+
with:
|
401
|
+
name: cypress-artifacts-${{ matrix.browser }}
|
402
|
+
path: |
|
403
|
+
e2e/cypress/screenshots
|
404
|
+
e2e/cypress/videos
|
405
|
+
```
|
406
|
+
|
407
|
+
### CircleCI Configuration
|
408
|
+
```yaml
|
409
|
+
# .circleci/config.yml
|
410
|
+
version: 2.1
|
411
|
+
|
412
|
+
orbs:
|
413
|
+
cypress: cypress-io/cypress@2
|
414
|
+
|
415
|
+
jobs:
|
416
|
+
e2e-tests:
|
417
|
+
docker:
|
418
|
+
- image: cimg/ruby:3.2-browsers
|
419
|
+
- image: cimg/postgres:14.0
|
420
|
+
environment:
|
421
|
+
POSTGRES_PASSWORD: password
|
422
|
+
|
423
|
+
parallelism: 4
|
424
|
+
|
425
|
+
steps:
|
426
|
+
- checkout
|
427
|
+
|
428
|
+
- restore_cache:
|
429
|
+
keys:
|
430
|
+
- gem-cache-{{ checksum "Gemfile.lock" }}
|
431
|
+
- yarn-cache-{{ checksum "yarn.lock" }}
|
432
|
+
|
433
|
+
- run:
|
434
|
+
name: Install dependencies
|
435
|
+
command: |
|
436
|
+
bundle install --path vendor/bundle
|
437
|
+
yarn install --frozen-lockfile
|
438
|
+
|
439
|
+
- save_cache:
|
440
|
+
key: gem-cache-{{ checksum "Gemfile.lock" }}
|
441
|
+
paths:
|
442
|
+
- vendor/bundle
|
443
|
+
|
444
|
+
- save_cache:
|
445
|
+
key: yarn-cache-{{ checksum "yarn.lock" }}
|
446
|
+
paths:
|
447
|
+
- node_modules
|
448
|
+
|
449
|
+
- run:
|
450
|
+
name: Setup database
|
451
|
+
command: |
|
452
|
+
bundle exec rails db:create db:schema:load
|
453
|
+
environment:
|
454
|
+
RAILS_ENV: test
|
455
|
+
|
456
|
+
- run:
|
457
|
+
name: Run E2E tests
|
458
|
+
command: |
|
459
|
+
TESTFILES=$(circleci tests glob "e2e/cypress/e2e/**/*.cy.js" | circleci tests split)
|
460
|
+
bundle exec rails cypress:run -- --spec $TESTFILES
|
461
|
+
|
462
|
+
- store_test_results:
|
463
|
+
path: test-results
|
464
|
+
|
465
|
+
- store_artifacts:
|
466
|
+
path: e2e/cypress/screenshots
|
467
|
+
|
468
|
+
- store_artifacts:
|
469
|
+
path: e2e/cypress/videos
|
470
|
+
|
471
|
+
workflows:
|
472
|
+
test:
|
473
|
+
jobs:
|
474
|
+
- e2e-tests
|
475
|
+
```
|
476
|
+
|
477
|
+
## Security Considerations
|
478
|
+
|
479
|
+
### Protecting Test Endpoints
|
480
|
+
```ruby
|
481
|
+
# config/initializers/cypress_on_rails.rb
|
482
|
+
CypressOnRails.configure do |c|
|
483
|
+
# Only enable in test/development
|
484
|
+
c.use_middleware = !Rails.env.production?
|
485
|
+
|
486
|
+
# Add authentication
|
487
|
+
c.before_request = lambda { |request|
|
488
|
+
# IP whitelist for CI
|
489
|
+
allowed_ips = ['127.0.0.1', '::1']
|
490
|
+
allowed_ips += ENV['ALLOWED_CI_IPS'].split(',') if ENV['ALLOWED_CI_IPS']
|
491
|
+
|
492
|
+
unless allowed_ips.include?(request.ip)
|
493
|
+
return [403, {}, ['Forbidden']]
|
494
|
+
end
|
495
|
+
|
496
|
+
# Token authentication
|
497
|
+
body = JSON.parse(request.body.string)
|
498
|
+
expected_token = ENV.fetch('CYPRESS_SECRET_TOKEN', 'development-token')
|
499
|
+
|
500
|
+
if body['auth_token'] != expected_token
|
501
|
+
return [401, {}, ['Unauthorized']]
|
502
|
+
end
|
503
|
+
|
504
|
+
nil
|
505
|
+
}
|
506
|
+
end
|
507
|
+
```
|
508
|
+
|
509
|
+
### Environment Variables
|
510
|
+
```bash
|
511
|
+
# .env.test
|
512
|
+
CYPRESS_SECRET_TOKEN=secure-random-token-here
|
513
|
+
CYPRESS_BASE_URL=http://localhost:5017
|
514
|
+
CYPRESS_RAILS_HOST=localhost
|
515
|
+
CYPRESS_RAILS_PORT=5017
|
516
|
+
|
517
|
+
# Never commit real credentials
|
518
|
+
DATABASE_URL=postgresql://localhost/myapp_test
|
519
|
+
REDIS_URL=redis://localhost:6379/1
|
520
|
+
```
|
521
|
+
|
522
|
+
### Sanitizing Test Data
|
523
|
+
```ruby
|
524
|
+
# e2e/app_commands/factory_bot.rb
|
525
|
+
class SafeFactoryBot
|
526
|
+
BLOCKED_FACTORIES = %w[admin super_admin payment_method].freeze
|
527
|
+
|
528
|
+
def self.create(factory_name, *args)
|
529
|
+
raise "Factory '#{factory_name}' is blocked in tests" if BLOCKED_FACTORIES.include?(factory_name.to_s)
|
530
|
+
|
531
|
+
FactoryBot.create(factory_name, *args)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
```
|
535
|
+
|
536
|
+
## Debugging Strategies
|
537
|
+
|
538
|
+
### Verbose Logging
|
539
|
+
```ruby
|
540
|
+
# config/initializers/cypress_on_rails.rb
|
541
|
+
CypressOnRails.configure do |c|
|
542
|
+
c.logger = Logger.new(STDOUT)
|
543
|
+
c.logger.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::INFO
|
544
|
+
end
|
545
|
+
|
546
|
+
# e2e/app_commands/debug.rb
|
547
|
+
def perform
|
548
|
+
logger.debug "Current user count: #{User.count}"
|
549
|
+
logger.debug "Environment: #{Rails.env}"
|
550
|
+
logger.debug "Database: #{ActiveRecord::Base.connection.current_database}"
|
551
|
+
|
552
|
+
result = yield if block_given?
|
553
|
+
|
554
|
+
logger.debug "Result: #{result.inspect}"
|
555
|
+
result
|
556
|
+
end
|
557
|
+
```
|
558
|
+
|
559
|
+
### Screenshot on Failure
|
560
|
+
```js
|
561
|
+
// cypress/support/index.js
|
562
|
+
Cypress.on('fail', (error, runnable) => {
|
563
|
+
// Take screenshot before failing
|
564
|
+
cy.screenshot(`failed-${runnable.title}`, { capture: 'fullPage' });
|
565
|
+
|
566
|
+
// Log additional debugging info
|
567
|
+
cy.task('log', {
|
568
|
+
test: runnable.title,
|
569
|
+
error: error.message,
|
570
|
+
url: cy.url(),
|
571
|
+
timestamp: new Date().toISOString()
|
572
|
+
});
|
573
|
+
|
574
|
+
throw error;
|
575
|
+
});
|
576
|
+
```
|
577
|
+
|
578
|
+
### Interactive Debugging
|
579
|
+
```js
|
580
|
+
// Add debugger statements
|
581
|
+
it('debug this test', () => {
|
582
|
+
cy.appFactories([['create', 'user']]);
|
583
|
+
|
584
|
+
cy.visit('/users');
|
585
|
+
debugger; // Cypress will pause here in open mode
|
586
|
+
|
587
|
+
cy.get('[data-cy=user-list]').should('exist');
|
588
|
+
});
|
589
|
+
|
590
|
+
// Or use cy.pause()
|
591
|
+
it('pause execution', () => {
|
592
|
+
cy.visit('/users');
|
593
|
+
cy.pause(); // Manually resume in Cypress UI
|
594
|
+
cy.get('[data-cy=user-list]').should('exist');
|
595
|
+
});
|
596
|
+
```
|
597
|
+
|
598
|
+
## Common Patterns
|
599
|
+
|
600
|
+
### Authentication Flow
|
601
|
+
```js
|
602
|
+
// cypress/support/commands.js
|
603
|
+
Cypress.Commands.add('login', (email, password) => {
|
604
|
+
cy.session([email, password], () => {
|
605
|
+
cy.visit('/login');
|
606
|
+
cy.get('[data-cy=email]').type(email);
|
607
|
+
cy.get('[data-cy=password]').type(password);
|
608
|
+
cy.get('[data-cy=submit]').click();
|
609
|
+
cy.url().should('include', '/dashboard');
|
610
|
+
});
|
611
|
+
});
|
612
|
+
|
613
|
+
// Usage
|
614
|
+
beforeEach(() => {
|
615
|
+
cy.login('user@example.com', 'password');
|
616
|
+
});
|
617
|
+
```
|
618
|
+
|
619
|
+
### File Upload Testing
|
620
|
+
```js
|
621
|
+
it('uploads a file', () => {
|
622
|
+
cy.fixture('document.pdf', 'base64').then(fileContent => {
|
623
|
+
cy.get('[data-cy=file-input]').attachFile({
|
624
|
+
fileContent,
|
625
|
+
fileName: 'document.pdf',
|
626
|
+
mimeType: 'application/pdf',
|
627
|
+
encoding: 'base64'
|
628
|
+
});
|
629
|
+
});
|
630
|
+
|
631
|
+
cy.get('[data-cy=upload-button]').click();
|
632
|
+
cy.contains('File uploaded successfully');
|
633
|
+
});
|
634
|
+
```
|
635
|
+
|
636
|
+
### API Mocking
|
637
|
+
```js
|
638
|
+
it('handles API errors gracefully', () => {
|
639
|
+
// Mock failed API response
|
640
|
+
cy.intercept('POST', '/api/users', {
|
641
|
+
statusCode: 500,
|
642
|
+
body: { error: 'Internal Server Error' }
|
643
|
+
}).as('createUser');
|
644
|
+
|
645
|
+
cy.visit('/users/new');
|
646
|
+
cy.get('[data-cy=submit]').click();
|
647
|
+
|
648
|
+
cy.wait('@createUser');
|
649
|
+
cy.contains('Something went wrong');
|
650
|
+
});
|
651
|
+
```
|
652
|
+
|
653
|
+
### Testing Async Operations
|
654
|
+
```js
|
655
|
+
it('waits for async operations', () => {
|
656
|
+
// Start a background job
|
657
|
+
cy.appEval(`
|
658
|
+
ImportJob.perform_later('large_dataset.csv')
|
659
|
+
`);
|
660
|
+
|
661
|
+
cy.visit('/imports');
|
662
|
+
|
663
|
+
// Poll for completion
|
664
|
+
cy.get('[data-cy=import-status]', { timeout: 30000 })
|
665
|
+
.should('contain', 'Completed');
|
666
|
+
});
|
667
|
+
```
|
668
|
+
|
669
|
+
## Summary
|
670
|
+
|
671
|
+
Following these best practices will help you:
|
672
|
+
- Write more maintainable and reliable tests
|
673
|
+
- Improve test performance and reduce flakiness
|
674
|
+
- Better organize your test code
|
675
|
+
- Secure your test infrastructure
|
676
|
+
- Debug issues more effectively
|
677
|
+
|
678
|
+
Remember: E2E tests should focus on critical user journeys. Use unit and integration tests for comprehensive coverage of edge cases.
|