testable 0.10.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ # rubocop:disable Style/DocumentationMethod
1
2
  module Testable
2
3
  module Errors
3
4
  NoUrlForDefinition = Class.new(StandardError)
@@ -5,6 +6,18 @@ module Testable
5
6
  NoUrlMatchPossible = Class.new(StandardError)
6
7
  NoTitleForDefinition = Class.new(StandardError)
7
8
  PageNotValidatedError = Class.new(StandardError)
9
+ PluralizedElementError = Class.new(StandardError)
10
+ RegionNamespaceError = Class.new(StandardError)
11
+ RegionFinderError = Class.new(StandardError)
12
+
13
+ public_constant :NoUrlForDefinition
14
+ public_constant :NoUrlMatchForDefinition
15
+ public_constant :NoUrlMatchPossible
16
+ public_constant :NoTitleForDefinition
17
+ public_constant :PageNotValidatedError
18
+ public_constant :PluralizedElementError
19
+ public_constant :RegionNamespaceError
20
+ public_constant :RegionFinderError
8
21
 
9
22
  class PageURLFromFactoryNotVerified < StandardError
10
23
  def message
@@ -13,3 +26,4 @@ module Testable
13
26
  end
14
27
  end
15
28
  end
29
+ # rubocop:enable Style/DocumentationMethod
@@ -1,3 +1,5 @@
1
+ # rubocop:disable Style/DocumentationMethod
2
+
1
3
  class String
2
4
  # This is only required if using a version of Ruby before 2.4. A match?
3
5
  # method for String was added in version 2.4.
@@ -11,3 +13,5 @@ class FalseClass
11
13
  false
12
14
  end
13
15
  end
16
+
17
+ # rubocop:enable Style/DocumentationMethod
@@ -20,9 +20,9 @@ class Object
20
20
 
21
21
  method_chain.split('.').inject(self) do |o, m|
22
22
  if data.nil?
23
- o.send(m.intern)
23
+ o.public_send(m.to_sym)
24
24
  else
25
- o.send(m.intern, data)
25
+ o.public_send(m.to_sym, data)
26
26
  end
27
27
  end
28
28
  end
@@ -80,7 +80,7 @@ module Testable
80
80
  def use_data_with(key, value)
81
81
  value = preprocess_value(value, key)
82
82
 
83
- element = send(key.to_s.tr(' ', '_'))
83
+ element = public_send(key.to_s.tr(' ', '_'))
84
84
  set_and_select(key, element, value)
85
85
  check_and_uncheck(key, element, value)
86
86
  click(key, element)
@@ -107,6 +107,8 @@ module Testable
107
107
  selected = list.sample.text
108
108
  selected = list.sample.text if selected.nil?
109
109
  value = selected
110
+ else # rubocop:disable Style/EmptyElse
111
+ # Silent pass through.
110
112
  end
111
113
 
112
114
  value
@@ -137,7 +139,7 @@ module Testable
137
139
  # visible (meaning, display is not 'none'), and is capable of accepting
138
140
  # input, thus being enabled.
139
141
  def object_enabled_for(key)
140
- web_element = send(key.to_s.tr(' ', '_'))
142
+ web_element = public_send(key.to_s.tr(' ', '_'))
141
143
  web_element.enabled? && web_element.present?
142
144
  end
143
145
  end
@@ -2,6 +2,8 @@ module Watir
2
2
  class Element
3
3
  OBSERVER_FILE = "/dom_observer.js".freeze
4
4
  DOM_OBSERVER = File.read("#{File.dirname(__FILE__)}#{OBSERVER_FILE}").freeze
5
+ private_constant :OBSERVER_FILE
6
+ private_constant :DOM_OBSERVER
5
7
 
6
8
  # This method makes a call to `execute_async_script` which means that the
7
9
  # DOM observer script must explicitly signal that it is finished by
@@ -45,11 +45,49 @@ module Testable
45
45
  # validation. That's necessary because a ready validation has to find
