testable 0.3.0 → 0.8.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +37 -25
  3. data/.hound.yml +31 -12
  4. data/.rubocop.yml +4 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +1 -1
  7. data/Gemfile +3 -1
  8. data/{LICENSE.txt → LICENSE.md} +2 -2
  9. data/README.md +36 -17
  10. data/Rakefile +52 -11
  11. data/bin/console +2 -2
  12. data/bin/setup +0 -0
  13. data/examples/testable-capybara-context.rb +64 -0
  14. data/examples/testable-capybara-rspec.rb +70 -0
  15. data/examples/testable-capybara.rb +46 -0
  16. data/examples/testable-info.rb +65 -0
  17. data/examples/testable-watir-context.rb +67 -0
  18. data/examples/testable-watir-datasetter.rb +52 -0
  19. data/examples/testable-watir-events.rb +44 -0
  20. data/examples/testable-watir-ready.rb +34 -0
  21. data/examples/testable-watir-test.rb +80 -0
  22. data/examples/testable-watir.rb +118 -0
  23. data/lib/testable.rb +142 -10
  24. data/lib/testable/attribute.rb +38 -0
  25. data/lib/testable/capybara/dsl.rb +82 -0
  26. data/lib/testable/capybara/node.rb +30 -0
  27. data/lib/testable/capybara/page.rb +29 -0
  28. data/lib/testable/context.rb +73 -0
  29. data/lib/testable/deprecator.rb +40 -0
  30. data/lib/testable/element.rb +162 -31
  31. data/lib/testable/errors.rb +6 -2
  32. data/lib/testable/extensions/core_ruby.rb +13 -0
  33. data/lib/testable/extensions/data_setter.rb +144 -0
  34. data/lib/testable/extensions/dom_observer.js +58 -4
  35. data/lib/testable/extensions/dom_observer.rb +73 -0
  36. data/lib/testable/locator.rb +63 -0
  37. data/lib/testable/logger.rb +16 -0
  38. data/lib/testable/page.rb +216 -0
  39. data/lib/testable/ready.rb +49 -7
  40. data/lib/testable/situation.rb +9 -28
  41. data/lib/testable/version.rb +7 -6
  42. data/testable.gemspec +19 -9
  43. metadata +90 -23
  44. data/circle.yml +0 -3
  45. data/lib/testable/data_setter.rb +0 -51
  46. data/lib/testable/dom_update.rb +0 -19
  47. data/lib/testable/factory.rb +0 -27
  48. data/lib/testable/interface.rb +0 -114
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << "./lib"
3
+
4
+ require "rspec"
5
+ # rubocop:disable Style/MixinUsage
6
+ include RSpec::Matchers
7
+ # rubocop:enable Style/MixinUsage
8
+
9
+ require "testable"
10
+
11
+ class Home
12
+ include Testable
13
+
14
+ url_is "https://veilus.herokuapp.com/"
15
+ url_matches(/heroku/)
16
+ title_is "Veilus"
17
+
18
+ # Elements can be defined with HTML-style names as found in Watir.
19
+ p :login_form, id: "open", visible: true
20
+ text_field :username, id: "username"
21
+ text_field :password
22
+ button :login, id: "login-button"
23
+ div :message, class: "notice"
24
+
25
+ # Elements can be defined with a generic name.
26
+ # element :login_form, id: "open", visible: true
27
+ # element :username, id: "username"
28
+ # element :password
29
+ # element :login, id: "login-button"
30
+ # element :message, class: "notice"
31
+ end
32
+
33
+ # You can pass argument options to the driver:
34
+
35
+ # args = ['user-data-dir=~/Library/Application\ Support/Google/Chrome']
36
+ # Testable.start_browser :chrome, options: {args: args}
37
+
38
+ # You can pass switches to the driver:
39
+
40
+ # Testable.set_browser :chrome, switches: %w[--ignore-certificate-errors
41
+ # --disable-popup-blocking
42
+ # --disable-translate
43
+ # --disable-notifications
44
+ # --disable-gpu
45
+ # --disable-login-screen-apps
46
+ # ]
47
+
48
+ Testable.start_browser :firefox
49
+
50
+ page = Home.new
51
+
52
+ # You can specify a URL to visit or you can rely on the provided
53
+ # url_is attribute on the page definition. So you could do this:
54
+ # page.visit("https://veilus.herokuapp.com/")
55
+ page.visit
56
+
57
+ expect(page.url).to eq(page.url_attribute)
58
+ expect(page.url).to match(page.url_match_attribute)
59
+ expect(page.title).to eq(page.title_attribute)
60
+
61
+ expect(page.has_correct_url?).to be_truthy
62
+ expect(page).to have_correct_url
63
+
64
+ expect(page.displayed?).to be_truthy
65
+ expect(page).to be_displayed
66
+
67
+ expect(page.has_correct_title?).to be_truthy
68
+ expect(page).to have_correct_title
69
+
70
+ expect(page.secure?).to be_truthy
71
+ expect(page).to be_secure
72
+
73
+ expect(page.html.include?('<article id="index">')).to be_truthy
74
+ expect(page.text.include?("Running a Local Version")).to be_truthy
75
+
76
+ page.login_form.click
77
+ page.username.set "admin"
78
+ page.password(id: 'password').set "admin"
79
+ page.login.click
80
+ expect(page.message.text).to eq('You are now logged in as admin.')
81
+
82
+ page.run_script("alert('Testing');")
83
+
84
+ expect(page.browser.alert.exists?).to be_truthy
85
+ expect(page.browser.alert.text).to eq("Testing")
86
+ page.browser.alert.ok
87
+ expect(page.browser.alert.exists?).to be_falsy
88
+
89
+ # You have to sometimes go down to Selenium to do certain things with
90
+ # the browser. Here the browser (which is a Watir Browser) that is part
91
+ # of the definition (page) is referencing the driver (which is a Selenium
92
+ # Driver) and is then calling into the `manage` subsystem, which gives
93
+ # access to the window.
94
+ page.browser.driver.manage.window.minimize
95
+
96
+ # Sleeps are a horrible thing. But they are useful for demonstrations.
97
+ # In this case, the sleep is there just to let you see that the browser
98
+ # did minimize before it gets maximized.
99
+ sleep 2
100
+
101
+ page.maximize
102
+
103
+ # Another brief sleep just to show that the maximize did fact work.
104
+ sleep 2
105
+
106
+ page.resize_to(640, 480)
107
+
108
+ # A sleep to show that the resize occurs.
109
+ sleep 2
110
+
111
+ page.move_to(page.screen_width / 2, page.screen_height / 2)
112
+
113
+ # A sleep to show that the move occurs.
114
+ sleep 2
115
+
116
+ page.screenshot("testing.png")
117
+
118
+ Testable.quit_browser
@@ -1,34 +1,160 @@
1
1
  require "testable/version"
