mojotech/capybara-ui

View on GitHub
lib/capybara/ui/widgets/widget.rb

Summary

Maintainability
A
2 hrs
Test Coverage
B
80%
module Capybara
  module UI
    class Widget
      extend Forwardable
      extend Widgets::DSL

      include WidgetParts::Struct
      include WidgetParts::Container
      include CucumberMethods

      class Removed < StandardError; end

      attr_reader :root

      # @!group Widget macros

      # Defines a new action.
      #
      # This is a shortcut to help defining a widget and a method that clicks
      # on that widget. You can then send a widget instance the message given
      # by +name+.
      #
      # You can access the underlying widget by appending "_widget" to the
      # action name.
      #
      # @example
      #   # Consider the widget will encapsulate the following HTML
      #   #
      #   # <div id="profile">
      #   #  <a href="/profiles/1/edit" rel="edit">Edit</a>
      #   # </div>
      #   class PirateProfile < Capybara::UI::Widget
      #     root "#profile"
      #
      #     # Declare the action
      #     action :edit, '[rel = edit]'
      #   end
      #
      #   pirate_profile = widget(:pirate_profile)
      #
      #   # Access the action widget
      #   action_widget = pirate_profile.widget(:edit_widget)
      #   action_widget = pirate_profile.edit_widget
      #
      #   # Click the link
      #   pirate_profile.edit
      #
      # @param name the name of the action
      # @param selector the selector for the widget that will be clicked
      def self.action(name, selector = nil)
        block = if selector
                  wname = :"#{name}_widget"

                  widget wname, selector

                  -> { widget(wname).click; self }
                else
                  -> { click; self }
                end

        define_method name, &block
      end

      # Creates a delegator for one child widget message.
      #
      # Since widgets are accessed through {WidgetParts::Container#widget}, we
      # can't use {Forwardable} to delegate messages to widgets.
      #
      # @param name the name of the receiver child widget
      # @param widget_message the name of the message to be sent to the child widget
      # @param method_name the name of the delegator. If +nil+ the method will
      #   have the same name as the message it will send.
      def self.widget_delegator(name, widget_message, method_name = nil)
        method_name = method_name || widget_message

        class_eval <<-RUBY
          def #{method_name}(*args)
            if args.size == 1
              widget(:#{name}).#{widget_message} args.first
            else
              widget(:#{name}).#{widget_message} *args
            end
          end
        RUBY
      end

      # @!endgroup

      # Finds a single instance of the current widget in +node+.
      #
      # @param node the node we want to search in
      #
      # @return a new instance of the current widget class.
      #
      # @raise [Capybara::ElementNotFoundError] if the widget can't be found
      def self.find_in(parent, *args)
        new(filter.node(parent, *args))
      rescue Capybara::Ambiguous => e
        raise AmbiguousWidget.new(e.message).
          tap { |x| x.set_backtrace e.backtrace }
      rescue Capybara::ElementNotFound => e
        raise MissingWidget.new(e.message).
          tap { |x| x.set_backtrace e.backtrace }
      end

      def self.find_all_in(parent, *args)
        filter.nodes(parent, *args).map { |e| new(e) }
      end

      # Determines if an instance of this widget class exists in
      # +parent_node+.
      #
      # @param parent_node [Capybara::Node] the node we want to search in
      #
      # @return +true+ if a widget instance is found, +false+ otherwise.
      def self.present_in?(parent, *args)
        filter.node?(parent, *args)
      end

      def self.not_present_in?(parent, *args)
        filter.nodeless?(parent, *args)
      end

      # Sets this widget's default selector.
      #
      # You can pass more than one argument to it, or a single Array. Any valid
      # Capybara selector accepted by Capybara::Node::Finders#find will work.
      #
      # === Examples
      #
      # Most of the time, your selectors will be Strings:
      #
      #   class MyWidget < Capybara::UI::Widget
      #     root '.selector'
      #   end
      #
      # This will match any element with a class of "selector". For example:
      #
      #   <span class="selector">Pick me!</span>
      #
      # ==== Composite selectors
      #
      # If you're using CSS as the query language, it's useful to be able to use
      # +text: 'Some text'+ to zero in on a specific node:
      #
      #   class MySpecificWidget < Capybara::UI::Widget
      #     root '.selector', text: 'Pick me!'
      #   end
      #
      # This is especially useful, e.g., when you want to create a widget
      # to match a specific error or notification:
      #
      #   class NoFreeSpace < Capybara::UI::Widget
      #     root '.error', text: 'No free space left!'
      #   end
      #
      # So, given the following HTML:
      #
      #   <body>
      #     <div class="error">No free space left!</div>
      #
      #     <!-- ... -->
      #   </body>
      #
      # You can test for the error's present using the following code:
      #
      #   document.visible?(:no_free_space) #=> true
      #
      # Note: When you want to match text, consider using +I18n.t+ instead of
      # hard-coding the text, so that your tests don't break when the text changes.
      #
      # Finally, you may want to override the query language:
      #
      #   class MyWidgetUsesXPath < Capybara::UI::Widget
      #     root :xpath, '//some/node'
      #   end
      def self.root(*selector, &block)
        @filter = NodeFilter.new(block || selector)
      end

      class MissingSelector < StandardError
      end

      def self.filter
        @filter || superclass.filter
      rescue NoMethodError
        raise MissingSelector, 'no selector defined'
      end

      def self.filter?
        filter rescue false
      end

      def self.selector
        filter.selector
      end

      def initialize(root)
        @root = root
      end

      # Clicks the current widget, or the child widget given by +name+.
      #
      # === Usage
      #
      # Given the following widget definition:
      #
      #   class Container < Capybara::UI::Widget
      #     root '#container'
      #
      #     widget :link, 'a'
      #   end
      #
      # Send +click+ with no arguments to trigger a +click+ event on +#container+.
      #
      #   widget(:container).click
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container').click
      #
      # Send +click :link+ to trigger a +click+ event on +a+:
      #
      #   widget(:container).click :link
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container a').click
      def click(*args)
        if args.empty?
          root.click
        else
          widget(*args).click
        end
      end

      # Hovers over the current widget, or the child widget given by +name+.
      #
      # === Usage
      #
      # Given the following widget definition:
      #
      #   class Container < Capybara::UI::Widget
      #     root '#container'
      #
      #     widget :link, 'a'
      #   end
      #
      # Send +hover+ with no arguments to trigger a +hover+ event on +#container+.
      #
      #   widget(:container).hover
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container').hover
      #
      # Send +hover :link+ to trigger a +hover+ event on +a+:
      #
      #   widget(:container).hover :link
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container a').hover
      def hover(*args)
        if args.empty?
          root.hover
        else
          widget(*args).hover
        end
      end

      # Double clicks the current widget, or the child widget given by +name+.
      #
      # === Usage
      #
      # Given the following widget definition:
      #
      #   class Container < Capybara::UI::Widget
      #     root '#container'
      #
      #     widget :link, 'a'
      #   end
      #
      # Send +double_click+ with no arguments to trigger an +ondblclick+ event on +#container+.
      #
      #   widget(:container).double_click
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container').double_click
      def double_click(*args)
        if args.empty?
          root.double_click
        else
          widget(*args).double_click
        end
      end

      # Right clicks the current widget, or the child widget given by +name+.
      #
      # === Usage
      #
      # Given the following widget definition:
      #
      #   class Container < Capybara::UI::Widget
      #     root '#container'
      #
      #     widget :link, 'a'
      #   end
      #
      # Send +right_click+ with no arguments to trigger an +oncontextmenu+ event on +#container+.
      #
      #   widget(:container).right_click
      #
      # This is the equivalent of doing the following using Capybara:
      #
      #   find('#container').right_click
      def right_click(*args)
        if args.empty?
          root.right_click
        else
          widget(*args).right_click
        end
      end

      # Determines if the widget underlying an action exists.
      #
      # @param name the name of the action
      #
      # @raise Missing if an action with +name+ can't be found.
      #
      # @return [Boolean] +true+ if the action widget is found, +false+
      #   otherwise.
      def has_action?(name)
        raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)

        visible?(:"#{name}_widget")
      end

      def id
        root['id']
      end

      def classes
        root['class'].split
      end

      # Determines if the widget has a specific class
      #
      # @param name the name of the class
      #
      # @return [Boolean] +true+ if the class is found, +false+ otherwise
      def class?(name)
        classes.include?(name)
      end

      def html
        xml = Nokogiri::HTML(page.body).at(root.path).to_xml

        Nokogiri::XML(xml, &:noblanks).to_xhtml.gsub("\n", "")
      end

      def text
        StringValue.new(root.text.strip)
      end

      # Converts this widget into a string representation suitable to be displayed
      # in a Cucumber table cell. By default calls #text.
      #
      # This method will be called by methods that build tables or rows (usually
      # #to_table or #to_row) so, in general, you won't call it directly, but feel
      # free to override it when needed.
      #
      # Returns a String.
      def to_cell
        text
      end

      def to_s
        text
      end

      def value
        text
      end

      private

      def page
        Capybara.current_session
      end
    end
  end
end