diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d4b159..2d6c33ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ undid `--blink-settings` overrides. `mobile:` defaults to `nil` and leaves touch as is, `false` turns it off [#664] - A click or navigation timed out when the last loading iframe moved to another process, e.g. submitting a form that opens an inline PDF in Chrome 145+, where the PDF viewer runs out of process [#672] +- A click or navigation didn't wait for an out-of-process iframe to load, e.g. a cross-site iframe with site isolation + on. Ferrum now follows its loading in the iframe's own target [#685] ### Removed diff --git a/lib/ferrum/contexts.rb b/lib/ferrum/contexts.rb index c9a68d07..b1215efa 100644 --- a/lib/ferrum/contexts.rb +++ b/lib/ferrum/contexts.rb @@ -230,17 +230,18 @@ def handle_attach(target, session_id, params, parent) elsif target.worker? || target.shared_worker? connect_worker(target) else - add_extensions_and_resume(session_id, params["waitingForDebugger"]) + add_extensions_and_resume(target, session_id, params["waitingForDebugger"]) end end # A page or iframe is paused until we resume it, so the extensions we # register now run before any script of its first document. An - # out-of-process iframe ignores them until the Page domain is enabled. - def add_extensions_and_resume(session_id, waiting) + # out-of-process iframe ignores them until the Page domain is enabled, + # and its parent page needs the Page domain to see it finish loading. + def add_extensions_and_resume(target, session_id, waiting) extensions = @client.options.extensions session = @client.session(session_id) - session.command("Page.enable", async: true) if extensions.any? + session.command("Page.enable", async: true) if extensions.any? || target.iframe? extensions.each do |source| session.command("Page.addScriptToEvaluateOnNewDocument", async: true, source: source) end diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 3597d5b0..35704faa 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -107,6 +107,7 @@ def initialize(client, context_id:, target_id:, proxy: nil) @options = client.options @frames = Concurrent::Map.new + @remote_frames = Concurrent::Map.new @main_frame = Frame.new(nil, self) @event = Utils::Event.new.tap(&:set) @document_version = 0 diff --git a/lib/ferrum/page/frames.rb b/lib/ferrum/page/frames.rb index 447a61df..1a864db2 100644 --- a/lib/ferrum/page/frames.rb +++ b/lib/ferrum/page/frames.rb @@ -74,6 +74,7 @@ def frame_by(id: nil, name: nil, execution_id: nil) def frames_subscribe subscribe_frame_attached subscribe_frame_detached + subscribe_remote_frames subscribe_frame_started_navigating subscribe_frame_started_loading subscribe_frame_navigated @@ -104,11 +105,50 @@ def subscribe_frame_detached frame.execution_id = nil else @frames.delete(params["frameId"]) - @event.set if @main_frame.idle? && frame&.idle? == false && frames.size == 1 + forget_remote_frame(params["frameId"]) unless params["reason"] == "swap" + @event.set if @main_frame.idle? && frame&.idle? == false && frames.size == 1 && @remote_frames.values.none? end end end + # A frame swapped into another process (`Page.frameDetached` with reason `swap`) leaves the page's frame tree + # and loads in its own `iframe` target, so its loading events come over that target's session. Until it stops + # loading there {#idling?} stays false, as it does for any other loading frame. + def subscribe_remote_frames + on("Target.attachedToTarget") do |params| + info, session_id = params.values_at("targetInfo", "sessionId") + next unless info["type"] == "iframe" + + frame_id = info["targetId"] + @remote_frames.put_if_absent(frame_id, true) + subscribe_remote_frame_loading(client.session(session_id), frame_id) + end + + on("Target.detachedFromTarget") do |params| + forget_remote_frame(params["targetId"]) + end + end + + def subscribe_remote_frame_loading(session, frame_id) + session.on("Page.frameStartedLoading") do |params| + next unless params["frameId"] == frame_id && @remote_frames.key?(frame_id) + + @remote_frames[frame_id] = true + @event.reset + end + + session.on("Page.frameStoppedLoading") do |params| + next unless params["frameId"] == frame_id && @remote_frames.key?(frame_id) + + @remote_frames[frame_id] = false + @event.set if idling? + end + end + + def forget_remote_frame(frame_id) + @event.set if @remote_frames.delete(frame_id) && idling? + end + # Tracks the +loaderId+ for each navigating frame and clears stale # lifecycle events from the previous navigation. def subscribe_frame_started_navigating @@ -257,11 +297,12 @@ def subscribe_execution_contexts_cleared end children.each { |id| @frames.delete(id) } + @remote_frames.clear end end def idling? - @frames.values.all?(&:idle?) + @frames.values.all?(&:idle?) && @remote_frames.values.none? end end end diff --git a/sig/ferrum/contexts.rbs b/sig/ferrum/contexts.rbs index ba9fbcaf..0352d0e9 100644 --- a/sig/ferrum/contexts.rbs +++ b/sig/ferrum/contexts.rbs @@ -55,7 +55,7 @@ module Ferrum def connect_worker: (Target target) -> void - def add_extensions_and_resume: (String session_id, bool? waiting) -> void + def add_extensions_and_resume: (Target target, String session_id, bool? waiting) -> void def resume: (String session_id) -> void diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index b73620fe..fca351f8 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -32,6 +32,7 @@ module Ferrum @target_id: String @options: Browser::Options @frames: ::Concurrent::Map[String, Frame] + @remote_frames: ::Concurrent::Map[String, bool] @main_frame: Frame @event: Utils::Event @mouse: Mouse diff --git a/sig/ferrum/page/frames.rbs b/sig/ferrum/page/frames.rbs index fd4d23ae..c05a01df 100644 --- a/sig/ferrum/page/frames.rbs +++ b/sig/ferrum/page/frames.rbs @@ -15,6 +15,12 @@ module Ferrum def subscribe_frame_detached: () -> untyped + def subscribe_remote_frames: () -> untyped + + def subscribe_remote_frame_loading: (SessionClient session, String frame_id) -> untyped + + def forget_remote_frame: (String? frame_id) -> untyped + def subscribe_frame_started_navigating: () -> untyped def subscribe_frame_started_loading: () -> untyped diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb index c3127df8..4ec27234 100644 --- a/spec/frame_spec.rb +++ b/spec/frame_spec.rb @@ -148,6 +148,17 @@ expect(page.evaluate("location.pathname")).to eq("/slow") end + it "waits for a cross-site frame a click adds", :launch do + browser = Ferrum::Browser.new(base_url: base_url, + browser_options: { "site-per-process" => nil, "disable-features" => "Translate" }) + browser.go_to("/cross_site_iframe") + browser.at_css("button").click + + expect(browser.evaluate("window.frameLoaded")).to be(true) + ensure + browser&.quit + end + context "with lazy loading iframes" do it "does not block visit on lazy iframe" do expect { page.go_to("/lazy_iframe") }.not_to raise_error diff --git a/spec/support/views/cross_site_iframe.erb b/spec/support/views/cross_site_iframe.erb new file mode 100644 index 00000000..a3dfd394 --- /dev/null +++ b/spec/support/views/cross_site_iframe.erb @@ -0,0 +1,17 @@ + + +
+ + + + + diff --git a/spec/support/views/slow_image.erb b/spec/support/views/slow_image.erb new file mode 100644 index 00000000..801effc2 --- /dev/null +++ b/spec/support/views/slow_image.erb @@ -0,0 +1,6 @@ + + + +