2
-
2
+ require "testable/page"
3
3
  require "testable/ready"
4
- require "testable/factory"
4
+ require "testable/logger"
5
+ require "testable/context"
5
6
  require "testable/element"
6
- require "testable/interface"
7
- require "testable/dom_update"
8
- require "testable/data_setter"
7
+ require "testable/locator"
8
+ require "testable/attribute"
9
+ require "testable/deprecator"
10
+
11
+ require "testable/capybara/page"
12
+
13
+ require "testable/extensions/core_ruby"
14
+ require "testable/extensions/data_setter"
15
+ require "testable/extensions/dom_observer"
9
16
 
10
17
  require "watir"
11
- require "selenium-webdriver"
18
+ require "capybara"
19
+ require "webdrivers"
12
20
 
13
21
  module Testable
14
22
  def self.included(caller)
15
- caller.extend Testable::Element
16
- caller.extend Testable::Interface::Page::Attribute
23
+ caller.extend Testable::Pages::Attribute
24
+ caller.extend Testable::Pages::Element
17
25
  caller.__send__ :include, Testable::Ready
18
- caller.__send__ :include, Testable::DataSetter
19
- caller.__send__ :include, Testable::Interface::Page
26
+ caller.__send__ :include, Testable::Pages
20
27
  caller.__send__ :include, Testable::Element::Locator
28
+ caller.__send__ :include, Testable::DataSetter
21
29
  end
22
30
 
23
31
  def initialize(browser = nil, &block)
24
32
  @browser = Testable.browser unless Testable.browser.nil?
25
33
  @browser = browser if Testable.browser.nil?
34
+ begin_with if respond_to?(:begin_with)
26
35
  instance_eval(&block) if block
27
36
  end
28
37
 
38
+ # This accessor is needed so that internal API calls, like `markup` or
39
+ # `text`, have access to the browser instance. This is also necessary
40
+ # in order for element handling to be called appropriately on the a
41
+ # valid browser instance. This is an instance-level access to whatever
42
+ # browser Testable is using.
29
43
  attr_accessor :browser