46
46
  # an element in order to determine the ready state, but that element
47
47
  # might not be present.
48
- def access_element(element, locators, _qualifiers)
49
- if element == "element".to_sym
50
- @browser.element(locators).to_subtype
48
+
49
+ # rubocop:disable Metrics/AbcSize
50
+ # rubocop:disable Metrics/CyclomaticComplexity
51
+ # rubocop:disable Metrics/PerceivedComplexity
52
+ # rubocop:disable Metrics/MethodLength
53
+ def access_element(element, locators, qualifiers)
54
+ if qualifiers.empty?
55
+ if element == "element".to_sym
56
+ @browser.element(locators).to_subtype
57
+ else
58
+ region_element.__send__(element, locators)
59
+ end
51
60
  else
52
- @browser.__send__(element, locators)
61
+ # If the qualifiers are not empty, then that means the framework
62
+ # has to consider a given set of elements so that it can check
63
+ # the qualifier against them.
64
+ plural = Testable.plural?(element)
65
+ element = Testable.pluralize(element) unless plural
66
+
67
+ elements = region_element.__send__(element, locators)
68
+
69
+ # Consider the following element definition:
70
+ #
71
+ # select_list :car_make, name: 'car', selected: 'Audi', enabled: true
72
+ #
73
+ # In this case, the qualifiers will be `selected` (with a value of)
74
+ # "Audi") and `enabled` (with a value of true. The arity of the
75
+ # `selected` method would be 1 while the arity of the `enabled`
76
+ # method would be 0.
77
+ qualifiers.each do |qualifier, value|
78
+ elements.to_a.select! do |ele|
79
+ if ele.public_method(:"#{qualifier}?").arity.zero?
80
+ ele.__send__(:"#{qualifier}?") == value
81
+ else
82
+ ele.__send__(:"#{qualifier}?", value)
83
+ end
84
+ end
85
+ end
86
+
87
+ # If the locator passed in was plural, then any elements matching
88
+ # the locator and qualifier have to be returned. Otherwise, it will
89
+ # just be the first item of the elements found.
90
+ plural ? elements : elements.first
53
91
  end
54
92
  rescue Watir::Exception::UnknownObjectException
55
93
  return false if caller_locations.any? do |str|
@@ -58,6 +96,10 @@ module Testable
58
96
 
59
97
  raise
60
98
  end
99
+ # rubocop:enable Metrics/AbcSize
100
+ # rubocop:enable Metrics/CyclomaticComplexity
101
+ # rubocop:enable Metrics/PerceivedComplexity
102
+ # rubocop:enable Metrics/MethodLength
61
103
  end
62
104
  end
63
105
  end
@@ -2,13 +2,17 @@ require "logger"
2
2
 
3
3
  module Testable
4
4
  class Logger
5
+ # Creates a logger instance with a predefined set of configuration
6
+ # options. This logger instance will be available to any portion of
7
+ # tests that are using the framework.
5
8
  def create(output = $stdout)
6
9
  logger = ::Logger.new(output)
7
10
  logger.progname = 'Testable'
8
11
  logger.level = :UNKNOWN
9
- logger.formatter = proc do |severity, time, progname, msg|
10
- "#{time.strftime('%F %T')} - #{severity} - #{progname} - #{msg}\n"
11
- end
12
+ logger.formatter =
13
+ proc do |severity, time, progname, msg|
14
+ "#{time.strftime('%F %T')} - #{severity} - #{progname} - #{msg}\n"
15
+ end
12
16
 
13
17
  logger
14
18
  end
