Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: gracefully handle unloaded iframes #315

Merged
merged 7 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion packages/axe-core-api/e2e/selenium/spec/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,16 @@ def recursive_compact(thing)
end
end


def get_check_by_id(check_list, id)
return check_list.find { |check| check.id == id }
end

describe "Crashes" do
it "throws if axe errors out on the top window" do
$driver.get fixture "/crash.html"
with_js($axe_post_43x + $crasher_js) {
expect { run_axe }.to raise_error /Boom!/
expect { run_axe }.to raise_error /Boom!/
}
end

Expand All @@ -95,6 +100,26 @@ def recursive_compact(thing)
end

describe "frame tests" do
it "works on pages with unloaded frames" do
$driver.get fixture "/lazy-loaded-iframe.html"
title = $driver.title
res = run_axe
expect(title).not_to eq "Error"
frame_tested = get_check_by_id res.results.incomplete, :'frame-tested'
expect(frame_tested.nodes.length).to be 1
expect(frame_tested.nodes[0].target).to contain_exactly(
"#ifr-lazy",
"#lazy-iframe"
)

label = get_check_by_id res.results.violations, :label
expect(label.nodes.length).to be 1
expect(label.nodes[0].target).to contain_exactly(
"#ifr-lazy",
"#lazy-baz",
"input"
)
end
it "injects into nested iframes", :fo => true do
$driver.get fixture "/nested-iframes.html"
res = run_axe
Expand Down Expand Up @@ -253,6 +278,15 @@ def recursive_compact(thing)

expect(res.results.url).to eq(fixture "/index.html")
end

it "keeps selenium page-load to user-set" do
my_page_load = 3.0
$driver.manage.timeouts.page_load = my_page_load
$driver.get fixture "/index.html"
res = run_axe

expect($driver.manage.timeouts.page_load).to be my_page_load
end
end

describe "axe.finishRun" do
Expand Down
72 changes: 43 additions & 29 deletions packages/axe-core-api/lib/axe/api/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,34 @@ def call(page)
end

def analyze_post_43x(page, lib)
@original_window = window_handle page
partial_results = run_partial_recursive(page, @context, lib, true)
throw partial_results if partial_results.respond_to?("key?") and partial_results.key?("errorMessage")
results = within_about_blank_context(page) { |page|
partial_res_str = partial_results.to_json
size_limit = 10_000_000
while not partial_res_str.empty? do
chunk_size = size_limit
chunk_size = partial_res_str.length if chunk_size > partial_res_str.length
chunk = partial_res_str[0..chunk_size-1]
partial_res_str = partial_res_str[chunk_size..-1]
store_chunk page, chunk
end

Common::Loader.new(page, lib).load_top_level Axe::Configuration.instance.jslib
begin
axe_finish_run page
rescue
raise StandardError.new "axe.finishRun failed. Please check out https://github.com/dequelabs/axe-core-gems/blob/develop/error-handling.md"
end
}
user_page_load = (get_selenium page).manage.timeouts.page_load
(get_selenium page).manage.timeouts.page_load = 1
straker marked this conversation as resolved.
Show resolved Hide resolved
begin
@original_window = window_handle page
partial_results = run_partial_recursive(page, @context, lib, true)
throw partial_results if partial_results.respond_to?("key?") and partial_results.key?("errorMessage")
results = within_about_blank_context(page) { |page|
partial_res_str = partial_results.to_json
size_limit = 10_000_000
while not partial_res_str.empty? do
chunk_size = size_limit
chunk_size = partial_res_str.length if chunk_size > partial_res_str.length
chunk = partial_res_str[0..chunk_size-1]
partial_res_str = partial_res_str[chunk_size..-1]
store_chunk page, chunk
end

Common::Loader.new(page, lib).load_top_level Axe::Configuration.instance.jslib
begin
axe_finish_run page
rescue
raise StandardError.new "axe.finishRun failed. Please check out https://github.com/dequelabs/axe-core-gems/blob/develop/error-handling.md"
end

}
ensure
(get_selenium page).manage.timeouts.page_load = user_page_load
end
Audit.new to_js, Results.new(results)
end

Expand Down Expand Up @@ -108,15 +115,15 @@ def window_handle(page)
page.current_window_handle
end

def run_partial_recursive(page, context, lib, top_level = false)
def run_partial_recursive(page, context, lib, top_level = false, frame_stack = [])
begin
current_window_handle = window_handle page
if not top_level
begin
Common::Loader.new(page, lib).load_top_level Axe::Configuration.instance.jslib
rescue
return [nil]
end

end

frame_contexts = get_frame_context_script page
Expand All @@ -134,12 +141,19 @@ def run_partial_recursive(page, context, lib, top_level = false)
end

for frame_context in frame_contexts
frame_selector = frame_context["frameSelector"]
frame_context = frame_context["frameContext"]
frame = axe_shadow_select page, frame_selector
switch_to_frame_by_handle page, frame
res = run_partial_recursive page, frame_context, lib
results += res
begin
frame_selector = frame_context["frameSelector"]
frame_context = frame_context["frameContext"]
frame = axe_shadow_select page, frame_selector
switch_to_frame_by_handle page, frame
res = run_partial_recursive page, frame_context, lib, false, [*frame_stack, frame]
results += res
rescue Selenium::WebDriver::Error::TimeoutError
page = get_selenium page
page.switch_to.window current_window_handle
frame_stack.each {|frame| page.switch_to.frame frame }
results.push nil
end
end

ensure
Expand Down
15 changes: 15 additions & 0 deletions packages/axe-core-api/lib/axe/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative "../webdriver_script_adapter/query_selector_adapter"
require_relative "../loader"
require_relative "./configuration"
require 'timeout'

module Axe
class Core
Expand Down Expand Up @@ -37,8 +38,22 @@ def use_run_partial
Core.has_run_partial?(@page) and not Axe::Configuration.instance.legacy_mode
end

def assert_frame_ready
begin
ready = Timeout.timeout(10) {
@page.evaluate_script <<-JS
document.readyState === 'complete'
JS
}
rescue Timeout::Error
ready = false
end
raise Exception.new "Page/frame not ready" if not ready
end

def load_axe_core(source)
return if already_loaded?
assert_frame_ready
loader = Common::Loader.new(@page, self)
loader.load_top_level source
return if use_run_partial
Expand Down
1 change: 1 addition & 0 deletions packages/axe-core-api/spec/axe/core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Axe
let(:page) {
spy("page", evaluate_script: false)
}
before { allow(page).to receive(:evaluate_script).and_return(false, true, false) }

describe "initialize" do
# We have removed comments from `axe.min.js`, so excluding this test
Expand Down