testable 0.3.0 → 0.8.0

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