diff --git a/CHANGELOG.md b/CHANGELOG.md index cde0500..d8afc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ If migrating from the `cypress-rails` gem: ```ruby # Remove gem 'cypress-rails' - + # Add gem 'cypress-on-rails', '~> 1.0' ``` diff --git a/README.md b/README.md index db5a4cc..064c0e2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,28 @@ Consider first learning the basics of Playwright before attempting to integrate * [Good start Here](https://playwright.dev/docs/writing-tests) +## Quick Start + +```bash +# 1. Add to Gemfile +gem 'cypress-on-rails', '~> 1.0' + +# 2. Install and generate +bundle install +bin/rails g cypress_on_rails:install + +# 3. Run tests (new rake tasks!) +bin/rails cypress:open # Open Cypress UI +bin/rails cypress:run # Run headless +``` + +For Playwright: +```bash +bin/rails g cypress_on_rails:install --framework playwright +bin/rails playwright:open # Open Playwright UI +bin/rails playwright:run # Run headless +``` + ## Overview Gem for using [cypress.io](http://github.com/cypress-io/) or [playwright.dev](https://playwright.dev/) in Rails and Ruby Rack applications to control state as mentioned in [Cypress Best Practices](https://docs.cypress.io/guides/references/best-practices.html#Organizing-Tests-Logging-In-Controlling-State). @@ -64,7 +86,16 @@ Has examples of setting up state with: * scenarios * custom commands -## Resources +## Documentation + +### 📚 Essential Guides +* **[Best Practices Guide](docs/BEST_PRACTICES.md)** - Recommended patterns and practices +* **[Troubleshooting Guide](docs/TROUBLESHOOTING.md)** - Solutions to common issues +* **[Playwright Guide](docs/PLAYWRIGHT_GUIDE.md)** - Complete Playwright documentation +* **[VCR Integration Guide](docs/VCR_GUIDE.md)** - HTTP recording and mocking +* **[DX Improvements](docs/DX_IMPROVEMENTS.md)** - Recent improvements based on user feedback + +### 🎥 Resources * [Video of getting started with this gem](https://grant-ps.blog/2018/08/10/getting-started-with-cypress-io-and-ruby-on-rails/) * [Article: Introduction to Cypress on Rails](https://www.shakacode.com/blog/introduction-to-cypress-on-rails/) diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md new file mode 100644 index 0000000..cdd626e --- /dev/null +++ b/docs/BEST_PRACTICES.md @@ -0,0 +1,678 @@ +# Best Practices Guide + +This guide provides recommended patterns and practices for using cypress-playwright-on-rails effectively. + +## Table of Contents +- [Project Structure](#project-structure) +- [Test Organization](#test-organization) +- [Data Management](#data-management) +- [Performance Optimization](#performance-optimization) +- [CI/CD Integration](#cicd-integration) +- [Security Considerations](#security-considerations) +- [Debugging Strategies](#debugging-strategies) +- [Common Patterns](#common-patterns) + +## Project Structure + +### Recommended Directory Layout +``` +├── e2e/ # All E2E test related files +│ ├── cypress/ # Cypress tests +│ │ ├── e2e/ # Test specs +│ │ │ ├── auth/ # Grouped by feature +│ │ │ ├── users/ +│ │ │ └── products/ +│ │ ├── fixtures/ # Test data +│ │ ├── support/ # Helpers and commands +│ │ └── downloads/ # Downloaded files during tests +│ ├── playwright/ # Playwright tests +│ │ ├── e2e/ # Test specs +│ │ └── support/ # Helpers +│ ├── app_commands/ # Shared Ruby commands +│ │ ├── scenarios/ # Complex test setups +│ │ └── helpers/ # Ruby helper modules +│ └── shared/ # Shared utilities +└── config/ + └── initializers/ + └── cypress_on_rails.rb # Configuration +``` + +### Separating Concerns +```ruby +# e2e/app_commands/helpers/test_data.rb +module TestData + def self.standard_user + { + name: 'John Doe', + email: 'john@example.com', + role: 'user' + } + end + + def self.admin_user + { + name: 'Admin User', + email: 'admin@example.com', + role: 'admin' + } + end +end + +# e2e/app_commands/scenarios/standard_setup.rb +require_relative '../helpers/test_data' + +User.create!(TestData.standard_user) +User.create!(TestData.admin_user) +``` + +## Test Organization + +### Group Related Tests +```js +// e2e/cypress/e2e/users/registration.cy.js +describe('User Registration', () => { + context('Valid Input', () => { + it('registers with email', () => { + // Test implementation + }); + + it('registers with social login', () => { + // Test implementation + }); + }); + + context('Invalid Input', () => { + it('shows error for duplicate email', () => { + // Test implementation + }); + }); +}); +``` + +### Use Page Objects +```js +// e2e/cypress/support/pages/LoginPage.js +class LoginPage { + visit() { + cy.visit('/login'); + } + + fillEmail(email) { + cy.get('[data-cy=email]').type(email); + } + + fillPassword(password) { + cy.get('[data-cy=password]').type(password); + } + + submit() { + cy.get('[data-cy=submit]').click(); + } + + login(email, password) { + this.visit(); + this.fillEmail(email); + this.fillPassword(password); + this.submit(); + } +} + +export default new LoginPage(); + +// Usage in test +import LoginPage from '../../support/pages/LoginPage'; + +it('user can login', () => { + LoginPage.login('user@example.com', 'password'); + cy.url().should('include', '/dashboard'); +}); +``` + +### Data-Test Attributes +```erb + +
+``` + +```js +// Prefer data-cy or data-test-id over classes/IDs +cy.get('[data-cy=email-input]').type('test@example.com'); +// NOT: cy.get('#email').type('test@example.com'); +``` + +## Data Management + +### Factory Bot Best Practices +```ruby +# spec/factories/users.rb +FactoryBot.define do + factory :user do + sequence(:email) { |n| "user#{n}@example.com" } + name { Faker::Name.name } + confirmed_at { Time.current } + + trait :admin do + role { 'admin' } + end + + trait :with_posts do + transient do + posts_count { 3 } + end + + after(:create) do |user, evaluator| + create_list(:post, evaluator.posts_count, user: user) + end + end + end +end +``` + +### Scenario Patterns +```ruby +# e2e/app_commands/scenarios/e_commerce_setup.rb +class ECommerceSetup + def self.run(options = {}) + ActiveRecord::Base.transaction do + # Create categories + categories = create_categories + + # Create products + products = create_products(categories) + + # Create users + users = create_users + + # Create orders + create_orders(users, products) if options[:with_orders] + + { categories: categories, products: products, users: users } + end + end + + private + + def self.create_categories + ['Electronics', 'Clothing', 'Books'].map do |name| + Category.create!(name: name) + end + end + + def self.create_products(categories) + # Implementation + end +end + +# Usage in test +ECommerceSetup.run(with_orders: true) +``` + +### Database Cleaning Strategies +```ruby +# e2e/app_commands/clean.rb +class SmartCleaner + PRESERVE_TABLES = %w[ + schema_migrations + ar_internal_metadata + spatial_ref_sys # PostGIS + ].freeze + + def self.clean(strategy: :transaction) + case strategy + when :transaction + DatabaseCleaner.strategy = :transaction + when :truncation + DatabaseCleaner.strategy = :truncation, { + except: PRESERVE_TABLES + } + when :deletion + DatabaseCleaner.strategy = :deletion, { + except: PRESERVE_TABLES + } + end + + DatabaseCleaner.clean + + # Reset sequences + reset_sequences if postgresql? + + # Clear caches + clear_caches + end + + private + + def self.postgresql? + ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + end + + def self.reset_sequences + ActiveRecord::Base.connection.tables.each do |table| + ActiveRecord::Base.connection.reset_pk_sequence!(table) + end + end + + def self.clear_caches + Rails.cache.clear + I18n.reload! if defined?(I18n) + end +end +``` + +## Performance Optimization + +### Minimize Database Operations +```js +// Bad: Multiple database operations +it('creates multiple users', () => { + cy.appFactories([['create', 'user', { name: 'User 1' }]]); + cy.appFactories([['create', 'user', { name: 'User 2' }]]); + cy.appFactories([['create', 'user', { name: 'User 3' }]]); +}); + +// Good: Batch operations +it('creates multiple users', () => { + cy.appFactories([ + ['create', 'user', { name: 'User 1' }], + ['create', 'user', { name: 'User 2' }], + ['create', 'user', { name: 'User 3' }] + ]); +}); + +// Better: Use create_list +it('creates multiple users', () => { + cy.appFactories([ + ['create_list', 'user', 3] + ]); +}); +``` + +### Smart Waiting Strategies +```js +// Bad: Fixed waits +cy.wait(5000); + +// Good: Wait for specific conditions +cy.get('[data-cy=loading]').should('not.exist'); +cy.get('[data-cy=user-list]').should('be.visible'); + +// Better: Wait for API calls +cy.intercept('GET', '/api/users').as('getUsers'); +cy.visit('/users'); +cy.wait('@getUsers'); +``` + +### Parallel Testing Configuration +```ruby +# config/database.yml +test: + <<: *default + database: myapp_test<%= ENV['TEST_ENV_NUMBER'] %> + +# config/initializers/cypress_on_rails.rb +if ENV['PARALLEL_WORKERS'].present? + CypressOnRails.configure do |c| + worker_id = ENV['TEST_ENV_NUMBER'] || '1' + c.server_port = 5000 + worker_id.to_i + end +end +``` + +## CI/CD Integration + +### GitHub Actions Example +```yaml +# .github/workflows/e2e.yml +name: E2E Tests +on: [push, pull_request] + +jobs: + cypress: + runs-on: ubuntu-latest + strategy: + matrix: + browser: [chrome, firefox, edge] + + services: + postgres: + image: postgres:14 + env: + POSTGRES_PASSWORD: password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: | + yarn install --frozen-lockfile + bundle install + + - name: Setup database + env: + RAILS_ENV: test + DATABASE_URL: postgresql://postgres:password@localhost:5432/test + run: | + bundle exec rails db:create + bundle exec rails db:schema:load + + - name: Run E2E tests + env: + RAILS_ENV: test + DATABASE_URL: postgresql://postgres:password@localhost:5432/test + run: | + bundle exec rails cypress:run + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-artifacts-${{ matrix.browser }} + path: | + e2e/cypress/screenshots + e2e/cypress/videos +``` + +### CircleCI Configuration +```yaml +# .circleci/config.yml +version: 2.1 + +orbs: + cypress: cypress-io/cypress@2 + +jobs: + e2e-tests: + docker: + - image: cimg/ruby:3.2-browsers + - image: cimg/postgres:14.0 + environment: + POSTGRES_PASSWORD: password + + parallelism: 4 + + steps: + - checkout + + - restore_cache: + keys: + - gem-cache-{{ checksum "Gemfile.lock" }} + - yarn-cache-{{ checksum "yarn.lock" }} + + - run: + name: Install dependencies + command: | + bundle install --path vendor/bundle + yarn install --frozen-lockfile + + - save_cache: + key: gem-cache-{{ checksum "Gemfile.lock" }} + paths: + - vendor/bundle + + - save_cache: + key: yarn-cache-{{ checksum "yarn.lock" }} + paths: + - node_modules + + - run: + name: Setup database + command: | + bundle exec rails db:create db:schema:load + environment: + RAILS_ENV: test + + - run: + name: Run E2E tests + command: | + TESTFILES=$(circleci tests glob "e2e/cypress/e2e/**/*.cy.js" | circleci tests split) + bundle exec rails cypress:run -- --spec $TESTFILES + + - store_test_results: + path: test-results + + - store_artifacts: + path: e2e/cypress/screenshots + + - store_artifacts: + path: e2e/cypress/videos + +workflows: + test: + jobs: + - e2e-tests +``` + +## Security Considerations + +### Protecting Test Endpoints +```ruby +# config/initializers/cypress_on_rails.rb +CypressOnRails.configure do |c| + # Only enable in test/development + c.use_middleware = !Rails.env.production? + + # Add authentication + c.before_request = lambda { |request| + # IP whitelist for CI + allowed_ips = ['127.0.0.1', '::1'] + allowed_ips += ENV['ALLOWED_CI_IPS'].split(',') if ENV['ALLOWED_CI_IPS'] + + unless allowed_ips.include?(request.ip) + return [403, {}, ['Forbidden']] + end + + # Token authentication + body = JSON.parse(request.body.string) + expected_token = ENV.fetch('CYPRESS_SECRET_TOKEN', 'development-token') + + if body['auth_token'] != expected_token + return [401, {}, ['Unauthorized']] + end + + nil + } +end +``` + +### Environment Variables +```bash +# .env.test +CYPRESS_SECRET_TOKEN=secure-random-token-here +CYPRESS_BASE_URL=http://localhost:5017 +CYPRESS_RAILS_HOST=localhost +CYPRESS_RAILS_PORT=5017 + +# Never commit real credentials +DATABASE_URL=postgresql://localhost/myapp_test +REDIS_URL=redis://localhost:6379/1 +``` + +### Sanitizing Test Data +```ruby +# e2e/app_commands/factory_bot.rb +class SafeFactoryBot + BLOCKED_FACTORIES = %w[admin super_admin payment_method].freeze + + def self.create(factory_name, *args) + raise "Factory '#{factory_name}' is blocked in tests" if BLOCKED_FACTORIES.include?(factory_name.to_s) + + FactoryBot.create(factory_name, *args) + end +end +``` + +## Debugging Strategies + +### Verbose Logging +```ruby +# config/initializers/cypress_on_rails.rb +CypressOnRails.configure do |c| + c.logger = Logger.new(STDOUT) + c.logger.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::INFO +end + +# e2e/app_commands/debug.rb +def perform + logger.debug "Current user count: #{User.count}" + logger.debug "Environment: #{Rails.env}" + logger.debug "Database: #{ActiveRecord::Base.connection.current_database}" + + result = yield if block_given? + + logger.debug "Result: #{result.inspect}" + result +end +``` + +### Screenshot on Failure +```js +// cypress/support/index.js +Cypress.on('fail', (error, runnable) => { + // Take screenshot before failing + cy.screenshot(`failed-${runnable.title}`, { capture: 'fullPage' }); + + // Log additional debugging info + cy.task('log', { + test: runnable.title, + error: error.message, + url: cy.url(), + timestamp: new Date().toISOString() + }); + + throw error; +}); +``` + +### Interactive Debugging +```js +// Add debugger statements +it('debug this test', () => { + cy.appFactories([['create', 'user']]); + + cy.visit('/users'); + debugger; // Cypress will pause here in open mode + + cy.get('[data-cy=user-list]').should('exist'); +}); + +// Or use cy.pause() +it('pause execution', () => { + cy.visit('/users'); + cy.pause(); // Manually resume in Cypress UI + cy.get('[data-cy=user-list]').should('exist'); +}); +``` + +## Common Patterns + +### Authentication Flow +```js +// cypress/support/commands.js +Cypress.Commands.add('login', (email, password) => { + cy.session([email, password], () => { + cy.visit('/login'); + cy.get('[data-cy=email]').type(email); + cy.get('[data-cy=password]').type(password); + cy.get('[data-cy=submit]').click(); + cy.url().should('include', '/dashboard'); + }); +}); + +// Usage +beforeEach(() => { + cy.login('user@example.com', 'password'); +}); +``` + +### File Upload Testing +```js +it('uploads a file', () => { + cy.fixture('document.pdf', 'base64').then(fileContent => { + cy.get('[data-cy=file-input]').attachFile({ + fileContent, + fileName: 'document.pdf', + mimeType: 'application/pdf', + encoding: 'base64' + }); + }); + + cy.get('[data-cy=upload-button]').click(); + cy.contains('File uploaded successfully'); +}); +``` + +### API Mocking +```js +it('handles API errors gracefully', () => { + // Mock failed API response + cy.intercept('POST', '/api/users', { + statusCode: 500, + body: { error: 'Internal Server Error' } + }).as('createUser'); + + cy.visit('/users/new'); + cy.get('[data-cy=submit]').click(); + + cy.wait('@createUser'); + cy.contains('Something went wrong'); +}); +``` + +### Testing Async Operations +```js +it('waits for async operations', () => { + // Start a background job + cy.appEval(` + ImportJob.perform_later('large_dataset.csv') + `); + + cy.visit('/imports'); + + // Poll for completion + cy.get('[data-cy=import-status]', { timeout: 30000 }) + .should('contain', 'Completed'); +}); +``` + +## Summary + +Following these best practices will help you: +- Write more maintainable and reliable tests +- Improve test performance and reduce flakiness +- Better organize your test code +- Secure your test infrastructure +- Debug issues more effectively + +Remember: E2E tests should focus on critical user journeys. Use unit and integration tests for comprehensive coverage of edge cases. \ No newline at end of file diff --git a/docs/DX_IMPROVEMENTS.md b/docs/DX_IMPROVEMENTS.md new file mode 100644 index 0000000..2a4c8d8 --- /dev/null +++ b/docs/DX_IMPROVEMENTS.md @@ -0,0 +1,163 @@ +# Developer Experience Improvements + +Based on analysis of user issues and feedback, here are the key improvements made to cypress-playwright-on-rails to enhance developer experience. + +## 🎯 Issues Addressed + +### 1. Manual Server Management (#152, #153) +**Previous Pain Point:** Users had to manually start Rails server in a separate terminal. + +**Solution Implemented:** +- ✅ Added rake tasks: `cypress:open`, `cypress:run`, `playwright:open`, `playwright:run` +- ✅ Automatic server lifecycle management +- ✅ Dynamic port selection +- ✅ Server hooks for customization + +### 2. Playwright Feature Parity (#169) +**Previous Pain Point:** Playwright users lacked documentation and helper functions. + +**Solution Implemented:** +- ✅ Comprehensive [Playwright Guide](PLAYWRIGHT_GUIDE.md) +- ✅ Complete helper functions in examples +- ✅ Migration guide from Cypress to Playwright +- ✅ Playwright-specific rake tasks + +### 3. VCR Configuration Confusion (#175, #160) +**Previous Pain Point:** VCR integration was poorly documented and error-prone. + +**Solution Implemented:** +- ✅ Detailed [VCR Integration Guide](VCR_GUIDE.md) +- ✅ Troubleshooting for common VCR errors +- ✅ GraphQL-specific VCR configuration +- ✅ Examples for both insert/eject and use_cassette modes + +### 4. Test Environment Issues (#157, #118) +**Previous Pain Point:** Confusion about running in test vs development environment. + +**Solution Implemented:** +- ✅ Clear documentation in [Troubleshooting Guide](TROUBLESHOOTING.md) +- ✅ Environment configuration examples +- ✅ Support for `CYPRESS_RAILS_HOST` and `CYPRESS_RAILS_PORT` +- ✅ Guidance on enabling file watching in test environment + +### 5. Database Management (#155, #114) +**Previous Pain Point:** Database cleaning issues and lack of transactional support. + +**Solution Implemented:** +- ✅ Transactional test mode with automatic rollback +- ✅ Smart database cleaning strategies +- ✅ ApplicationRecord error handling +- ✅ Rails transactional fixtures support + +### 6. Authentication & Security (#137) +**Previous Pain Point:** No built-in way to secure test endpoints. + +**Solution Implemented:** +- ✅ `before_request` hook for authentication +- ✅ Security best practices documentation +- ✅ IP whitelisting examples +- ✅ Token-based authentication examples + +## 📊 Impact Summary + +### Before These Improvements +- 😤 Manual server management required +- 📖 Sparse documentation +- 🔍 Issues buried in GitHub +- 🐛 Common errors without solutions +- 🎭 Playwright as second-class citizen + +### After These Improvements +- 🚀 One-command test execution +- 📚 Comprehensive documentation +- 🛠 Solutions for all common issues +- ✨ Feature parity for Playwright +- 🔒 Security best practices included + +## 🗺 Documentation Structure + +``` +docs/ +├── BEST_PRACTICES.md # Patterns and recommendations +├── TROUBLESHOOTING.md # Solutions to common issues +├── PLAYWRIGHT_GUIDE.md # Complete Playwright documentation +├── VCR_GUIDE.md # VCR integration details +└── DX_IMPROVEMENTS.md # This file +``` + +## 🚀 Quick Wins for New Users + +1. **Start Testing in 30 Seconds** + ```bash + gem 'cypress-on-rails' + bundle install + rails g cypress_on_rails:install + rails cypress:open # Done! + ``` + +2. **Switch from cypress-rails** + - Drop-in replacement with same commands + - Migration guide in CHANGELOG + +3. **Debug Failures Easily** + - Comprehensive troubleshooting guide + - Common errors with solutions + - Stack Overflow-style Q&A format + +## 🔮 Future Improvements + +Based on remaining open issues, consider implementing: + +1. **Parallel Testing Support (#119)** + - Native parallel execution + - Automatic database partitioning + - CI-specific optimizations + +2. **Better Error Messages** + - Contextual help in error output + - Links to relevant documentation + - Suggested fixes + +3. **Interactive Setup Wizard** + - Guided installation process + - Framework detection + - Automatic configuration + +4. **Performance Monitoring** + - Test execution metrics + - Slow test detection + - Optimization suggestions + +## 💡 Developer Experience Principles + +These improvements follow key DX principles: + +1. **Zero to Testing Fast** - Minimize time to first test +2. **Pit of Success** - Make the right thing the easy thing +3. **Progressive Disclosure** - Simple things simple, complex things possible +4. **Excellent Error Messages** - Every error should suggest a solution +5. **Documentation as Code** - Keep docs next to implementation +6. **Community Driven** - Address real user pain points + +## 📈 Metrics of Success + +Improvements can be measured by: +- ⬇️ Reduced issue creation for solved problems +- ⬇️ Decreased time to first successful test +- ⬆️ Increased adoption rate +- ⬆️ Higher user satisfaction +- 🔄 More contributions from community + +## 🤝 Contributing + +To continue improving developer experience: + +1. **Report Issues** with detailed reproduction steps +2. **Suggest Improvements** via GitHub discussions +3. **Share Solutions** that worked for you +4. **Contribute Examples** to documentation +5. **Help Others** in Slack/forums + +## Conclusion + +These documentation and feature improvements directly address the most common pain points users face. By providing comprehensive guides, troubleshooting resources, and automated solutions, we've significantly improved the developer experience for both new and existing users of cypress-playwright-on-rails. \ No newline at end of file diff --git a/docs/PLAYWRIGHT_GUIDE.md b/docs/PLAYWRIGHT_GUIDE.md new file mode 100644 index 0000000..3c3184d --- /dev/null +++ b/docs/PLAYWRIGHT_GUIDE.md @@ -0,0 +1,554 @@ +# Complete Playwright Guide + +This guide provides comprehensive documentation for using Playwright with cypress-playwright-on-rails. + +## Table of Contents +- [Installation](#installation) +- [Basic Setup](#basic-setup) +- [Commands and Helpers](#commands-and-helpers) +- [Factory Bot Integration](#factory-bot-integration) +- [Fixtures and Scenarios](#fixtures-and-scenarios) +- [Database Management](#database-management) +- [Advanced Configuration](#advanced-configuration) +- [Migration from Cypress](#migration-from-cypress) + +## Installation + +### 1. Add the gem to your Gemfile +```ruby +group :test, :development do + gem 'cypress-on-rails', '~> 1.0' +end +``` + +### 2. Install with Playwright framework +```bash +bundle install +bin/rails g cypress_on_rails:install --framework playwright + +# Or with custom folder +bin/rails g cypress_on_rails:install --framework playwright --install_folder=spec/e2e +``` + +### 3. Install Playwright +```bash +# Using yarn +yarn add -D @playwright/test + +# Using npm +npm install --save-dev @playwright/test + +# Install browsers +npx playwright install +``` + +## Basic Setup + +### Directory Structure +``` +e2e/ +├── playwright/ +│ ├── e2e/ # Test files +│ │ └── example.spec.js +│ ├── support/ +│ │ └── on-rails.js # Helper functions +│ ├── e2e_helper.rb # Ruby helper +│ └── app_commands/ # Ruby commands +│ ├── clean.rb +│ ├── factory_bot.rb +│ └── scenarios/ +│ └── basic.rb +└── playwright.config.js # Playwright configuration +``` + +### Playwright Configuration +```js +// playwright.config.js +module.exports = { + testDir: './e2e/playwright/e2e', + timeout: 30000, + use: { + baseURL: process.env.BASE_URL || 'http://localhost:5017', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure' + }, + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } } + ] +}; +``` + +## Commands and Helpers + +### Complete on-rails.js Helper File +```js +// e2e/playwright/support/on-rails.js +const { request } = require('@playwright/test'); + +const API_PREFIX = ''; // or '/api' if configured + +async function appCommands(body) { + const context = await request.newContext(); + const response = await context.post(`${API_PREFIX}/__e2e__/command`, { + data: body, + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok()) { + const text = await response.text(); + throw new Error(`Command failed: ${response.status()} - ${text}`); + } + + return response.json(); +} + +async function app(name, commandOptions = {}) { + const result = await appCommands({ + name, + options: commandOptions + }); + return result[0]; +} + +async function appScenario(name, options = {}) { + return app(`scenarios/${name}`, options); +} + +async function appFactories(factories) { + return app('factory_bot', factories); +} + +async function appFixtures() { + return app('activerecord_fixtures'); +} + +async function appClean() { + return app('clean'); +} + +async function appEval(code) { + return app('eval', { code }); +} + +module.exports = { + app, + appCommands, + appScenario, + appFactories, + appFixtures, + appClean, + appEval +}; +``` + +### Using Helpers in Tests +```js +// e2e/playwright/e2e/user.spec.js +const { test, expect } = require('@playwright/test'); +const { app, appFactories, appScenario, appClean } = require('../support/on-rails'); + +test.describe('User Management', () => { + test.beforeEach(async () => { + await appClean(); + }); + + test('create and view user', async ({ page }) => { + // Create user with factory bot + const users = await appFactories([ + ['create', 'user', { name: 'John Doe', email: 'john@example.com' }] + ]); + + await page.goto(`/users/${users[0].id}`); + await expect(page.locator('h1')).toContainText('John Doe'); + }); + + test('load scenario', async ({ page }) => { + await appScenario('user_with_posts'); + await page.goto('/users'); + await expect(page.locator('.user-count')).toContainText('5 users'); + }); +}); +``` + +## Factory Bot Integration + +### Creating Records +```js +test('factory bot examples', async ({ page }) => { + // Single record + const user = await appFactories([ + ['create', 'user', { name: 'Alice' }] + ]); + + // Multiple records + const posts = await appFactories([ + ['create_list', 'post', 3, { published: true }] + ]); + + // With traits + const adminUser = await appFactories([ + ['create', 'user', 'admin', { name: 'Admin User' }] + ]); + + // With associations + const postWithComments = await appFactories([ + ['create', 'post', 'with_comments', { comment_count: 5 }] + ]); + + // Building without saving + const userData = await appFactories([ + ['build', 'user', { name: 'Not Saved' }] + ]); +}); +``` + +### Using Attributes For +```js +test('get factory attributes', async ({ page }) => { + const attributes = await appFactories([ + ['attributes_for', 'user'] + ]); + + // Use attributes to fill form + await page.fill('[name="user[name]"]', attributes[0].name); + await page.fill('[name="user[email]"]', attributes[0].email); +}); +``` + +## Fixtures and Scenarios + +### Loading Rails Fixtures +```js +test('load fixtures', async ({ page }) => { + await appFixtures(); + + await page.goto('/products'); + // Fixtures are loaded +}); +``` + +### Creating Scenarios +```ruby +# e2e/playwright/app_commands/scenarios/complex_setup.rb +# Create a complex test scenario +5.times do |i| + user = User.create!( + name: "User #{i}", + email: "user#{i}@example.com" + ) + + 3.times do |j| + user.posts.create!( + title: "Post #{j} by User #{i}", + content: "Content for post #{j}", + published: j.even? + ) + end +end + +# Add some comments +Post.published.each do |post| + 2.times do + post.comments.create!( + author: "Commenter", + content: "Great post!" + ) + end +end +``` + +Using scenarios in tests: +```js +test('complex scenario', async ({ page }) => { + await appScenario('complex_setup'); + + await page.goto('/posts'); + await expect(page.locator('.post')).toHaveCount(15); + await expect(page.locator('.published')).toHaveCount(7); +}); +``` + +## Database Management + +### Cleaning Between Tests +```js +// Global setup +test.beforeEach(async () => { + await appClean(); +}); + +// Or selectively +test('with fresh database', async ({ page }) => { + await appClean(); + await app('load_seed'); // Optionally load seeds + + // Your test here +}); +``` + +### Custom Clean Commands +```ruby +# e2e/playwright/app_commands/clean.rb +if defined?(DatabaseCleaner) + DatabaseCleaner.strategy = :truncation + DatabaseCleaner.clean +else + # Manual cleaning + tables = ActiveRecord::Base.connection.tables + tables.delete('schema_migrations') + tables.delete('ar_internal_metadata') + + tables.each do |table| + ActiveRecord::Base.connection.execute("DELETE FROM #{table}") + end +end + +# Reset sequences for PostgreSQL +if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + ActiveRecord::Base.connection.tables.each do |table| + ActiveRecord::Base.connection.reset_pk_sequence!(table) + end +end + +Rails.cache.clear if Rails.cache +``` + +## Advanced Configuration + +### Running Custom Ruby Code +```js +test('execute ruby code', async ({ page }) => { + // Run arbitrary Ruby code + const result = await appEval(` + User.count + `); + console.log('User count:', result); + + // More complex evaluation + const stats = await appEval(` + { + users: User.count, + posts: Post.count, + latest_user: User.last&.name + } + `); +}); +``` + +### Authentication for Commands +```js +// e2e/playwright/support/authenticated-on-rails.js +const TOKEN = process.env.CYPRESS_SECRET_TOKEN; + +async function authenticatedCommand(name, options = {}) { + const context = await request.newContext(); + const response = await context.post('/__e2e__/command', { + data: { + name, + options, + cypress_token: TOKEN + }, + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.status() === 401) { + throw new Error('Authentication failed'); + } + + return response.json(); +} +``` + +### Parallel Testing +```js +// playwright.config.js +module.exports = { + workers: 4, // Run 4 tests in parallel + fullyParallel: true, + + use: { + // Each worker gets unique database + baseURL: process.env.BASE_URL || 'http://localhost:5017', + }, + + globalSetup: './global-setup.js', + globalTeardown: './global-teardown.js' +}; + +// global-setup.js +module.exports = async config => { + // Setup databases for parallel workers + for (let i = 0; i < config.workers; i++) { + process.env[`TEST_ENV_NUMBER_${i}`] = i.toString(); + } +}; +``` + +## Migration from Cypress + +### Command Comparison + +| Cypress | Playwright | +|---------|------------| +| `cy.app('clean')` | `await app('clean')` | +| `cy.appFactories([...])` | `await appFactories([...])` | +| `cy.appScenario('name')` | `await appScenario('name')` | +| `cy.visit('/path')` | `await page.goto('/path')` | +| `cy.contains('text')` | `await expect(page.locator('text')).toBeVisible()` | +| `cy.get('.class')` | `page.locator('.class')` | +| `cy.click()` | `await locator.click()` | + +### Converting Test Files +```js +// Cypress version +describe('Test', () => { + beforeEach(() => { + cy.app('clean'); + cy.appFactories([ + ['create', 'user', { name: 'Test' }] + ]); + }); + + it('works', () => { + cy.visit('/users'); + cy.contains('Test'); + }); +}); + +// Playwright version +const { test, expect } = require('@playwright/test'); +const { app, appFactories } = require('../support/on-rails'); + +test.describe('Test', () => { + test.beforeEach(async () => { + await app('clean'); + await appFactories([ + ['create', 'user', { name: 'Test' }] + ]); + }); + + test('works', async ({ page }) => { + await page.goto('/users'); + await expect(page.locator('text=Test')).toBeVisible(); + }); +}); +``` + +## Running Tests + +### Using Rake Tasks +```bash +# Open Playwright UI +bin/rails playwright:open + +# Run tests headless +bin/rails playwright:run +``` + +### Manual Execution +```bash +# Start Rails server +CYPRESS=1 bin/rails server -p 5017 + +# In another terminal +npx playwright test + +# With specific browser +npx playwright test --project=chromium + +# With UI mode +npx playwright test --ui + +# Debug mode +npx playwright test --debug +``` + +### CI Configuration +```yaml +# .github/workflows/playwright.yml +name: Playwright Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - run: yarn install + - run: npx playwright install --with-deps + + - run: bundle exec rails db:create db:schema:load + env: + RAILS_ENV: test + + - run: bundle exec rails playwright:run + env: + RAILS_ENV: test + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: playwright-traces + path: test-results/ +``` + +## Best Practices + +1. **Always clean between tests** to ensure isolation +2. **Use Page Object Model** for complex applications +3. **Leverage Playwright's auto-waiting** instead of explicit waits +4. **Run tests in parallel** for faster CI +5. **Use fixtures for static data**, factories for dynamic data +6. **Commit test recordings** for debugging failures + +## Debugging + +### Enable Debug Mode +```bash +# Run with debug +PWDEBUG=1 npx playwright test + +# This will: +# - Open browser in headed mode +# - Open Playwright Inspector +# - Pause at the start of each test +``` + +### Using Traces +```js +// Enable traces for debugging +const { chromium } = require('playwright'); + +test('debug this', async ({ page }, testInfo) => { + // Start tracing + await page.context().tracing.start({ + screenshots: true, + snapshots: true + }); + + // Your test + await page.goto('/'); + + // Save trace + await page.context().tracing.stop({ + path: `trace-${testInfo.title}.zip` + }); +}); +``` + +View traces: +```bash +npx playwright show-trace trace-debug-this.zip +``` \ No newline at end of file diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..8205a0e --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,351 @@ +# Troubleshooting Guide + +This guide addresses common issues and questions when using cypress-playwright-on-rails. + +## Table of Contents +- [VCR Integration Issues](#vcr-integration-issues) +- [Playwright Support](#playwright-support) +- [Test Environment Configuration](#test-environment-configuration) +- [Database and Transaction Issues](#database-and-transaction-issues) +- [Authentication and Security](#authentication-and-security) +- [Performance and Parallel Testing](#performance-and-parallel-testing) +- [Common Errors](#common-errors) + +## VCR Integration Issues + +### Issue: "No route matches [POST] '/api/__e2e__/vcr/insert'" (#175) + +**Problem:** VCR middleware is not properly configured or mounted. + +**Solution:** +1. Ensure VCR middleware is enabled in `config/initializers/cypress_on_rails.rb`: +```ruby +CypressOnRails.configure do |c| + c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present? + + c.vcr_options = { + hook_into: :webmock, + default_cassette_options: { record: :once }, + cassette_library_dir: Rails.root.join('spec/fixtures/vcr_cassettes') + } +end +``` + +2. Add to your `cypress/support/index.js`: +```js +import 'cypress-on-rails/support/index' +``` + +3. Make sure your API prefix matches: +```ruby +c.api_prefix = '/api' # If your app uses /api prefix +``` + +### Using VCR with GraphQL (#160) + +For GraphQL operations with `use_cassette`: +```ruby +CypressOnRails.configure do |c| + c.use_vcr_use_cassette_middleware = !Rails.env.production? && ENV['CYPRESS'].present? + # Note: Don't enable both VCR middlewares simultaneously +end +``` + +Add to `cypress/support/commands.js`: +```js +Cypress.Commands.add('mockGraphQL', () => { + cy.on('window:before:load', (win) => { + const originalFetch = win.fetch; + const fetch = (path, options, ...rest) => { + if (options && options.body) { + try { + const body = JSON.parse(options.body); + if (body.operationName) { + return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest); + } + } catch (e) { + return originalFetch(path, options, ...rest); + } + } + return originalFetch(path, options, ...rest); + }; + cy.stub(win, 'fetch', fetch); + }); +}); +``` + +## Playwright Support + +### Loading Fixtures in Playwright (#169) + +While Cypress has `cy.appFixtures()`, Playwright requires a different approach: + +**Solution 1: Create helper functions** +```js +// spec/playwright/support/on-rails.js +async function appFixtures() { + const response = await page.request.post('/__e2e__/command', { + data: { + name: 'activerecord_fixtures', + options: {} + } + }); + return response.json(); +} + +// Use in tests +test('load fixtures', async ({ page }) => { + await appFixtures(); + await page.goto('/'); +}); +``` + +**Solution 2: Use Factory Bot instead** +```js +// spec/playwright/support/factories.js +async function appFactories(factories) { + const response = await page.request.post('/__e2e__/command', { + data: { + name: 'factory_bot', + options: factories + } + }); + return response.json(); +} + +// Use in tests +test('create data', async ({ page }) => { + await appFactories([ + ['create', 'user', { name: 'Test User' }] + ]); + await page.goto('/users'); +}); +``` + +## Test Environment Configuration + +### Running Tests in Test Environment with Change Detection (#157) + +**Problem:** Running in development mode has different configuration than test mode. + +**Solution 1: Configure test environment with file watching** +```ruby +# config/environments/test.rb +if ENV['CYPRESS'].present? + # Enable file watching in test environment for Cypress + config.file_watcher = ActiveSupport::FileUpdateChecker + config.cache_classes = false + config.reload_classes_only_on_change = true +end +``` + +**Solution 2: Use custom environment** +```bash +# Create config/environments/cypress.rb based on test.rb +cp config/environments/test.rb config/environments/cypress.rb + +# Modify cypress.rb to enable reloading +# Run with: +RAILS_ENV=cypress CYPRESS=1 bin/rails server +``` + +### Headless Mode Configuration (#118) + +To run Cypress in truly headless mode: +```bash +# For CI/headless execution +bin/rails cypress:run + +# Or manually: +CYPRESS=1 bin/rails server -p 5017 & +yarn cypress run --headless --project ./e2e +``` + +## Database and Transaction Issues + +### ApplicationRecord MySQL Error (#155) + +**Problem:** ApplicationRecord being queried as a table. + +**Solution:** Exclude ApplicationRecord from logging: +```ruby +# spec/e2e/app_commands/log_fail.rb +def perform + load "#{Rails.root}/db/seeds.rb" if options && options['load_seeds'] + + descendants = ActiveRecord::Base.descendants + # Exclude abstract classes + descendants.reject! { |model| model.abstract_class? || model == ApplicationRecord } + + descendants.each_with_object({}) do |model, result| + result[model.name] = model.limit(100).map(&:attributes) + rescue => e + result[model.name] = { error: e.message } + end +end +``` + +### Using Rails Transactional Fixtures (#114) + +Instead of database_cleaner, use Rails built-in transactional fixtures: + +```ruby +# spec/e2e/app_commands/clean.rb +require 'active_record/test_fixtures' + +class TransactionalClean + include ActiveRecord::TestFixtures + + def perform + setup_fixtures + yield if block_given? + ensure + teardown_fixtures + end +end + +# Use with new rake tasks: +CypressOnRails.configure do |c| + c.transactional_server = true # Enables automatic rollback +end +``` + +## Authentication and Security + +### Authenticating Commands (#137) + +Protect your commands with authentication: + +```ruby +# config/initializers/cypress_on_rails.rb +CypressOnRails.configure do |c| + c.before_request = lambda { |request| + body = JSON.parse(request.body.string) + + # Option 1: Token-based auth + if body['cypress_token'] != ENV.fetch('CYPRESS_SECRET_TOKEN') + return [401, {}, ['unauthorized']] + end + + # Option 2: Warden/Devise auth + # if !request.env['warden'].authenticate(:secret_key) + # return [403, {}, ['forbidden']] + # end + + nil # Continue with command execution + } +end +``` + +In Cypress tests: +```js +Cypress.Commands.overwrite('app', (originalFn, name, options) => { + return originalFn(name, { + ...options, + cypress_token: Cypress.env('SECRET_TOKEN') + }); +}); +``` + +## Performance and Parallel Testing + +### Parallel Test Execution (#119) + +While not built-in, you can achieve parallel testing: + +**Option 1: Using cypress-parallel** +```bash +yarn add -D cypress-parallel + +# In package.json +"scripts": { + "cy:parallel": "cypress-parallel -s cy:run -t 4" +} +``` + +**Option 2: Database partitioning** +```ruby +# config/initializers/cypress_on_rails.rb +if ENV['CYPRESS_PARALLEL_ID'].present? + # Use different database per parallel process + config.database_name = "test_cypress_#{ENV['CYPRESS_PARALLEL_ID']}" +end +``` + +**Option 3: CircleCI Parallelization** +```yaml +# .circleci/config.yml +jobs: + cypress: + parallelism: 4 + steps: + - run: + command: | + TESTFILES=$(circleci tests glob "e2e/**/*.cy.js" | circleci tests split) + yarn cypress run --spec $TESTFILES +``` + +## Common Errors + +### Webpack Compilation Error (#146) + +**Error:** "Module not found: Error: Can't resolve 'cypress-factory'" + +**Solution:** This is usually a path issue. Check: +1. Your support file imports the correct path: +```js +// cypress/support/index.js +import './on-rails' // Not 'cypress-factory' +``` + +2. Ensure the file exists at the expected location +3. Clear Cypress cache if needed: +```bash +yarn cypress cache clear +yarn install +``` + +### Server Not Starting + +If rake tasks fail to start the server: +```ruby +# Check for port conflicts +lsof -i :3001 + +# Use a different port +CYPRESS_RAILS_PORT=5017 bin/rails cypress:open + +# Or configure in initializer +CypressOnRails.configure do |c| + c.server_port = 5017 +end +``` + +### State Not Resetting Between Tests + +Ensure clean state: +```js +// cypress/support/index.js +beforeEach(() => { + cy.app('clean'); + cy.app('load_seed'); // Optional +}); + +// Or use state reset endpoint +beforeEach(() => { + cy.request('POST', '/cypress_rails_reset_state'); +}); +``` + +## Getting Help + +If you encounter issues not covered here: + +1. Check existing [GitHub issues](https://github.com/shakacode/cypress-playwright-on-rails/issues) +2. Search the [Slack channel](https://join.slack.com/t/reactrails/shared_invite/enQtNjY3NTczMjczNzYxLTlmYjdiZmY3MTVlMzU2YWE0OWM0MzNiZDI0MzdkZGFiZTFkYTFkOGVjODBmOWEyYWQ3MzA2NGE1YWJjNmVlMGE) +3. Post in the [forum](https://forum.shakacode.com/c/cypress-on-rails/55) +4. Create a new issue with: + - Your Rails version + - cypress-on-rails version + - Minimal reproduction steps + - Full error messages and stack traces \ No newline at end of file diff --git a/docs/VCR_GUIDE.md b/docs/VCR_GUIDE.md new file mode 100644 index 0000000..911ae48 --- /dev/null +++ b/docs/VCR_GUIDE.md @@ -0,0 +1,499 @@ +# VCR Integration Guide + +Complete guide for recording and replaying HTTP interactions in your tests using VCR with cypress-playwright-on-rails. + +## Table of Contents +- [Overview](#overview) +- [Installation](#installation) +- [Configuration](#configuration) +- [Insert/Eject Mode](#inserteject-mode) +- [Use Cassette Mode](#use-cassette-mode) +- [GraphQL Integration](#graphql-integration) +- [Advanced Usage](#advanced-usage) +- [Troubleshooting](#troubleshooting) + +## Overview + +VCR (Video Cassette Recorder) records your test suite's HTTP interactions and replays them during future test runs for fast, deterministic tests. This is particularly useful for: +- Testing against third-party APIs +- Avoiding rate limits +- Testing without internet connection +- Ensuring consistent test data +- Speeding up test execution + +## Installation + +### 1. Add required gems +```ruby +# Gemfile +group :test, :development do + gem 'vcr' + gem 'webmock' + gem 'cypress-on-rails', '~> 1.0' +end +``` + +### 2. Install npm package (optional, for enhanced features) +```bash +yarn add -D cypress-on-rails +# or +npm install --save-dev cypress-on-rails +``` + +## Configuration + +### Basic VCR Setup + +```ruby +# config/initializers/cypress_on_rails.rb +CypressOnRails.configure do |c| + # Enable VCR middleware + c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present? + + # VCR configuration options + c.vcr_options = { + # HTTP library to hook into + hook_into: :webmock, + + # Default recording mode + default_cassette_options: { + record: :once, # :once, :new_episodes, :none, :all + match_requests_on: [:method, :uri, :body], + allow_unused_http_interactions: false + }, + + # Where to save cassettes + cassette_library_dir: Rails.root.join('spec/fixtures/vcr_cassettes'), + + # Configure which hosts to ignore + ignore_hosts: ['localhost', '127.0.0.1', '0.0.0.0'], + + # Filter sensitive data + filter_sensitive_data: { + '