capybara.rb 7.92 KB
Newer Older
1 2
# frozen_string_literal: true

3
# rubocop:disable Style/GlobalVars
4 5
require 'capybara/rails'
require 'capybara/rspec'
6
require 'capybara-screenshot/rspec'
7
require 'selenium-webdriver'
8 9

# Give CI some extra time
10
timeout = ENV['CI'] || ENV['CI_SERVER'] ? 60 : 30
11

12 13 14
# Support running Capybara on a specific port to allow saving commonly used pages
Capybara.server_port = ENV['CAPYBARA_PORT'] if ENV['CAPYBARA_PORT']

15 16 17 18 19 20 21
# Define an error class for JS console messages
JSConsoleError = Class.new(StandardError)

# Filter out innocuous JS console messages
JS_CONSOLE_FILTER = Regexp.union([
  '"[HMR] Waiting for update signal from WDS..."',
  '"[WDS] Hot Module Replacement enabled."',
22
  '"[WDS] Live Reloading enabled."',
23 24
  'Download the Vue Devtools extension',
  'Download the Apollo DevTools'
25 26
])

27 28
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze

29 30 31 32 33 34 35
# Run Workhorse on the given host and port, proxying to Puma on a UNIX socket,
# for a closer-to-production experience
Capybara.register_server :puma_via_workhorse do |app, port, host, **options|
  file = Tempfile.new
  socket_path = file.path
  file.close! # We just want the filename

36
  TestEnv.with_workhorse(host, port, socket_path) do
37 38 39 40
    Capybara.servers[:puma].call(app, nil, socket_path, **options)
  end
end

41 42
Capybara.register_driver :chrome do |app|
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
43 44 45 46 47 48
    # This enables access to logs with `page.driver.manage.get_log(:browser)`
    loggingPrefs: {
      browser: "ALL",
      client: "ALL",
      driver: "ALL",
      server: "ALL"
49
    }
50
  )
51

52
  options = Selenium::WebDriver::Chrome::Options.new
53 54 55 56

  # Force the browser's scale factor to prevent inconsistencies on high-res devices
  options.add_argument('--force-device-scale-factor=1')

57
  options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
58 59 60 61 62

  # Chrome won't work properly in a Docker container in sandbox mode
  options.add_argument("no-sandbox")

  # Run headless by default unless CHROME_HEADLESS specified
63
  options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
64

65
  # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab/issues/4252
Stan Hu's avatar
Stan Hu committed
66 67
  options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']

68
  # Explicitly set user-data-dir to prevent crashes. See https://gitlab.com/gitlab-org/gitlab-foss/issues/58882#note_179811508
69 70
  options.add_argument("user-data-dir=/tmp/chrome") if ENV['CI'] || ENV['CI_SERVER']

71 72 73
  # Chrome 75 defaults to W3C mode which doesn't allow console log access
  options.add_option(:w3c, false)

74 75 76 77 78 79
  Capybara::Selenium::Driver.new(
    app,
    browser: :chrome,
    desired_capabilities: capabilities,
    options: options
  )
80 81
end

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
Capybara.register_driver :firefox do |app|
  capabilities = Selenium::WebDriver::Remote::Capabilities.firefox(
    log: {
      level: :trace
    }
  )

  options = Selenium::WebDriver::Firefox::Options.new(log_level: :trace)

  options.add_argument("--window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")

  # Run headless by default unless WEBDRIVER_HEADLESS specified
  options.add_argument("--headless") unless ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i

  Capybara::Selenium::Driver.new(
    app,
    browser: :firefox,
    desired_capabilities: capabilities,
    options: options
  )
end

104
Capybara.server = :puma_via_workhorse
105
Capybara.javascript_driver = ENV.fetch('WEBDRIVER', :chrome).to_sym
106
Capybara.default_max_wait_time = timeout
107
Capybara.ignore_hidden_elements = true
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
108
Capybara.default_normalize_ws = true
Nick Thomas's avatar
Nick Thomas committed
109
Capybara.enable_aria_label = true
110