@@ -0,0 +1,173 @@
1
+ module Testable
2
+ module Pages
3
+ module Region
4
+ # Allows for a "has_one" method to be declared on a model, like a page
5
+ # object to specify that the model has a region associated with it. In
6
+ # this case, that would be a single region that can be located by a
7
+ # reference to a specific class (which is also a model) and, relative
8
+ # to that model, is within some specific means of identification.
9
+ # rubocop:disable Naming/PredicateName
10
+ def has_one(identifier, **context, &block)
11
+ within = context[:in] || context[:within]
12
+ region_class = context[:class] || context[:region_class]
13
+ define_region_accessor(identifier, within: within, region_class: region_class, &block)
14
+ end
15
+
16
+ # Allows for a "has_many" method to be declared on a model, like a poage
17
+ # object to specify that the model was a certain region associated with
18
+ # it, but where that region occurs more than once.
19
+ def has_many(identifier, **context, &block)
20
+ region_class = context[:class] || context[:region_class]
21
+ collection_class = context[:through] || context[:collection_class]
22
+ each = context[:each] || raise(ArgumentError, 'the "has_many" method requires an "each" param')
23
+ within = context[:in] || context[:within]
24
+ define_region_accessor(
25
+ identifier,
26
+ within: within,
27
+ each: each,
28
+ region_class: region_class,
29
+ collection_class: collection_class,
30
+ &block
31
+ )
32
+ define_finder_method(identifier)
33
+ end
34
+ # rubocop:enable Naming/PredicateName
35
+
36
+ private
37
+
38
+ # Defines an accessor method for an region.
39
+ # ..........................
40
+ # rubocop:disable Metrics/AbcSize
41
+ # rubocop:disable Metrics/CyclomaticComplexity
42
+ # rubocop:disable Metrics/MethodLength
43
+ # rubocop:disable Metrics/PerceivedComplexity
44
+ # rubocop:disable Metrics/ParameterLists
45
+ # rubocop:disable Metrics/BlockLength
46
+ # rubocop:disable Metrics/BlockNesting
47
+ # def define_region_accessor(identifier, within: nil, each: nil, collection_class: nil, region_class: nil, &block)
48
+ def define_region_accessor(identifier, within: nil, each: nil, region_class: nil, collection_class: nil, &block)
49
+ include(Module.new do
50
+ define_method(identifier) do
51
+ # The class path is what essentially determines the model to
52
+ # reference that the region is a part of. However, it is possible
53
+ # to define an inline region, which would effectively make the
54
+ # class anonymous, and thus not having an actual name. Thus is it
55
+ # necessary to provide a stand-in name for those cases.
56
+ class_path = self.class.name ? self.class.name.split('::') : ['TestableRegion']
57
+
58
+ # Namespacing is needed in cases where there are nested classes.
59
+ # It's important to get the full namespace of any classes that are
60
+ # said to reference the region.
61
+ namespace =
62
+ if class_path.size > 1
63
+ class_path.pop
64
+ Object.const_get(class_path.join('::'))
65
+ elsif class_path.size == 1
66
+ self.class
67
+ else
68
+ raise Testable::Errors::RegionNamespaceError, "Cannot understand namespace from #{class_path}"
69
+ end
70
+
71
+ # A copy of the passed-in region class is necessary because the
72
+ # `region_class` is declared outside of this defined function.
73
+ # And the function could change that class. Thus a reference is
74
+ # needed to the original `region_class` but also allowing for that
75
+ # class to be changed.
76
+ region_single_class = region_class
77
+
78
+ unless region_single_class
79
+ if block_given?
80
+ region_single_class = Class.new
81
+ region_single_class.class_eval { include(Testable) }
82
+ region_single_class.class_eval(&block)
83
+ else
84
+ singular_klass = identifier.to_s.split('_').map(&:capitalize).join
85
+
86
+ # rubocop:disable Style/MissingElse
87
+ if each
88
+ collection_class_name = "#{singular_klass}Region"
89
+ singular_klass = singular_klass.sub(/s\z/, '')
90
+ end
91
+ # rubocop:enable Style/MissingElse
92
+
93
+ singular_klass << 'Region'
94
+ region_single_class = namespace.const_get(singular_klass)
95
+ end
96
+ end
97
+
98
+ # The scope is used to provide a reference to the element that
99
+ # is said to act as a container for the region.
100
+ scope =
101
+ case within
102
+ when Proc
103
+ instance_exec(&within)
104
+ when Hash
105
+ region_element.element(within)
106
+ else
107
+ region_element
108
+ end
109
+
110
+ if each
111
+ # The `elements` will be a `Watir::HTMLElementCollection`.
112
+ elements = (scope.exists? ? scope.elements(each) : [])
113
+
114
+ if collection_class_name && namespace.const_defined?(collection_class_name)
115
+ region_collection_class = namespace.const_get(collection_class_name)
116
+ elsif collection_class
117
+ region_collection_class = collection_class
118
+ else
119
+ return elements.map { |element| region_single_class.new(@browser, element, self) }
120
+ end
121
+
122
+ region_collection_class.class_eval do
123
+ include Enumerable
124
+
125
+ attr_reader :region_collection
126
+
127
+ define_method(:initialize) do |browser, region_element, region_elements|
128
+ super(browser, region_element, self)
129
+
130
+ @region_collection =
131
+ if region_elements.all? { |element| element.is_a?(Watir::Element) }
132
+ region_elements.map { |element| region_single_class.new(browser, element, self) }
133
+ else
134
+ region_elements
135
+ end
136
+ end
137
+
138
+ def each(&block)
139
+ region_collection.each(&block)
140
+ end
141
+ end
142
+
143
+ region_collection_class.new(@browser, scope, elements)
144
+ else
145
+ region_single_class.new(@browser, scope, self)
146
+ end
147
+ end
148
+ end)
149
+ end
150
+ # rubocop:enable Metrics/AbcSize
151
+ # rubocop:enable Metrics/CyclomaticComplexity
152
+ # rubocop:enable Metrics/MethodLength
153
+ # rubocop:enable Metrics/PerceivedComplexity
154
+ # rubocop:enable Metrics/ParameterLists
155
+ # rubocop:enable Metrics/BlockLength
156
+ # rubocop:enable Metrics/BlockNesting
157
+
158
+ def define_finder_method(identifier)
159
+ finder_method_name = identifier.to_s.sub(/s\z/, '')
160
+
161
+ include(Module.new do
162
+ define_method(finder_method_name) do |**opts|
163
+ __send__(identifier).find do |entity|
164
+ opts.all? do |key, value|
165
+ entity.__send__(key) == value
166
+ end
167
+ end || raise(Testable::Errors::RegionFinderError, "No #{finder_method_name} matching: #{opts}.")
168
+ end
169
+ end)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -1,23 +1,27 @@
1
1
  module Testable
