Class: Dill::Widget

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Dill::WidgetParts::Container, Dill::WidgetParts::Struct
Defined in:
lib/dill/widgets/widget.rb

Direct Known Subclasses

AutoTable::Row, BaseTable, Field, FieldGroup, List, ListItem

Defined Under Namespace

Classes: MissingSelector, Removed

Widget macros collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Dill::WidgetParts::Container

#has_no_widget?, #has_widget?, #widget

Methods included from Dill::WidgetParts::Struct

included

Constructor Details

#initialize(node = nil, &query) ⇒ Widget

Returns a new instance of Widget.



272
273
274
275
# File 'lib/dill/widgets/widget.rb', line 272

def initialize(node = nil, &query)
  self.node = node
  self.query = query
end

Class Method Details

.action(name, selector) ⇒ Object

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.

Examples:

# Consider the widget will encapsulate the following HTML
#
# <div id="profile">
#  <a href="/profiles/1/edit" rel="edit">Edit</a>
# </div>
class PirateProfile < Dill::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

Parameters:

  • name

    the name of the action

  • selector

    the selector for the widget that will be clicked



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/dill/widgets/widget.rb', line 45

def self.action(name, selector)
  wname = :"#{name}_widget"

  widget wname, selector

  define_method name do
    widget(wname).click

    self
  end
end

.find_in(parent, *args) ⇒ Object

Finds a single instance of the current widget in node.

Parameters:

  • node

    the node we want to search in

Returns:

  • a new instance of the current widget class.

Raises:

  • (Capybara::ElementNotFoundError)

    if the widget can’t be found



183
184
185
# File 'lib/dill/widgets/widget.rb', line 183

def self.find_in(parent, *args)
  new { parent.root.find(*selector(*args)) }
end

.present_in?(parent) ⇒ Boolean

Determines if an instance of this widget class exists in parent_node.

Parameters:

  • parent_node (Capybara::Node)

    the node we want to search in

Returns:

  • (Boolean)

    true if a widget instance is found, false otherwise.



193
194
195
# File 'lib/dill/widgets/widget.rb', line 193

def self.present_in?(parent)
  find_in(parent).present?
end

.root(*selector, &block) ⇒ Object

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 < Dill::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 < Dill::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 < Dill::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.has_widget?(: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 < Dill::Widget
  root :xpath, '//some/node'
end


250
251
252
# File 'lib/dill/widgets/widget.rb', line 250

def self.root(*selector, &block)
  @selector = block ? [block] : selector.flatten
end

.selector(*args) ⇒ Object

Returns the selector specified with root.



258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/dill/widgets/widget.rb', line 258

def self.selector(*args)
  if @selector
    fst = @selector.first

    fst.respond_to?(:call) ? fst.call(*args) : @selector
  else
    if superclass.respond_to?(:selector)
      superclass.selector
    else
      raise MissingSelector, 'no selector defined'
    end
  end
end

.widget(name, selector, type = Widget) ⇒ Object .widget(name, type) ⇒ Object

Declares a new child widget.

Child widgets are accessible inside the container widget using the Dill::WidgetParts::Container#widget message, or by sending a message name. They are automatically scoped to the parent widget’s root node.

Examples:

Defining a widget

# Given the following HTML:
#
# <div id="root">
#   <span id="child">Child</span>
# </div>
class Container < Dill::Widget
  root '#root'

  widget :my_widget, '#child'
end

container = widget(:container)

# accessing using #widget
my_widget = container.widget(:my_widget)

# accessing using #my_widget
my_widget = container.my_widget

Overloads:

  • .widget(name, selector, type = Widget) ⇒ Object

    The most common form, it allows you to pass in a selector as well as a type for the child widget. The selector will override type‘s root selector, if type has one defined.

    Parameters:

    • name

      the child widget’s name.

    • selector

      the child widget’s selector. You can pass either a String or, if you want to use a composite selector, an Array.

    • type (defaults to: Widget)

      the child widget’s parent class.

  • .widget(name, type) ⇒ Object

    This form allows you to omit selector from the arguments. It will reuse type‘s root selector.

    Parameters:

    • name

      the child widget’s name.

    • type

      the child widget’s parent class.

    Raises:

    • ArgumentError if type has no root selector defined.

Yields:

  • A block allowing you to further customize the widget behavior.

Raises:

  • (ArgumentError)

See Also:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/dill/widgets/widget.rb', line 107

def self.widget(name, *rest, &block)
  raise ArgumentError, "`#{name}' is a reserved name" \
    if WidgetParts::Container.instance_methods.include?(name.to_sym)

  case rest.first
  when Class
    arg_count = rest.size + 1
    raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)" \
      unless arg_count == 2

    type = rest.first
    raise TypeError, "can't convert `#{type}' to Widget" \
      unless type.methods.include?(:selector)
    raise ArgumentError, "missing root selector for `#{type}'" \
      unless type.selector

    selector = type.selector
  when String, Array, Proc
    arg_count = rest.size + 1

    case arg_count
    when 0, 1
      raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)"
    when 2
      selector, type = [*rest, Widget]
    when 3
      selector, type = rest

      raise TypeError, "can't convert `#{type}' to Widget" \
        unless Class === type
    else
      raise ArgumentError, "wrong number of arguments (#{arg_count} for 3)"
    end
  else
    raise ArgumentError, "unknown method signature: #{rest.inspect}"
  end

  child = WidgetClass.new(selector, type, &block)

  const_set(Dill::WidgetName.new(name).to_sym, child)

  child
