DEV Community

Christian Sedlmair
Christian Sedlmair

Posted on • Edited on

System Testing

Overview

Do you know Playwright?

Playwright is emerging as the new standard for front-end testing.

It is faster and more recent than Selenium, and its syntax is clearer than that of Capybara-DSL. See the discussion here: https://discuss.rubyonrails.org/t/rails-7-capybara-speedup-possible/87571.

Notice

Playwright can be used with the Capybara DSL, which is ideal for existing projects. Initially, I tried to keep the old tests in the Capybara DSL while writing the new ones in the Playwright DSL within the same project. However, this approach was unsuccessful, so ultimately I rewrote everything in Playwright.

As recommended, it is configured to create a new instance at the start of each run and open/close a new window for every test.

Playwright within Rails/RSpec

Derived from Docs

Setup

  • Remove all Selenium-gems
  • add gem capybara within group test
  • add gem playwright-ruby-client within group test
  • npm i --save-dev playwright (unlike docs do not install it by npx i ... because it would not install dependencies)

a File like playwright_helper.rb

(for a project with devise authentication)

require 'rails_helper'
require 'capybara'
require 'playwright'
require 'support/playwright_helpers'
require 'uri'

RSpec.configure do |config|
  config.include Devise::Test::IntegrationHelpers, type: :system
  config.include PlaywrightHelpers, type: :system

  # Set default driver for system tests
  Capybara.default_driver = :null
  Capybara.javascript_driver = :null

  video_dir = '/tmp/rails-system-test-videos'

  config.before(:all, type: :system) do
    FileUtils.rm_rf(video_dir)
    FileUtils.mkdir_p(video_dir)

    @playwright = Playwright.create(playwright_cli_executable_path: './node_modules/.bin/playwright')
    @browser = @playwright.playwright.firefox.launch(headless: false)
  end

  config.after(:all, type: :system) do
    # Close browser and Playwright after all tests
    @browser&.close
    @playwright&.stop
  end

  # Define the null driver
  class CapybaraNullDriver < Capybara::Driver::Base
    def needs_server?
      true
    end
  end

  Capybara.register_driver(:null) { CapybaraNullDriver.new }

  config.around(type: :system) do |example|
    driven_by :null

    # Create a new context and page for each test
    @browser.new_context(
      record_video_dir: video_dir,
      baseURL: Capybara.current_session.server.base_url
    ) do |context|
      context.set_default_timeout(2000.0)
      @page = context.new_page

      # Capture Browser console logs
      @browser_logs = []
      @page.on('console', lambda do |message|
        @browser_logs << { type: message.type, text: message.text }
      end)

      # LOGIN
      @current_user = FactoryBot.create :user_adm
      sign_in @current_user

      example.run

      # Save video only if the test fails
      if example.exception
        puts "file://#{@page.video.path}"
      else
        FileUtils.rm(@page.video.path) if @page.video&.path
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

support/playwright_helpers.rb

# Extends Playwright::Page with a convenience method to extract the URL path.

Playwright::Page.class_eval do
  def path
    URI(url).path
  end
end

# Helper methods for Playwright in system specs.
module PlaywrightHelpers

  # Retrieves browser logs, optionally filtered by log type and/or a matching pattern.
  #
  # @param log_type [String, Symbol, Array<String, Symbol>, nil] The log type(s) to filter by (e.g., 'error', 'warning').
  # @param match [Regexp, String, nil] A pattern to filter log messages.
  # @return [Array<String>, nil] Filtered log messages or nil if no logs match.
  def browser_logs(log_type: nil, match: nil)
    log_types = Array(log_type).map(&:to_s).reject(&:empty?)

    logs = if log_types.any?
             @browser_logs.select { |log| log_types.include?(log[:type]) }
                          .map { |log| log[:text] }
           else
             @browser_logs.map { |log| "[#{log[:type]}] #{log[:text]}" }
           end

    logs.select! { |log| log.match?(match) } if match

    logs unless logs.empty?
  end
end
Enter fullscreen mode Exit fullscreen mode

and a test:

require 'playwright_helper'

RSpec.describe "pw", type: :system do

    it 'test' do
      @page.goto(root_path)
      @page.locator('body').wait_for(state: :visible)
      # => without the .wait_for the test would not fail without the body-tag
    end
end
Enter fullscreen mode Exit fullscreen mode

Here is a Code Example

Mainly importand docs, for writing tests, may be the API Coverage and Page class

Overview

Top comments (0)