30
44
 
31
45
  class << self
46
+ # Provides a means to allow a configure block on Testable. This allows you
47
+ # to setup Testable, as such:
48
+ #
49
+ # Testable.configure do |config|
50
+ # config.driver_timeout = 5
51
+ # config.wire_level_logging = :info
52
+ # config.log_level = :debug
53
+ # end
54
+ def configure
55
+ yield self
56
+ end
57
+
58
+ # Watir provides a default timeout of 30 seconds. This allows you to change
59
+ # that in the Testable context. For example:
60
+ #
61
+ # Testable.driver_timeout = 5
62
+ #
63
+ # This would equivalent to doing this:
64
+ #
65
+ # Watir.default_timeout = 5
66
+ def driver_timeout=(value)
67
+ Watir.default_timeout = value
68
+ end
69
+
70
+ # The Testable logger object. To log messages:
71
+ #
72
+ # Testable.logger.info('Some information.')
73
+ # Testable.logger.debug('Some diagnostics')
74
+ #
75
+ # To alter or check the current logging level, you can call `.log_level=`
76
+ # or `.log_level`. By default the logger will output all messages to the
77
+ # standard output ($stdout) but it can be altered to log to a file or to
78
+ # another IO location by calling `.log_path=`.
79
+ def logger
80
+ @logger ||= Testable::Logger.new.create
81
+ end
82
+
83
+ # To enable logging, do this:
84
+ #
85
+ # Testable.log_level = :DEBUG
86
+ # Testable.log_level = 'DEBUG'
87
+ # Testable.log_level = 0
88
+ #
89
+ # This can accept any of a Symbol / String / Integer as an input
90
+ # To disable all logging, which is the case by default, do this:
91
+ #
92
+ # Testable.log_level = :UNKNOWN
93
+ def log_level=(value)
94
+ logger.level = value
95
+ end
96
+
97
+ # To query what level is being logged, do this:
98
+ #
99
+ # Testable.log_level
100
+ #
101
+ # The logging level will be UNKNOWN by default.
102
+ def log_level
103
+ %i[DEBUG INFO WARN ERROR FATAL UNKNOWN][logger.level]
104
+ end
105
+
106
+ # The writer method allows you to configure where you want the output of
107
+ # the Testable logs to go, with the default being standard output. Here
108
+ # is how you could change this to a specific file:
109
+ #
110
+ # Testable.log_path = 'testable.log'
111
+ def log_path=(logdev)
112
+ logger.reopen(logdev)
113
+ end
114
+
115
+ # The wire logger provides logging from Watir, which is very similar to the
116
+ # logging provided by Selenium::WebDriver::Logger. The default level is set
117
+ # to warn. This means you will see any deprecation notices as well as any
118
+ # warning messages. To see details on each element interaction the level
119
+ # can be set to info. To see details on what Watir is doing when it takes a
120
+ # selector hash and converts it into XPath, the level can be set to debug.
121
+ # If you want to ignore specific warnings that are appearing during test
122
+ # execution:
123
+ #
124
+ # Watir.logger.ignore :warning_name
125
+ #
126
+ # If you want to ignore all deprecation warnings in your tests:
127
+ #
128
+ # Watir.logger.ignore :deprecations
129
+ #
130
+ # To have the wire logger generate output to a file:
131
+ #
132
+ # Watir.logger.output = "wire.log"
133
+
134
+ def wire_path=(logdev)
135
+ Watir.logger.reopen(logdev)
136
+ end
137
+
138
+ def wire_level_logging=(value)
139
+ Watir.logger.level = value
140
+ end
141
+
142
+ def wire_level_logging
143
+ %i[DEBUG INFO WARN ERROR FATAL UNKNOWN][Watir.logger.level]
144
+ end
145
+
146
+ def watir_api
147
+ browser.methods - Object.public_methods -
148
+ Watir::Container.instance_methods
149
+ end
150
+
151
+ def selenium_api
152
+ browser.driver.methods - Object.public_methods
153
+ end
154
+
155
+ # This accessor is needed so that Testable itself can provide a browser
156
+ # reference to indicate connection to WebDriver. This is a class-level
157
+ # access to the browser.
32
158
  attr_accessor :browser
33
159
 
34
160
  def set_browser(app = :chrome, *args)
@@ -36,8 +162,14 @@ module Testable
36
162
  Testable.browser = @browser