end

.widget_delegator(name, widget_message, method_name = nil) ⇒ Object

Creates a delegator for one child widget message.

Since widgets are accessed through Dill::WidgetParts::Container#widget, we can’t use Forwardable to delegate messages to widgets.

Parameters:

  • name

    the name of the receiver child widget

  • widget_message

    the name of the message to be sent to the child widget

  • method_name (defaults to: nil)

    the name of the delegator. If nil the method will have the same name as the message it will send.



160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/dill/widgets/widget.rb', line 160

def self.widget_delegator(name, widget_message, method_name = nil)
  method_name = method_name || widget_message

  class_eval "    def \#{method_name}(*args)\n      if args.size == 1\n        widget(:\#{name}).\#{widget_message} args.first\n      else\n        widget(:\#{name}).\#{widget_message} *args\n      end\n    end\n  RUBY\nend\n"

Instance Method Details

#absent?Boolean

Alias for #gone?

Returns:

  • (Boolean)


278
279
280
# File 'lib/dill/widgets/widget.rb', line 278

def absent?
  gone?
end

#click(*args) ⇒ Object

Clicks the current widget, or the child widget given by name.

Usage

Given the following widget definition:

class Container < Dill::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


309
310
311
312
313
314
315
# File 'lib/dill/widgets/widget.rb', line 309

def click(*args)
  if args.empty?
    root.click
  else
    widget(*args).click
  end
end

#diff(table, wait_time = Capybara.default_wait_time) ⇒ Object

Compares this widget with the given Cucumber table.

Example

Then(/^some step that takes in a cucumber table$/) do |table|
  widget(:my_widget).diff table
end


324
325
326
# File 'lib/dill/widgets/widget.rb', line 324

def diff(table, wait_time = Capybara.default_wait_time)
  table.diff!(to_table) || true
end

#gone?Boolean

Returns true if the widget is not visible, or has been removed from the DOM.

Returns:

  • (Boolean)


330
331
332
# File 'lib/dill/widgets/widget.rb', line 330

def gone?
  ! root rescue true
end

#has_action?(name) ⇒ Boolean

Determines if the widget underlying an action exists.

Parameters:

  • name

    the name of the action

Returns:

  • (Boolean)

    true if the action widget is found, false otherwise.

Raises:

  • Missing if an action with name can’t be found.



342
343
344
345
346
# File 'lib/dill/widgets/widget.rb', line 342

def has_action?(name)
  raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)

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

#inspectObject



348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/dill/widgets/widget.rb', line 348

def inspect
  inspection = "<!-- #{self.class.name}: -->\n"

  begin
    root = self.root
    xml = Nokogiri::HTML(page.body).at(root.path).to_xml

    inspection << Nokogiri::XML(xml, &:noblanks).to_xhtml
  rescue Capybara::NotSupportedByDriverError
    inspection << "<#{root.tag_name}>\n#{to_s}"
  rescue Capybara::ElementNotFound, *page.driver.invalid_element_errors
    "#<DETACHED>"
  end
end

#present?Boolean

Returns true if widget is visible.

Returns:

  • (Boolean)


364
365
366
# File 'lib/dill/widgets/widget.rb', line 364

def present?
  !! root rescue false
end

#rootObject



368
369
370
# File 'lib/dill/widgets/widget.rb', line 368

def root
  node || query.()
end

#textObject



372
373
374
# File 'lib/dill/widgets/widget.rb', line 372

def text
  root.text.strip
end

#to_cellObject

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.



384
385
386
# File 'lib/dill/widgets/widget.rb', line 384

def to_cell
  text
end

#to_sObject



388
389
390
# File 'lib/dill/widgets/widget.rb', line 388

def to_s
  text
end

#valueObject



392
393
394
# File 'lib/dill/widgets/widget.rb', line 392

def value
  text
end