Back to Repositories

Testing Web Interaction Patterns in Capybara Test Suite

This Sinatra-based test application serves as a comprehensive testing suite for Capybara’s web interaction capabilities. It implements various HTTP endpoints and scenarios to validate Capybara’s core functionalities in a controlled environment.

Test Coverage Overview

The test suite provides extensive coverage of web application interactions and responses.

Key areas tested include:
  • HTTP request handling (GET, POST, DELETE)
  • Cookie management and session handling
  • File uploads and form submissions
  • Redirect scenarios and error responses
  • Content-type handling and header manipulation

Implementation Analysis

The implementation uses Sinatra as the web framework to create a lightweight test environment. It employs RESTful routing patterns and includes specialized endpoints for testing various Capybara features like navigation, form handling, and AJAX responses.

Notable patterns include:
  • Custom error handling classes
  • Dynamic view rendering with ERB
  • Configurable response behaviors
  • Stateful request tracking

Technical Details

Testing infrastructure includes:
  • Sinatra for routing and request handling
  • Rack middleware integration
  • ERB templating engine
  • YAML for data formatting
  • Custom middleware for session management
  • Puma server for test execution

Best Practices Demonstrated

The test suite exemplifies several testing best practices including isolation of test scenarios, comprehensive edge case coverage, and clear separation of concerns.

Notable practices include:
  • Modular endpoint organization
  • Consistent error handling patterns
  • Flexible configuration options
  • Comprehensive HTTP method coverage
  • Realistic web application scenarios

teamcapybara/capybara

lib/capybara/spec/test_app.rb

            
# frozen_string_literal: true

require 'sinatra/base'
require 'tilt/erb'
require 'rack'
require 'yaml'

