Module: Tuile::Testing

Defined in:
lib/tuile/testing.rb,
sig/tuile.rbs

Overview

Finds a component in the tree, so a spec can drive the UI it built four layers down:

Testing.get(Component::Button, caption: "Save").handle_key?(Keys::ENTER)
Testing.get(id: :name).value = "Zaphod"
Testing.find(Component::Checkbox, in: pane, count: 3)

Testing.get demands exactly one match and raises with a Testing.dump of the tree it searched; Testing.find returns every match and takes an optional count:. Both search Screen's whole tree by default — popups included, since they live under the same ScreenPane as the content — or the subtree given as in:.

Call these qualified, as above: find and get collide with names a spec suite is likely to have already (Capybara's find), so there is no Component#get and mixing this module in is not recommended. Tuile's own specs sit inside module Tuile and so need no include.

A hidden component is never found: these simulate a user, and a spec that drove a hidden Component::Button would pass against a form nobody can operate. Both walk Component#walk_shown_tree; a failed lookup says how many hidden components would have matched, and Testing.dump shows them.

Testing.find(Component::TextField, count: 0)   # the user can't reach it
refute field.visible?                          # it is hidden

There is deliberately no visible: filter handing one back to drive (D_visibility).

For what a component shows, assert on Screen#buffer instead — this locates and drives, it does not replace that channel. See book ch8 for the worked usage and design/decisions.md D_component_lookup for the design.

Defined Under Namespace

Classes: LookupError

Class Method Summary collapse

Class Method Details

.dump(scope, marked = [], excluded = []) ⇒ String

The searched tree, one component per row, indented by depth and with the Tuile:: namespaces stripped so a fifty-row dump stays readable (an app's own component classes keep their full name):

#<ScreenPane rect=(0,0 160x50)>
  #<Window rect=(0,0 40x10) caption="Settings">
    #<Layout::Vertical rect=(1,1 38x8)>
→       #<Button id=:save rect=(1,1 38x1) caption="Save">

@param scope — root of the tree to dump.

@param marked — components to flag with a leading arrow — the matches, when a lookup found the wrong number of them.

@param excluded — components to flag with — ones that matched the spec but were skipped for being hidden.

Parameters:

Returns:

  • (String)


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/tuile/testing.rb', line 110

def dump(scope, marked = [], excluded = [])
  base = scope.depth
  rows = []
  # walk_tree, not walk_shown_tree: a reader looks here to find out where
  # their component went, so the ones the search skipped are the point.
  scope.walk_tree do |c|
    mark = if marked.any? { _1.equal?(c) } then ""
           elsif excluded.any? { _1.equal?(c) } then ""
           else " "
           end
    row = c.inspect.sub("#<Tuile::Component::", "#<").sub("#<Tuile::", "#<")
    rows << "#{mark} #{"  " * (c.depth - base)}#{row}"
  end
  rows.join("\n")
end

.find(klass = Component, in: nil, id: nil, caption: nil, count: nil, &predicate) ⇒ Object

Every component in the searched tree matching the spec, in pre-order.

find(Component::Button)                     # every button on screen
find(Component::HasBadInput, in: form)      # a mixin works too
find(Component::Label, caption: /^Total/)   # Regexp: partial match
find(Component::Popup, count: 1..)          # assert at least one

@param klass — matched with is_a?, so a mixin (Component::HasValue) finds every field that includes it.

@param in — root of the subtree to search, itself included. Defaults to Screen.instance.pane — the whole UI.

@param id — matched against Component#id.

@param caption — matched with === against Component::HasCaption#caption.to_s, so a String is exact and a Regexp is a partial match. Never matches a component without a caption.

@param count — how many matches are expected; any number when nil.



65
66
67
68
69
70
71
72
73
74
# File 'lib/tuile/testing.rb', line 65

def find(klass = Component, in: nil, id: nil, caption: nil, count: nil, &predicate)
  # `in` is a Ruby keyword, so the local it binds is unreachable by name.
  scope = binding.local_variable_get(:in) || Screen.instance.pane
  spec = ->(c) { matches_spec?(c, klass, id, caption, predicate) }
  matches = []
  scope.walk_shown_tree { |c| matches << c if spec.call(c) }
  return matches if count.nil? || spec_match?(count, matches.size)

  raise LookupError, failure(klass, id, caption, predicate, count, matches, scope, spec)
end

.get(klass = Component, in: nil, id: nil, caption: nil, &predicate) ⇒ Object

The one component matching the spec — find with count: 1, so it raises rather than returning nil, and raises on an ambiguous spec too.

get(Component::ComboBox, in: sampler.demo_window)

@param klass — see find.

@param in — see find.

@param id — see find.

@param caption — see find.



90
91
92
93
# File 'lib/tuile/testing.rb', line 90

def get(klass = Component, in: nil, id: nil, caption: nil, &predicate)
  scope = binding.local_variable_get(:in)
  find(klass, in: scope, id:, caption:, count: 1, &predicate).first
end