2
2
  module_function
3
3
 
4
- VERSION = "0.10.0".freeze
4
+ VERSION = "1.0.0".freeze
5
+ public_constant :VERSION
5
6
 
7
+ # Returns version information about Testable and its core dependencies.
6
8
  def version
7
9
  """
8
10
  Testable v#{Testable::VERSION}
9
11
  watir: #{gem_version('watir')}
10
12
  selenium-webdriver: #{gem_version('selenium-webdriver')}
11
- capybara: #{gem_version('capybara')}
12
13
  """
13
14
  end
14
15
 
16
+ # Returns a gem version for a given gem, assuming the gem has
17
+ # been loaded.
15
18
  def gem_version(name)
16
19
  Gem.loaded_specs[name].version
17
20
  rescue NoMethodError
18
21
  puts "No gem loaded for #{name}."
19
22
  end
20
23
 
24
+ # Returns all of the dependencies that Testable relies on.
21
25
  def dependencies
22
26
  Gem.loaded_specs.values.map { |spec| "#{spec.name} #{spec.version}\n" }
23
27
  .uniq.sort.join(",").split(",")
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Jeff Nyman"]
9
9
  spec.email = ["jeffnyman@gmail.com"]
10
10
 
11
- spec.summary = %q{Web and API Automation, using Capybara and Watir}
11
+ spec.summary = %q{Web and API Automation, using Watir}
12
12
  spec.description = %q{Provides a semantic DSL to construct fluent interfaces for test execution logic.}
