gamera 0.1.0

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