teamcapybara/capybara · error · Capybara::ScopeError

`switch_to_frame(:top)` cannot be called from inside a desce

Error message

`switch_to_frame(:top)` cannot be called from inside a descendant frame's `within` block.

What it means

switch_to_frame(:top) resets to the top-level document, but only when every scope from the outermost frame onward is a frame or nil. If a within scope sits inside the frame chain, the slice check fails and Capybara::ScopeError is raised rather than silently discarding that element scope.

Source

Thrown at lib/capybara/session.rb:425

    #
    def switch_to_frame(frame)
      case frame
      when Capybara::Node::Element
        driver.switch_to_frame(frame)
        scopes.push(:frame)
      when :parent
        if scopes.last != :frame
          raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's " \
                                      '`within` block.'
        end
        scopes.pop
        driver.switch_to_frame(:parent)
      when :top
        idx = scopes.index(:frame)
        top_level_scopes = [:frame, nil]
        if idx
          if scopes.slice(idx..).any? { |scope| !top_level_scopes.include?(scope) }
            raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's " \
                                        '`within` block.'
          end
          scopes.slice!(idx..)
          driver.switch_to_frame(:top)
        end
      else
        raise ArgumentError, 'You must provide a frame element, :parent, or :top when calling switch_to_frame'
      end
    end

    ##
    #
    # Execute the given block within the given iframe using given frame, frame name/id or index.
    # May not be supported by all drivers.
    #
    # @overload within_frame(element)
    #   @param [Capybara::Node::Element]  frame element
    # @overload within_frame([kind = :frame], locator, **options)

View on GitHub (pinned to 15b5fdb76e)

Solutions

  1. Exit the within block (and any nested scoping) before calling switch_to_frame(:top)
  2. Let within_frame blocks expire naturally - each block exit already returns one level toward the top
  3. Restructure page objects so top-level resets happen at the top level, not inside scoped helpers

Example fix

# before
within_frame(:css, 'iframe.editor') do
  within('.canvas') do
    switch_to_frame(:top) # Capybara::ScopeError
  end
end

# after
within_frame(:css, 'iframe.editor') do
  within('.canvas') { click_button 'Done' }
end # back at the top-level document automatically
Defensive patterns

Strategy: validation

Validate before calling

scopes = page.send(:scopes)
idx = scopes.index(:frame)
at_frame_top_only = idx.nil? || scopes.slice(idx..).all? { |s| s.nil? || s == :frame }

switch_to_frame(:top) if at_frame_top_only

Try / catch

begin
  switch_to_frame(:top)
rescue Capybara::ScopeError => e
  raise "a `within` block is open inside the frame - close it first (#{e.message})"
end

Prevention

When it happens

Trigger: within_frame(el) { within('.modal') { switch_to_frame(:top) } } - after the :frame scope a within scope remains, so scopes.slice(idx..).any? { |s| ![:frame, nil].include?(s) } trips; likewise calling switch_to_frame(:top) from helpers already running inside a within block.

Common situations: Deeply nested frame + within page objects; attempts to 'escape everything' from inside scoped helpers; refactors that moved switch_to_frame calls into methods already inside within.

Related errors


AI-assisted analysis of teamcapybara/capybara@15b5fdb76e (2026-08-21). Data as JSON: /api/errors/cc2345722290224d. Report an issue: GitHub.