13
13
  spec.homepage = "https://github.com/jeffnyman/testable"
14
14
  spec.license = "MIT"
@@ -31,12 +31,10 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "bundler", "~> 2.0"
32
32
  spec.add_development_dependency "rake", ">= 12.3.3"
33
33
  spec.add_development_dependency "rspec", "~> 3.0"
34
- spec.add_development_dependency "simplecov"
35
34
  spec.add_development_dependency "rubocop"
36
35
  spec.add_development_dependency "pry"
37
36
 
38
37
  spec.add_runtime_dependency "watir", ["~> 6.16"]
39
- spec.add_runtime_dependency "capybara", [">= 2", "< 4"]
40
38
 
41
39
  spec.post_install_message = %{
42
40
  (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Nyman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-11 00:00:00.000000000 Z
11
+ date: 2020-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: simplecov
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: rubocop
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -108,26 +94,6 @@ dependencies:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
96
  version: '6.16'
111
- - !ruby/object:Gem::Dependency
112
- name: capybara
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '2'
118
- - - "<"
119
- - !ruby/object:Gem::Version
120
- version: '4'
121
- type: :runtime
122
- prerelease: false
123
- version_requirements: !ruby/object:Gem::Requirement
124
- requirements:
125
- - - ">="
126
- - !ruby/object:Gem::Version
127
- version: '2'
128
- - - "<"
129
- - !ruby/object:Gem::Version
130
- version: '4'
131
97
  description: Provides a semantic DSL to construct fluent interfaces for test execution
132
98
  logic.
133
99
  email:
@@ -148,9 +114,6 @@ files:
148
114
  - Rakefile
149
115
  - bin/console
150
116
  - bin/setup
151
- - examples/testable-capybara-context.rb
152
- - examples/testable-capybara-rspec.rb
153
- - examples/testable-capybara.rb
154
117
  - examples/testable-info.rb
155
118
  - examples/testable-watir-context.rb
156
119
  - examples/testable-watir-datasetter.rb
@@ -160,9 +123,6 @@ files:
160
123
  - examples/testable-watir.rb
161
124
  - lib/testable.rb
162
125
  - lib/testable/attribute.rb
163
- - lib/testable/capybara/dsl.rb
164
- - lib/testable/capybara/node.rb
165
- - lib/testable/capybara/page.rb
166
126
  - lib/testable/context.rb
167
127
  - lib/testable/deprecator.rb
168
128
  - lib/testable/element.rb
@@ -175,6 +135,7 @@ files:
175
135
  - lib/testable/logger.rb
176
136
  - lib/testable/page.rb
177
137
  - lib/testable/ready.rb
138
+ - lib/testable/region.rb
178
139
  - lib/testable/situation.rb
179
140
  - lib/testable/version.rb
180
141
  - testable.gemspec
@@ -186,7 +147,7 @@ metadata:
186
147
  source_code_uri: https://github.com/jeffnyman/testable
187
148
  changelog_uri: https://github.com/jeffnyman/testable/blob/master/CHANGELOG.md
188
149
  post_install_message: "\n(::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::)\n
189
- \ Testable 0.10.0 has been installed.\n(::) (::) (::) (::) (::) (::) (::) (::) (::)
150
+ \ Testable 1.0.0 has been installed.\n(::) (::) (::) (::) (::) (::) (::) (::) (::)
190
151
  (::) (::) (::)\n "
191
152
  rdoc_options: []
192
153
  require_paths:
@@ -205,5 +166,5 @@ requirements: []
205
166
  rubygems_version: 3.1.2
206
167
  signing_key:
207
168
  specification_version: 4
208
- summary: Web and API Automation, using Capybara and Watir
169
+ summary: Web and API Automation, using Watir
209
170
  test_files: []