lib/capybara/cuprite/browser/frame.rb



module Capybara::Cuprite
  class Browser
    module Frame
      def execution_context_id
        @mutex.synchronize do
          if !@frame_stack.empty?
            @frames[@frame_stack.last]["execution_context_id"]
          else
            @execution_context_id
          end
        end
      end

      def frame_url
        evaluate("window.location.href")
      end

      def frame_title
        evaluate("document.title")
      end

      def switch_to_frame(handle)
        case handle
        when Capybara::Node::Base
          @frame_stack << handle.native.node["frameId"]
          inject_extensions
        when :parent
          @frame_stack.pop
        when :top
          @frame_stack = []
        end
      end

      private

      def subscribe_events
        @client.subscribe("Page.frameAttached") do |params|
          @frames[params["frameId"]] = { "parent_id" => params["parentFrameId"] }
        end

        @client.subscribe("Page.frameStartedLoading") do |params|
          @waiting_frames << params["frameId"]
          @mutex.try_lock
        end

        @client.subscribe("Page.frameNavigated") do |params|
          id = params["frame"]["id"]
          if frame = @frames[id]
            frame.merge!(params["frame"].slice("name", "url"))
          end
        end

        @client.subscribe("Page.frameScheduledNavigation") do |params|
          # Trying to lock mutex if frame is the main frame
          @waiting_frames << params["frameId"]
          @mutex.try_lock
        end

        @client.subscribe("Page.frameStoppedLoading") do |params|
          # `DOM.performSearch` doesn't work without getting #document node first.
          # It returns node with nodeId 1 and nodeType 9 from which descend the
          # tree and we save it in a variable because if we call that again root
          # node will change the id and all subsequent nodes have to change id too.
          # `command` is not allowed in the block as it will deadlock the process.
          if params["frameId"] == @frame_id
            signal if @waiting_frames.empty?
            @client.command("DOM.getDocument", depth: 0)
          end

          if @waiting_frames.include?(params["frameId"])
            @waiting_frames.delete(params["frameId"])
            signal if @waiting_frames.empty?
          end
        end

        @client.subscribe("Runtime.executionContextCreated") do |params|
          frame_id = params.dig("context", "auxData", "frameId")
          execution_context_id = params.dig("context", "id")

          # Remember the very first frame since it's the main one
          @frame_id ||= frame_id
          @execution_context_id ||= execution_context_id

          if @frames[frame_id]
            @frames[frame_id].merge!("execution_context_id" => execution_context_id)
          else
            @frames[frame_id] = { "execution_context_id" => execution_context_id }
          end
        end

        @client.subscribe("Runtime.executionContextDestroyed") do |params|
          execution_context_id = params["executionContextId"]
          id, frame = @frames.find { |_, p| p["execution_context_id"] == execution_context_id }
          frame["execution_context_id"] = nil if frame

          if @execution_context_id == execution_context_id
            @execution_context_id = nil
          end
        end

        @client.subscribe("Runtime.executionContextsCleared") do
          # If we didn't have time to set context id at the beginning we have
          # to set lock and release it when we set something.
          @execution_context_id = nil
        end
      end
    end
  end
end