gamera 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/gamera.rb +9 -0
- data/lib/gamera/builder.rb +244 -0
- data/lib/gamera/builders/sequel_fixture_builder.rb +249 -0
- data/lib/gamera/exceptions.rb +32 -0
- data/lib/gamera/general_proxy.rb +83 -0
- data/lib/gamera/page.rb +203 -0
- data/lib/gamera/page_sections/form.rb +150 -0
- data/lib/gamera/page_sections/table.rb +231 -0
- data/lib/gamera/utils/database_cleaner.rb +75 -0
- data/lib/pry_setup.rb +34 -0
- metadata +259 -0
@@ -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
|
data/lib/gamera/page.rb
ADDED
@@ -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
|