gamera 0.1.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.
@@ -0,0 +1,32 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2015, The Gamera Development Team. See the COPYRIGHT file at
6
+ # the top-level directory of this distribution and at
7
+ # http://github.com/gamera-team/gamera/COPYRIGHT.
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #++
27
+
28
+ module Gamera
29
+ class NoUrlMatcherForPage < StandardError; end
30
+ class DatabaseNotConfigured < StandardError; end
31
+ class WrongPageVisited < StandardError; end
32
+ end
@@ -0,0 +1,83 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2015, The Gamera Development Team. See the COPYRIGHT file at
6
+ # the top-level directory of this distribution and at
7
+ # http://github.com/gamera-team/gamera/COPYRIGHT.
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #++
27
+
28
+ module Gamera
29
+ # This implements a specific sub-pattern of the proxy pattern. Rather than
30
+ # knowing about a specific class's methods, it will add a singleton method to
31
+ # a given object for each method defined by that method's class or the
32
+ # method's class and up through a specified class in the ancestor chain.
33
+ #
34
+ # Important Note: This module must be *prepended* rather than *included* for
35
+ # +self+ to refer to the class containing the module. If the proxying isn't
36
+ # happening, this is likely the problem.
37
+ #
38
+ # Usage example:
39
+ # if you are testing a class +Foo+ with Capybara and you'd like to take a
40
+ # screenshot everytime a method in that class is called
41
+ # class Foo
42
+ # prepend Gamera::GeneralProxy
43
+ #
44
+ # def my_method
45
+ # # do something interesting in a browser
46
+ # end
47
+ #
48
+ # def my_other_method
49
+ # # do something else interesting in a browser
50
+ # end
51
+ # end
52
+ #
53
+ # In the spec file
54
+ #
55
+ # describe Foo do
56
+ # let(:foo) { Foo.new }
57
+ # it "does something"
58
+ # foo.start_proxying(->(*args)
59
+ # {Capybara::Screenshot.screenshot_and_save_page
60
+ # super(*args)})
61
+ # foo.my_method # => screenshot taken & method called
62
+ # foo.my_other_method # => screenshot taken & method called
63
+ # foo.stop_proxying
64
+ # foo.my_method # => *crickets* (aka method called)
65
+ # foo.my_other_method # => *crickets*
66
+ # ...
67
+ module GeneralProxy
68
+ def start_proxying(a_lambda = ->(*args) { super(*args) }, top_class = self.class)
69
+ @top_class = top_class
70
+ ancestors = self.class.ancestors
71
+ proxy_target_classes = ancestors[1..ancestors.index(top_class)]
72
+ proxy_target_classes.each do |klass|
73
+ klass.instance_methods(false).each do |method|
74
+ define_singleton_method(method, a_lambda)
75
+ end
76
+ end
77
+ end
78
+
79
+ def stop_proxying
80
+ start_proxying(-> (*args) { super(*args) }, @top_class)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,203 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2015, The Gamera Development Team. See the COPYRIGHT file at
6
+ # the top-level directory of this distribution and at
7
+ # http://github.com/gamera-team/gamera/COPYRIGHT.
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #++
27
+
28
+ require 'capybara'
29
+ require_relative 'exceptions'
30
+
31
+ module Gamera
32
+ # This is a base class which implements common methods for page object
33
+ # classes.
34
+ #
35
+ # You can use this to create a Ruby class which wraps a web page, providing
36
+ # an API for automating elements or processes on the page
37
+ #
38
+ # @example Page Object class for registration page
39
+ # class NewRegistrationPage < Gamera::Page
40
+ # def initialize
41
+ # @url = 'http://example.com/registration/new'
42
+ # @url_matcher = /registration\/new/
43
+ # end
44
+ #
45
+ # # page elements
46
+ # def name_field
47
+ # find_field('Name')
48
+ # end
49
+ #
50
+ # def email_field
51
+ # find_field('Email Address')
52
+ # end
53
+ #
54
+ # def password_field
55
+ # find_field('Password')
56
+ # end
57
+ #
58
+ # def password_confirmation_field
59
+ # find_field('Confirm Password')
60
+ # end
61
+ #
62
+ # def instructions
63
+ # def instructions
64
+ # find('#instructions')
65
+ # end
66
+ #
67
+ # # page processes
68
+ # def save
69
+ # find_button('Save').click
70
+ # end
71
+ #
72
+ # def register_user(name:, email:, password:)
73
+ # name_field.set(name)
74
+ # email_field.set(email)
75
+ # password_field.set(password)
76
+ # password_confirmation_field.set(password)
77
+ # save
78
+ # end
79
+ # end
80
+ #
81
+ # # This could be used in a test or automation script, e.g.
82
+ # ...
83
+ # reg_page = NewRegistrationPage.new
84
+ # reg_page.visit
85
+ # reg_page.register_user(name: 'Laurence Peltier',
86
+ # email: 'lpeltier@example.com',
87
+ # password: 'super_secret')
88
+ # ...
89
+ #
90
+ # @example Page class for general Rails page with flash messages
91
+ # class RailsPage < Gamera::Page
92
+ # def flash_error_css
93
+ # 'div.flash.error'
94
+ # end
95
+ #
96
+ # def flash_notice_css
97
+ # 'div.flash.notice'
98
+ # end
99
+ #
100
+ # def flash_error
101
+ # find(flash_error_css)
102
+ # end
103
+ #
104
+ # def flash_notice
105
+ # find(flash_notice_css)
106
+ # end
107
+ #
108
+ # def has_flash_message?(message)
109
+ # has_css?(flash_notice_css, text: message)
110
+ # end
111
+ #
112
+ # def has_flash_error?(error)
113
+ # has_css?(flash_error_css, text: error)
114
+ # end
115
+ #
116
+ # def has_no_flash_error?
117
+ # has_no_css?(flash_error_css)
118
+ # end
119
+ #
120
+ # def has_no_flash_message?
121
+ # has_no_css?(flash_notice_css)
122
+ # end
123
+ #
124
+ # def has_submission_problems?
125
+ # has_flash_error?('There were problems with your submission')
126
+ # end
127
+ #
128
+ # def fail_if_submission_problems
129
+ # fail(SubmissionProblemsError, flash_error.text) if has_submission_problems?
130
+ # end
131
+ # end
132
+ class Page
133
+ include Capybara::DSL
134
+
135
+ attr_reader :url, :url_matcher
136
+
137
+ def initialize(url, url_matcher = nil)
138
+ @url = url
139
+ @url_matcher = url_matcher
140
+ end
141
+
142
+ # Open the page url in the browser specified in your Capybara configuration
143
+ #
144
+ # @raise [WrongPageVisited] if the site redirects to URL that doesn't match
145
+ # the url_matcher regex
146
+ def visit
147
+ super(url)
148
+ fail WrongPageVisited, "Expected URL '#{url}', got '#{page.current_url}'" unless displayed?
149
+ end
150
+
151
+ # Check to see if we eventually land on the right page
152
+ #
153
+ # @param wait_time_seconds [Integer] How long to wait for the correct page to load
154
+ # @return [Boolean] true if the url loaded in the browser matches the url_matcher pattern
155
+ # @raise [NoUrlMatcherForPage] if there's no url_matcher for this page
156
+ def displayed?(wait_time_seconds = Capybara.default_wait_time)
157
+ fail Gamera::NoUrlMatcherForPage if url_matcher.nil?
158
+ start_time = Time.now
159
+ loop do
160
+ return true if page.current_url.chomp('/') =~ url_matcher
161
+ break unless Time.now - start_time <= wait_time_seconds
162
+ sleep(0.05)
163
+ end
164
+ false
165
+ end
166
+
167
+ # A method to wait for slow loading data on a page. Useful, for example,
168
+ # when waiting on a page that shows the count of records loaded via a slow
169
+ # web or import.
170
+ #
171
+ # @param retries [Integer] Number of times to reload the page before giving up
172
+ # @param allowed_errors [Array] Array of errors that trigger a refresh, e.g., if an ExpectationNotMetError was raised during an acceptance test
173
+ # @param block [Block] The block to execute after each refresh
174
+ def with_refreshes(retries, allowed_errors = [RSpec::Expectations::ExpectationNotMetError], &block)
175
+ retries_left ||= retries
176
+ visit
177
+ block.call(retries_left)
178
+ rescue *allowed_errors => e
179
+ retries_left -= 1
180
+ retry if retries_left >= 0
181
+ raise e
182
+ end
183
+
184
+ # This is a flag for tracking which page object classes don't cover all of
185
+ # the elements and/or controls on the target web page.
186
+ #
187
+ # @return [Boolean] true unless everything's been captured in the page
188
+ # object class
189
+ def sparse?
190
+ false
191
+ end
192
+
193
+ # This is a utility method to clean up URLs formed by concatenation since we
194
+ # sometimes ended up with "//" in the middle of URLs which broke the
195
+ # url_matcher checks.
196
+ #
197
+ # @param elements [String] duck types
198
+ # @return [String] of elements joined by single "/" characters.
199
+ def path_join(*elements)
200
+ "/#{elements.join('/')}".gsub(%r(//+}), '/')
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,150 @@
1
+ # encoding:utf-8
2
+ #--
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2015, The Gamera Development Team. See the COPYRIGHT file at
6
+ # the top-level directory of this distribution and at
7
+ # http://github.com/gamera-team/gamera/COPYRIGHT.
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+ #++
27
+
28
+ require 'capybara'
29
+
30
+ module Gamera
31
+ module PageSections
32
+ # This class represents an html form on a web page. For example, if you had
33
+ # a page like
34
+ #
35
+ # <html>
36
+ # <body>
37
+ # <h2>Example form</h2>
38
+ # <form action='/user/register'>
39
+ # <label for="name">Name</label><input type="text" name="name" value="" id="name">
40
+ # <label for="email">Email</label><input type="text" name="email" value="" id="email">
41
+ # <label for="password">Password</label><input type="text" name="password" value="" id="password">
42
+ #
43
+ # <input type="button" name="Register" id="save_button">
44
+ # <input type="button" name="Cancel" id="cancel_button">
45
+ # </form>
46
+ # </body>
47
+ # </html>
48
+ #
49
+ # you could include this in a page object class like so:
50
+ #
51
+ # class RegistrationPage < Gamera::Page
52
+ # include Forwardable
53
+ #
54
+ # attr_reader :registration_form, :table
55
+ #
56
+ # def initialize
57
+ # super(path_join(BASE_URL, '/registration/new'), %r{registration/new$})
58
+ #
59
+ # form_fields = {
60
+ # name: 'Name',
61
+ # email: 'Email',
62
+ # password: 'Password'
63
+ # }
64
+ #
65
+ # @registration_form = Gamera::PageSections::Form.new(form_fields)
66
+ # def_delegators :registration_form, *registration_form.field_method_names
67
+ # end
68
+ #
69
+ # def register
70
+ # find_button('Register').click
71
+ # end
72
+ #
73
+ # def cancel
74
+ # find_button('Cancel').click
75
+ # end
76
+ # end
77
+ #
78
+ class Form
79
+ include Capybara::DSL
80
+
81
+ attr_accessor :fields, :field_method_names
82
+
83
+ def initialize(fields)
84
+ @fields = fields
85
+ @field_method_names = []
86
+ define_field_methods
87
+ end
88
+
89
+ # Utility method to populate the form based on a hash of field names and
90
+ # values
91
+ #
92
+ # @param fields [Hash] The keys are the [field_name]s and the values are the values to which the fields are to be set.
93
+ def fill_in_form(fields)
94
+ fields.each do |field, value|
95
+ f = send("#{field}_field")
96
+ f.set(value.to_s) || Array(value).each { |val| f.select(val) }
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Creates methods for the specified form fields of the form "<field_name>_field"
103
+ # (based on the results of [define_field_name]) that can be called to
104
+ # interact with form controls on the web page
105
+ def define_field_methods
106
+ if fields.is_a?(Array)
107
+ fields.each do |field_label|
108
+ field = field_label.downcase.gsub(' ', '_').gsub(/\W/, '').to_sym
109
+ field_method_name = define_field_name(field)
110
+ define_field_method(field_method_name, field_label)
111
+ end
112
+ elsif fields.is_a?(Hash)
113
+ fields.each do |field, field_string|
114
+ field_method_name = define_field_name(field)
115
+ define_field_method(field_method_name, field_string)
116
+ end
117
+ end
118
+ end
119
+
120
+ # converts the provided field string into a suitable method name for
121
+ # [define_field_methods] to use
122
+ #
123
+ # @param field [String] The user-readable name of a control on an html form
124
+ def define_field_name(field)
125
+ (field.to_s + '_field').to_sym.tap do |field_method_name|
126
+ field_method_names << field_method_name
127
+ end
128
+ end
129
+
130
+ # Defines an instance method named <field_method_name> for a given field
131
+ #
132
+ # @param field_method_name [String] Ruby-syntax-friendly name for the method being defined
133
+ # @param field_string [String] The user-readable name or selector for the html form control
134
+ def define_field_method(field_method_name, field_string)
135
+ field_string = field_string.chomp(':')
136
+ self.class.send(:define_method, field_method_name) do
137
+ label_before_field_xpath = "//label[contains(., '#{field_string}')]/following-sibling::*[local-name() = 'input' or local-name() = 'textarea' or local-name() = 'select'][1]"
138
+ label_after_field_xpath = "//label[contains(., '#{field_string}')]/preceding-sibling::*[local-name() = 'input' or local-name() = 'textarea' or local-name() = 'select'][1]"
139
+ if has_selector?(:field, field_string)
140
+ find_field(field_string)
141
+ elsif has_xpath?(label_before_field_xpath)
142
+ find(:xpath, label_before_field_xpath)
143
+ else
144
+ find(:xpath, label_after_field_xpath)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end