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.
- 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
|