testable 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []