Skip to content

Commit

Permalink
Rework network interception API to allow mutating request/response
Browse files Browse the repository at this point in the history
This makes it more aligned to Java/.NET and simplifies interface
allowing to mutate requests and response in-place. Selenium will then
ensure that untouched requests/responses are continued as-is and mutated
ones are provided as stubs.
  • Loading branch information
p0deje committed Sep 12, 2021
1 parent 319fd1a commit eef1f40
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,49 +28,44 @@ module HasNetworkInterception
# a stubbed response instead.
#
# @example Log requests and pass through
# driver.intercept do |request|
# driver.intercept do |request, &continue|
# puts "#{request.method} #{request.url}"
# request.continue
# continue.call(request)
# end
#
# @example Stub requests for images
# driver.intercept do |request|
# driver.intercept do |request, &continue|
# if request.url.match?(/\.png$/)
# request.respond(body: File.read('myfile.png'))
# else
# request.continue
# request.url = 'https://upload.wikimedia.org/wikipedia/commons/d/d5/Selenium_Logo.png'
# end
# continue.call(request)
# end
#
# @example Log responses and pass through
# driver.intercept do |request|
# request.continue do |response|
# driver.intercept do |request, &continue|
# continue.call(request) do |response|
# puts "#{response.code} #{response.body}"
# response.continue
# end
# end
#
# @example Mutate specific response
# driver.intercept do |request|
# request.continue do |response|
# if request.url.include?('/myurl')
# request.respond(body: "#{response.body}, Added by Selenium!")
# else
# response.continue
# end
# driver.intercept do |request, &continue|
# continue.call(request) do |response|
# response.body << 'Added by Selenium!' if request.url.include?('/myurl')
# end
# end
#
# @param [#call] block which is called when request is interecepted
# @yieldparam [DevTools::Request]
# @param [Proc] block which is called when request is intercepted
# @yieldparam [DevTools::Request] request
# @yieldparam [Proc] continue block which proceeds with the request and optionally yields response
#

def intercept(&block)
devtools.network.set_cache_disabled(cache_disabled: true)
devtools.fetch.on(:request_paused) do |params|
id = params['requestId']
if params.key?('responseStatusCode') || params.key?('responseErrorReason')
intercept_response(id, params, &intercepted_requests[id].on_response)
intercept_response(id, params, &pending_response_requests.delete(id))
else
intercept_request(id, params, &block)
end
Expand All @@ -80,33 +75,53 @@ def intercept(&block)

private

def intercepted_requests
@intercepted_requests ||= {}
def pending_response_requests
@pending_response_requests ||= {}
end

def intercept_request(id, params)
request = DevTools::Request.new(
devtools: devtools,
id: id,
url: params.dig('request', 'url'),
method: params.dig('request', 'method'),
headers: params.dig('request', 'headers')
)
intercepted_requests[id] = request
def intercept_request(id, params, &block)
original = DevTools::Request.from(id, params)
mutable = DevTools::Request.from(id, params)

yield request
block.call(mutable) do |&continue| # rubocop:disable Performance/RedundantBlockCall
pending_response_requests[id] = continue

if original == mutable
devtools.fetch.continue_request(request_id: id)
else
devtools.fetch.continue_request(
request_id: id,
url: mutable.url,
method: mutable.method,
post_data: mutable.post_data,
headers: mutable.headers.map do |k, v|
{name: k, value: v}
end
)
end
end
end

def intercept_response(id, params)
response = DevTools::Response.new(
devtools: devtools,
id: id,
code: params['responseStatusCode'],
headers: params['responseHeaders']
)
intercepted_requests.delete(id)
return devtools.fetch.continue_request(request_id: id) unless block_given?

body = devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
original = DevTools::Response.from(id, body, params)
mutable = DevTools::Response.from(id, body, params)
yield mutable

yield response
if original == mutable
devtools.fetch.continue_request(request_id: id)
else
devtools.fetch.fulfill_request(
request_id: id,
body: Base64.strict_encode64(mutable.body),
response_code: mutable.code,
response_headers: mutable.headers.map do |k, v|
{name: k, value: v}
end
)
end
end
end # HasNetworkInterception
end # DriverExtensions
Expand Down
61 changes: 25 additions & 36 deletions rb/lib/selenium/webdriver/devtools/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,54 +22,43 @@ module WebDriver
class DevTools
class Request

attr_reader :url, :method, :headers
attr_accessor :url, :method, :headers, :post_data
attr_reader :id

#
# Creates request from DevTools message.
# @api private
attr_reader :on_response
#

def self.from(id, params)
new(
id: id,
url: params.dig('request', 'url'),
method: params.dig('request', 'method'),
headers: params.dig('request', 'headers'),
post_data: params.dig('request', 'postData')
)
end

def initialize(devtools:, id:, url:, method:, headers:)
@devtools = devtools
def initialize(id:, url:, method:, headers:, post_data:)
@id = id
@url = url
@method = method
@headers = headers
@on_response = Proc.new(&:continue)
end

#
# Continues the request, optionally yielding
# the response before it reaches the browser.
#
# @param [#call] block which is called when response is intercepted
# @yieldparam [DevTools::Response]
#

def continue(&block)
@on_response = block if block_given?
@devtools.fetch.continue_request(request_id: @id)
@post_data = post_data
end

