-
-
Save arielj/4f52b6fcdb8035babcd3f6dac8c2dc77 to your computer and use it in GitHub Desktop.
# RSpec: | |
# | |
# Put this file in spec/support/interceptor.rb | |
# | |
# Require the file in your rails_helper.rb: | |
# | |
# require_relative "support/interceptor" | |
# | |
# Include the module and add the callbacks in your RSpec config in rails_helper.rb | |
# | |
# RSpec.configure do |config| | |
# config.include(Interceptor, type: :system) | |
# config.before(:each, type: :system) do | |
# driven_by Capybara.javascript_driver # this is required because of https://github.com/rspec/rspec-rails/issues/2550 | |
# start_intercepting | |
# end | |
# | |
# config.after(:each, type: :system) do | |
# stop_intercepting | |
# end | |
# | |
# | |
# MiniTest: | |
# | |
# Put this file in test/support/interceptor.rb | |
# | |
# Require the file in your test_helper.rb: | |
# | |
# require_relative "support/interceptor" | |
# | |
# Include the module and add the callbacks in your test/application_system_test_case.rb file | |
# | |
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase | |
# driven_by :selenium, using: :chrome, screen_size: [1400, 1400] | |
# include Interceptor | |
# | |
# def after_setup | |
# start_intercepting | |
# super | |
# end | |
# | |
# def before_teardown | |
# stop_intercepting | |
# super | |
# end | |
# end | |
# | |
# How to use: | |
# | |
# Call the `intercept` method in any system spec with a url, response and an http method (optional, defaults to "GET") | |
# | |
# test "something" do | |
# intercept("some_url.com", "fixed response") | |
# visit root_url | |
# | |
# # assert something that depends on the intercepted request | |
# end | |
# | |
# | |
# - You can configure default interceptions that should apply to all tests by overriding the `default_interceptions` method | |
# - You can configure the allowed requests by overriding the `allowed_requests` method (defaults to any request to the Rails app) | |
module Interceptor | |
# Add an interception hash for a given url, http method, and response | |
# @url can be a regexp or a string | |
# @method can be a string or a symbol, an can be uppercase or lowercase | |
def intercept(url, response = "", method = :any) | |
@interceptions << {url: url, method: method, response: response} | |
end | |
def start_intercepting | |
# ignore if the driver is RackTest | |
return unless page.driver.browser.respond_to?(:intercept) | |
# only attach the intercept callback once to the browser | |
@interceptions = default_interceptions | |
return if @intercepting | |
page.driver.browser.intercept do |request, &continue| | |
url = request.url | |
method = request.method | |
if (interception = response_for(url, method)) | |
# set mocked body if there's an interception for the url and method | |
continue.call(request) do |response| | |
response.body = interception[:response] | |
end | |
elsif allowed_request?(url, method) | |
# leave request untouched if allowed | |
continue.call(request) | |
else | |
# intercept any external request with an empty response and print some logs | |
continue.call(request) do |response| | |
log_request(url, method) | |
response.body = "" | |
end | |
end | |
end | |
@intercepting = true | |
end | |
def stop_intercepting | |
return unless @intercepting | |
# remove the callback, cleanup | |
clear_devtools_intercepts | |
@intercepting = false | |
# some requests may finish after the test is done if we let them go through untouched | |
sleep(0.2) | |
end | |
# Override this method to define default interceptions that should apply to all tests | |
# Each element of the array should be a hash with `url`, `response` and `method` key, like | |
# the hash added by the `intercept` method | |
# | |
# For example: | |
# - [{url: "https://external.api.com", response: ""}, {url: another_domain, response: fixed_response, method: :get}] | |
def default_interceptions | |
[] | |
end | |
# Override this method to add more allowed requests that shouldn't be intercepted | |
# | |
# Elements of this array can be: | |
# - a string | |
# - a regexp | |
# - a hash with `url` and `method` keys where: | |
# - url can be a string or a regexp | |
# - method can be `:any`, can be omitted (same as setting `:any`), or can be an | |
# http method as symbol or string and lowercase or uppercase | |
# | |
# For example, these are valid elements for the array: | |
# - "https://allowed.domain.com" | |
# - {url: "https://allowed.domain.com", method: "GET"} (or {url: /allowed\.domain\.com/, method: :get}) | |
# - {url: /allowed\.domain\.com/, method: :any} (or {url: /allowed\.domain\.com/} or /allowed\.domain\.com/) | |
# | |
# NOTE that you probably always want at least the Capybara.server_host url in this array | |
def allowed_requests | |
[%r{http://#{Capybara.server_host}}] | |
end | |
private | |
# check if the given request url and http method pair is allowed by any rule | |
def allowed_request?(url, method = "GET") | |
allowed_requests.any? do |allowed| | |
allowed_url = allowed.is_a?(Hash) ? allowed[:url] : allowed | |
matches_url = url.match?(allowed_url) | |
allowed_method = allowed.is_a?(Hash) ? allowed[:method] : :any | |
allowed_method ||= :any | |
matches_method = allowed_method == :any || method == allowed_method.to_s.upcase | |
matches_url && matches_method | |
end | |
end | |
# find the interception hash for a given url and http method pair | |
def response_for(url, method = "GET") | |
@interceptions.find do |interception| | |
matches_url = url.match?(interception[:url]) | |
intercepted_method = interception[:method] || :any | |
matches_method = intercepted_method == :any || method == intercepted_method.to_s.upcase | |
matches_url && matches_method | |
end | |
end | |
# clears the devtools callback for the interceptions | |
def clear_devtools_intercepts | |
callbacks = page.driver.browser.devtools.callbacks | |
if callbacks.has_key?("Fetch.requestPaused") | |
callbacks.delete("Fetch.requestPaused") | |
end | |
end | |
def log_request(url, method) | |
message = "External JavaScript request not intercepted: #{method} #{url}" | |
puts message | |
Rails.logger.warn message | |
end | |
end |
hey @rogerkk, that not
is correct there, it's saying that the request was not explicitly intercepted by the developer (it was intercepted by the code as a fallback, but it's just responding with an empty string so result of that request can be unexpected), it's a log for the person running the tests to see they have to explicitly add an interception for that url
Maybe it would be better to phrase it like External request not intercepted: .... Intercepting with empty response.
for clarity
when an interception is defined by the developer, that message is not displayed
Aha, then I get the intention. Thanks!
Your rephrasing of the log message makes it clearer! Perhaps it could be even clearer with something like External request not intercepted by explicit rule: ... Intercepting with empty response
. 🤷♂️
Thanks @arielj for this.
I noticed this made our tests slower, so my idea was enable this just for specific tests. I had to make some tweaks:
clear_devtools_intercepts
method. Basically reverting whatpage.driver.browser.intercept
method does.
def clear_devtools_intercepts
callbacks = page.driver.browser.devtools.callbacks
if callbacks.has_key?("Fetch.requestPaused")
callbacks.delete("Fetch.requestPaused")
end
if callbacks.has_key?("Network.loadingFailed")
callbacks.delete("Network.loadingFailed")
end
page.driver.browser.devtools.network.set_cache_disabled(cache_disabled: false)
page.driver.browser.devtools.network.disable
page.driver.browser.devtools.fetch.disable
end
- Because errors on Github Actions I added response code 200 and CORS headers on interception code:
...
if (interception = response_for(url, method))
# set mocked body if there's an interception for the url and method
continue.call(request) do |response|
response.code = 200
response.headers["Access-Control-Allow-Origin"] = "*"
response.body = interception[:response]
end
elsif ...
- Added metadata filtering to specs
# on your spec config file
config.before(:each, intercept_js_requests: true, type: :system) do
start_intercepting
end
config.after(:each, intercept_js_requests: true, type: :system) do
stop_intercepting
end
# your test
it "foo", intercept_js_requests: true do
intercept(...)
end
Full version here: https://gist.github.com/stephannv/b82ea7efb85b79fec37d3ea58abdced7
Thanks for the script @arielj. I was wondering if anyone's had any issues with this since chrome v120. We've been getting the following error since the chrome upgrade
Failure/Error: @interceptions << { url: url, method: method, response: response }
NoMethodError:
undefined method `<<' for nil:NilClass
@interceptions << { url: url, method: method, response: response }
^^
# ./spec/support/interceptor.rb:68:in `intercept'
# ./spec/features_helper.rb:28:in `block (2 levels) in <top (required)>'
It appears that the guard clause on line 73 is triggered, meaning that the driver is not responding to intercept
. Downgrading to v119 works, but it's not really a solution. I haven't succeeded in finding out what has changed here, e.g. a new way of calling intercept
, but have anyone else seen this problem and found a solution?
Thanks for the useful script!
I wonder if the "not" on line 179 might be a mistake? It seems like this log statement is saying the opposite of what is actually happening. 🤔