111 112 113
Capybara::Screenshot.append_timestamp = false

Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
114
  example.full_description.downcase.parameterize(separator: "_")[0..99]
115
end
116 117
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
118 119 120 121
# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
Capybara::Screenshot.register_driver(:chrome) do |driver, path|
  driver.browser.save_screenshot(path)
end
122 123

RSpec.configure do |config|
124 125
  config.include CapybaraHelpers, type: :feature

126
  config.before(:context, :js) do
127 128 129 130
    # This prevents Selenium from creating thousands of connections while waiting for
    # an element to appear
    webmock_enable_with_http_connect_on_start!

131 132
    next if $capybara_server_already_started

133
    TestEnv.eager_load_driver_server
134
    $capybara_server_already_started = true
135
  end
136

137 138 139 140 141 142 143
  config.after(:context, :js) do
    # WebMock doesn't stub connections, so we need to restore the original behavior
    # to prevent many specs from failing:
    # https://github.com/bblimke/webmock/blob/master/README.md#connecting-on-nethttpstart
    webmock_enable!
  end

144
  config.before(:example, :js) do
145 146
    session = Capybara.current_session

147
    allow(Gitlab::Application.routes).to receive(:default_url_options).and_return(
148 149
      host: session.server.host,
      port: session.server.port,
150 151
      protocol: 'http')

152 153 154 155
    # CSRF protection is disabled by default. We only enable this for JS specs because some forms
    # require Javascript to set the CSRF token.
    allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true)

156
    # reset window size between tests
157 158 159
    unless session.current_window.size == CAPYBARA_WINDOW_SIZE
      begin
        session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE)
160
      rescue StandardError # ?
161
      end
162
    end
163
  end
164

165 166 167 168 169 170 171 172
  # The :capybara_ignore_server_errors metadata means unhandled exceptions raised
  # by the application under test will not necessarily fail the server. This is
  # useful when testing conditions that are expected to raise a 500 error in
  # production; it should not be used on the happy path.
  config.around(:each, :capybara_ignore_server_errors) do |example|
    Capybara.raise_server_errors = false

    example.run
173 174 175
  ensure
    Capybara.raise_server_errors = true
  end
Maxime Orefice's avatar
Maxime Orefice committed
176

177
  config.append_after do |example|
Maxime Orefice's avatar
Maxime Orefice committed
178 179 180 181
    if example.metadata[:screenshot]
      screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html]
      example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]}
    end
182 183
  end

184
  config.after(:example, :js) do |example|
185
    # when a test fails, display any messages in the browser's console
186 187 188 189
    # but fail don't add the message if the failure is a pending test that got
    # fixed. If we raised the `JSException` the fixed test would be marked as
    # failed again.
    if example.exception && !example.exception.is_a?(RSpec::Core::Pending::PendingExampleFixedError)
190 191 192 193 194 195 196 197 198 199 200 201
      begin
        console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER }

        if console.present?
          message = "Unexpected browser console output:\n" + console.map(&:message).join("\n")
          raise JSConsoleError, message
        end
      rescue Selenium::WebDriver::Error::WebDriverError => error
        if error.message =~ /unknown command: session\/[0-9a-zA-Z]+(?:\/se)?\/log/
          message = "Unable to access Chrome javascript console logs. You may be using an outdated version of ChromeDriver."
          raise JSConsoleError, message
        else
202
          raise error
203
        end
204 205 206
      end
    end

207 208 209 210
    # prevent localStorage from introducing side effects based on test order
    unless ['', 'about:blank', 'data:,'].include? Capybara.current_session.driver.browser.current_url
      execute_script("localStorage.clear();")
    end
211

212 213
    # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
    # but `block_and_wait_for_requests_complete` is called before it so by
214
    # calling it explicitly here, we prevent any new requests from being fired
215
    # See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25
216 217
    # We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
    Capybara.reset_sessions! unless example.exception
218 219
    block_and_wait_for_requests_complete
  end
220
end