37
163
  end
38
164
 
165
+ alias start_browser set_browser
166
+
39
167
  def quit_browser
40
168
  @browser.quit
41
169
  end
170
+
171
+ def api
172
+ methods - Object.public_methods
173
+ end
42
174
  end
43
175
  end
@@ -0,0 +1,38 @@
1
+ require "testable/situation"
2
+
3
+ module Testable
4
+ module Pages
5
+ module Attribute
6
+ include Situation
7
+
8
+ def url_is(url = nil)
9
+ url_is_empty if url.nil? && url_attribute.nil?
10
+ url_is_empty if url.nil? || url.empty?
11
+ @url = url
12
+ end
13
+
14
+ def url_attribute
15
+ @url
16
+ end
17
+
18
+ def url_matches(pattern = nil)
19
+ url_match_is_empty if pattern.nil?
20
+ url_match_is_empty if pattern.is_a?(String) && pattern.empty?
21
+ @url_match = pattern
22
+ end
23
+
24
+ def url_match_attribute
25
+ @url_match
26
+ end
27
+
28
+ def title_is(title = nil)
29
+ title_is_empty if title.nil? || title.empty?
30
+ @title = title
31
+ end
32
+
33
+ def title_attribute
34
+ @title
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ module Testable
2
+ module DSL
3
+ # The DSL module is mixed into the Node class to provide the DSL for
4
+ # defining elements and components.
5
+ def self.included(caller)
6
+ caller.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # The ClassMethods provide a set of macro-like methods for wrapping
11
+ # HTML fragments in Node objects.
12
+
13
+ # Defines an element that wraps an HTML fragment.
14
+ def element(name, selector, options = {})
15
+ define_method(name.to_s) do
16
+ Node.new(node: @node.find(selector, options))
17
+ end
18
+
19
+ define_helpers(name, selector)
20
+ end
21
+
22
+ # Defines a collection of elements that wrap HTML fragments.
23
+ def elements(name, selector, options = {})
24
+ options = { minimum: 1 }.merge(options)
25
+
26
+ define_method(name.to_s) do
27
+ @node.all(selector, options).map do |node|
28
+ Node.new(node: node)
29
+ end
30
+ end
31
+
32
+ define_helpers(name, selector)
33
+ end
34
+
35
+ # Defines a component that wraps an HTML fragment.
36
+ def component(name, klass, selector, options = {})
37
+ unless klass < Node
38
+ raise ArgumentError, 'Must be given a subclass of Node'
39
+ end
40
+
41
+ define_method(name.to_s) do
42
+ klass.new(node: @node.find(selector, options))
43
+ end
44
+
45
+ define_helpers(name, selector)
46
+ end
47
+
48
+ # Defines a collection of components that wrap HTML fragments.
49
+ def components(name, klass, selector, options = {})
50
+ unless klass < Node
51
+ raise ArgumentError, 'Must be given a subclass of Node'
52
+ end
53
+
54
+ options = { minimum: 1 }.merge(options)
55
+
56
+ define_method(name.to_s) do
57
+ @node.all(selector, options).map do |node|
58
+ klass.new(node: node)
59
+ end
60
+ end
61
+
62
+ define_helpers(name, selector)
63
+ end
64
+
65
+ private
66
+
67
+ def define_helpers(name, selector)
68
+ define_existence_predicates(name, selector)
69
+ end
70
+
71
+ def define_existence_predicates(name, selector)
72
+ define_method("has_#{name}?") do
73
+ @node.has_selector?(selector)
74
+ end
75
+
76
+ define_method("has_no_#{name}?") do
77
+ @node.has_no_selector?(selector)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ require "testable/capybara/dsl"
2
+
3
+ module Testable
4
+ class Node
5
+ # The Node class represents a wrapped HTML page or fragment. It exposes all
6
+ # methods of the Cogent DSL, making sure that any Capybara API methods
7
+ # are passed to the node instance.
8
+ include DSL
9
+
10
+ attr_reader :node
11
+
12
+ # A Capybara node is being wrapped in a node instance.
13
+ def initialize(node:)
14
+ @node = node
15
+ end
16
+
17
+ # Any Capybara API calls will be sent to the node object.
18
+ def method_missing(name, *args, &block)
19
+ if @node.respond_to?(name)
20
+ @node.send(name, *args, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(name, include_private = false)
27
+ @node.respond_to?(name) || super
28
+ end
29
+ end
30
+ end