class TestApp < Sinatra::Base
  class TestAppError < Exception; end # rubocop:disable Lint/InheritException

  class TestAppOtherError < Exception # rubocop:disable Lint/InheritException
    def initialize(string1, msg)
      super()
      @something = string1
      @message = msg
    end
  end
  set :root, File.dirname(__FILE__)
  set :static, true
  set :raise_errors, true
  set :show_exceptions, false

  # Also check lib/capybara/spec/views/*.erb for pages not listed here

  get '/' do
    response.set_cookie('capybara', value: 'root cookie', domain: request.host, path: request.path)
    'Hello world! <a href="with_html">Relative</a>'
  end

  get '/foo' do
    'Another World'
  end

  get '/redirect' do
    redirect '/redirect_again'
  end

  get '/redirect_with_fragment' do
    redirect '/landed#with_fragment'
  end

  get '/redirect_again' do
    redirect '/landed'
  end

  post '/redirect_307' do
    redirect '/landed', 307
  end

  post '/redirect_308' do
    redirect '/landed', 308
  end

  get '/referer_base' do
    '<a href="/get_referer">direct link</a>' \
      '<a href="/redirect_to_get_referer">link via redirect</a>' \
      '<form action="/get_referer" method="get"><input type="submit"></form>'
  end

  get '/redirect_to_get_referer' do
    redirect '/get_referer'
  end

  get '/get_referer' do
    request.referer.nil? ? 'No referer' : "Got referer: #{request.referer}"
  end

  get '/host' do
    "Current host is #{request.scheme}://#{request.host}:#{request.port}"
  end

  get '/redirect/:times/times' do
    times = params[:times].to_i
    if times.zero?
      'redirection complete'
    else
      redirect "/redirect/#{times - 1}/times"
    end
  end

  get '/landed' do
    'You landed'
  end

  post '/landed' do
    "You post landed: #{params.dig(:form, 'data')}"
  end

  get '/with-quotes' do
    %q("No," he said, "you can't do that.")
  end

  get '/form/get' do
    %(<pre id="results">#{params[:form].to_yaml}</pre>)
  end

  post '/relative' do
    %(<pre id="results">#{params[:form].to_yaml}</pre>)
  end

  get '/favicon.ico' do
    nil
  end

  post '/redirect' do
    redirect '/redirect_again'
  end

  delete '/delete' do
    'The requested object was deleted'
  end

  get '/delete' do
    'Not deleted'
  end

  get '/redirect_back' do
    redirect back
  end

  get '/redirect_secure' do
    redirect "https://#{request.host}:#{request.port}/host"
  end

  get '/slow_response' do
    sleep 2
    'Finally!'
  end

  get '/set_cookie' do
    cookie_value = 'test_cookie'
    response.set_cookie('capybara', cookie_value)
    "Cookie set to #{cookie_value}"
  end

  get '/get_cookie' do
    request.cookies['capybara']
  end

  get '/get_header' do
    env['HTTP_FOO']
  end

  get '/get_header_via_redirect' do
    redirect '/get_header'
  end

  get '/error' do
    raise TestAppError, 'some error'
  end

  get '/other_error' do
    raise TestAppOtherError.new('something', 'other error')
  end

  get '/load_error' do
    raise LoadError
  end

  get '/with.*html' do
    erb :with_html, locals: { referrer: request.referrer }
  end

  get '/with_title' do
    <<-HTML
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
          <title>#{params[:title] || 'Test Title'}</title>
        </head>

        <body>
          <svg><title>abcdefg</title></svg>
        </body>
      </html>
    HTML
  end

  get '/with_iframe' do
    <<-HTML
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
          <title>Test with Iframe</title>
        </head>

        <body>
          <iframe src="#{params[:url]}" id="#{params[:id]}"></iframe>
        </body>
      </html>
    HTML
  end

  get '/base/with_base' do
    <<-HTML
      <!DOCTYPE html>
      <html>
        <head>
          <base href="/">
          <title>Origin</title>
        </head>
        <body>
          <a href="with_title">Title page</a>
          <a href="?a=3">Bare query</a>
        </body>
      </html>
    HTML
  end

  get '/base/with_other_base' do
    <<-HTML
      <!DOCTYPE html>
      <html>
        <head>
          <base href="/base/">
          <title>Origin</title>
        </head>
        <body>
          <a href="with_title">Title page</a>
          <a href="?a=3">Bare query</a>
        </body>
      </html>
    HTML
  end

  get '/csp' do
    response.headers['Content-Security-Policy'] = "default-src 'none'; connect-src 'self'; base-uri 'none'; font-src 'self'; img-src 'self' data:; object-src 'none'; script-src 'self' 'nonce-jAviMuMisoTisVXjgLoWdA=='; style-src 'self' 'nonce-jAviMuMisoTisVXjgLoWdA=='; form-action 'self';"
    <<-HTML
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
          <title>CSP</title>
        </head>

        <body>
          <div>CSP</div>
        </body>
      </html>
    HTML
  end

  get '/download.csv' do
    content_type 'text/csv'
    'This, is, comma, separated' \
      'Thomas, Walpole, was , here'
  end

  get %r{/apple-touch-.*\.png/} do
    halt(404)
  end

  get '/:view' do |view|
    view_template = "#{__dir__}/views/#{view}.erb"
    has_layout = File.exist?(view_template) && File.open(view_template) { |f| f.first.downcase.include?('doctype') }
    erb view.to_sym, locals: { referrer: request.referrer }, layout: !has_layout
  end

  post '/form' do
    self.class.form_post_count += 1
    %(
      <pre id="content_type">#{request.content_type}</pre>
      <pre id="results">#{params.fetch(:form, {}).merge('post_count' => self.class.form_post_count).to_yaml}</pre>
    )
  end

  post '/upload_empty' do
    if params[:form][:file].nil?
      "Successfully ignored empty file field. Content type was #{request.content_type}"
    else
      'Something went wrong.'
    end
  end

  post '/upload' do
    buffer = []
    buffer << "Content-type: #{params.dig(:form, :document, :type)}"
    buffer << "File content: #{params.dig(:form, :document, :tempfile).read}"
    buffer.join(' | ')
  rescue StandardError
    'No file uploaded'
  end

  post '/upload_multiple' do
    docs = params.dig(:form, :multiple_documents)
    buffer = [docs.size.to_s]
    docs.each do |doc|
      buffer << "Content-type: #{doc[:type]}"
      buffer << "File content: #{doc[:tempfile].read}"
    end
    buffer.join(' | ')
  rescue StandardError
    'No files uploaded'
  end

  class << self
    attr_accessor :form_post_count
  end

  @form_post_count = 0
end

Rack::Handler::Puma.run TestApp, Port: 8070 if $PROGRAM_NAME == __FILE__