#
# Fulfills the request providing the stubbed response.
#
# @param [Integer] code
# @param [Hash] headers
# @param [String] body
#

def respond(code: 200, headers: {}, body: '')
@devtools.fetch.fulfill_request(
request_id: @id,
body: Base64.strict_encode64(body),
response_code: code,
response_headers: headers.map do |k, v|
{name: k, value: v}
end
)
def ==(other)
self.class == other.class &&
id == other.id &&
url == other.url &&
method == other.method &&
headers == other.headers &&
post_data == other.post_data
end

def inspect
%(#<#{self.class.name} @method="#{method}" @url="#{url}")
%(#<#{self.class.name} @id="#{id}" @method="#{method}" @url="#{url}")
end

end # Request
Expand Down
49 changes: 26 additions & 23 deletions rb/lib/selenium/webdriver/devtools/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,42 @@ module WebDriver
class DevTools
class Response

attr_reader :code, :headers

def initialize(devtools:, id:, code:, headers:)
@devtools = devtools
@id = id
@code = code
@headers = headers
end
attr_accessor :code, :body, :headers
attr_reader :id

#
# Returns the response body.
# @return [String]
# Creates response from DevTools message.
# @api private
#

def body
@body ||= begin
result = @devtools.fetch.get_response_body(request_id: @id)
encoded_body = result.dig('result', 'body')

Base64.strict_decode64(encoded_body)
end
def self.from(id, encoded_body, params)
new(
id: id,
code: params['responseStatusCode'],
body: Base64.strict_decode64(encoded_body),
headers: params['responseHeaders'].each_with_object({}) do |header, hash|
hash[header['name']] = header['value']
end
)
end

#
# Continues the response unmodified.
#
def initialize(id:, code:, body:, headers:)
@id = id
@code = code
@body = body
@headers = headers
end

def continue
@devtools.fetch.continue_request(request_id: @id)
def ==(other)
self.class == other.class &&
id == other.id &&
code == other.code &&
body == other.body &&
headers == other.headers
end

def inspect
%(#<#{self.class.name} @code="#{code}")
%(#<#{self.class.name} @id="#{id}" @code="#{code}")
end

end # Response
Expand Down
71 changes: 19 additions & 52 deletions rb/spec/integration/selenium/webdriver/devtools_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,79 +149,46 @@ module WebDriver

context 'network interception', except: {browser: :firefox_nightly,
reason: 'Fetch.enable is not yet supported'} do
it 'allows to continue requests' do
it 'continues requests' do
requests = []
driver.intercept do |request|
driver.intercept do |request, &continue|
requests << request
request.continue
continue.call(request)
end
driver.navigate.to url_for('html5Page.html')
expect(driver.title).to eq('HTML5')
expect(requests).not_to be_empty
end

it 'allows to stub requests' do
driver.intercept do |request|
request.respond(body: '<title>Intercepted!</title>')
end
driver.navigate.to url_for('html5Page.html')
expect(driver.title).to eq('Intercepted!')
end

it 'intercepts specific requests' do
stubbed = []
continued = []
driver.intercept do |request|
if request.method == 'GET' && request.url.include?('resultPage.html')
stubbed << request
request.respond(body: '<title>Intercepted!</title>')
else
continued << request
request.continue
it 'changes requests' do
driver.intercept do |request, &continue|
uri = URI(request.url)
if uri.path.match?(%r{/html5/.*\.jpg})
uri.path = '/beach.jpg'
request.url = uri.to_s
end
continue.call(request)
end

driver.navigate.to url_for('formPage.html')
expect(driver.title).to eq('We Leave From Here')
expect(stubbed).to be_empty
expect(continued).not_to be_empty

driver.find_element(id: 'submitButton').click
expect(driver.title).to eq('Intercepted!')
expect(stubbed).not_to be_empty
driver.navigate.to url_for('html5Page.html')
expect(driver.find_elements(tag_name: 'img').map(&:size).uniq).to eq([Dimension.new(640, 480)])
end

it 'allows to continue responses' do
it 'continues responses' do
responses = []
driver.intercept do |request|
request.continue do |response|
driver.intercept do |request, &continue|
continue.call(request) do |response|
responses << response
response.continue
end
end
driver.navigate.to url_for('html5Page.html')
expect(driver.title).to eq('HTML5')
expect(responses).not_to be_empty
end

it 'allows to stub responses' do
driver.intercept do |request|
request.continue do |response|
request.respond(body: "#{response.body}, '<h4 id=\"appended\">Appended!</h4>'")
end
end
driver.navigate.to url_for('html5Page.html')
expect(driver.find_elements(id: "appended")).not_to be_empty
end

it 'intercepts specific responses' do
driver.intercept do |request|
request.continue do |response|
if request.url.include?('html5Page.html')
request.respond(body: "#{response.body}, '<h4 id=\"appended\">Appended!</h4>'")
else
response.continue
end
it 'changes responses' do
driver.intercept do |request, &continue|
continue.call(request) do |response|
response.body << '<h4 id="appended">Appended!</h4>' if request.url.include?('html5Page.html')
end
end
driver.navigate.to url_for('html5Page.html')
Expand Down

0 comments on commit eef1f40

Please sign in to comment.