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, it uses the log_in method)

require 'rails_helper'
require 'capybara'
require 'playwright'
require 'support/playwright_utils'

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

  # Docs https://playwright-ruby-client.vercel.app/docs/article/guides/rails_integration_with_null_driver

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

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

    # Launch Playwright and browser once for all tests
    @playwright = Playwright.create(playwright_cli_executable_path: './node_modules/.bin/playwright')
    # Use browser_type to access Firefox
    @browser = @playwright.playwright.firefox.launch(headless: true)
  end

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

  class CapybaraNullDriver < Capybara::Driver::Base
    def needs_server?
      true
    end
  end

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

  config.around(driver: :null) 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 JavaScript console logs
      @browser_console_logs = []
      @page.on('console', lambda do |message|
        @browser_console_logs << { type: message.type, text: message.text }
      end)

      # LOGIN PROCESS
      User.destroy_all
      @current_user = FactoryBot.build :user_admin
      @current_user.roles << FactoryBot.create(:role, short: :scan)
      @current_user.save!
      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

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

# Helper methods for Playwright in system-ssr-tests specs.
module PlaywrightHelpers

  def wait_for_js_log(matcher, timeout: 2000)
    wait_for_js_log_condition(
      "browser log to #{matcher.is_a?(Regexp) ? 'match' : 'equal'} «#{matcher}»",
      timeout: timeout
    ) do
      js_logs.any? { |text| text.match?(matcher) || text == matcher }
    end
  end

  def wait_for_js_warning(matcher, timeout: 2000)
    wait_for_js_log_condition(
      "browser warning to #{matcher.is_a?(Regexp) ? 'match' : 'equal'} «#{matcher}»",
      timeout: timeout
    ) do
      js_warnings.any? { |text| text.match?(matcher) || text == matcher }
    end
  end

  def wait_for_js_error(matcher, timeout: 2000)
    wait_for_js_log_condition(
      "browser error to #{matcher.is_a?(Regexp) ? 'match' : 'equal'} «#{matcher}»",
      timeout: timeout
    ) do
      js_errors.any? { |text| text.match?(matcher) || text == matcher }
    end
  end

  def js_logs(type: nil)
    logs = if type.present?
             @browser_console_logs.select { |log| log[:type] == type.to_s }
           else
             @browser_console_logs
           end
    logs.map { |l| l[:text] } if logs.present?
  end

  def js_errors
    e = @browser_console_logs.select { |log| log[:type] == "error" }
    e.map { |l| l[:text] } if e.present?
  end

  def js_warnings
    w = @browser_console_logs.select { |log| log[:type] == "warning" }
    w.map { |l| l[:text] } if w.present?
  end

  private

  def wait_for_js_log_condition(notice, timeout: 2000, &condition)
    start_ms = Time.now.to_f * 1000

    loop do
      return true if condition.call

      elapsed_ms = (Time.now.to_f * 1000) - start_ms
      if elapsed_ms > timeout
        raise "Timeout: waited #{elapsed_ms.round} ms for #{notice}"
      end

      sleep 0.1
    end
  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)