ae_page_objects 0.0.1.beta.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.
- data/lib/ae_page_objects.rb +57 -0
- data/lib/ae_page_objects/concerns/load_ensuring.rb +30 -0
- data/lib/ae_page_objects/concerns/staleable.rb +29 -0
- data/lib/ae_page_objects/concerns/visitable.rb +61 -0
- data/lib/ae_page_objects/core/application.rb +85 -0
- data/lib/ae_page_objects/core/application_router.rb +60 -0
- data/lib/ae_page_objects/core/configurable.rb +27 -0
- data/lib/ae_page_objects/core/configuration.rb +33 -0
- data/lib/ae_page_objects/core/constant_resolver.rb +33 -0
- data/lib/ae_page_objects/core/dependencies_hook.rb +21 -0
- data/lib/ae_page_objects/core/dsl/collection.rb +135 -0
- data/lib/ae_page_objects/core/dsl/element.rb +45 -0
- data/lib/ae_page_objects/core/dsl/form_for.rb +30 -0
- data/lib/ae_page_objects/core/dsl/nested_element.rb +24 -0
- data/lib/ae_page_objects/core/internal_helpers.rb +9 -0
- data/lib/ae_page_objects/core/rake_router.rb +148 -0
- data/lib/ae_page_objects/document.rb +19 -0
- data/lib/ae_page_objects/element.rb +86 -0
- data/lib/ae_page_objects/element_proxy.rb +72 -0
- data/lib/ae_page_objects/elements/checkbox.rb +19 -0
- data/lib/ae_page_objects/elements/collection.rb +61 -0
- data/lib/ae_page_objects/elements/form.rb +10 -0
- data/lib/ae_page_objects/elements/has_one.rb +11 -0
- data/lib/ae_page_objects/elements/select.rb +7 -0
- data/lib/ae_page_objects/node.rb +73 -0
- data/lib/ae_page_objects/version.rb +3 -0
- metadata +184 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
module Dsl
|
3
|
+
module Collection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dsl::Element
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
include InternalHelpers
|
9
|
+
|
10
|
+
# Defines a collection of elements. Blocks are evaluated on the item class used by the
|
11
|
+
# collection. collection() defines a method on the class that returns an instance of a collection
|
12
|
+
# class which contains instances of the collection's item class.
|
13
|
+
#
|
14
|
+
# Supported signatures are described below.
|
15
|
+
#
|
16
|
+
# ------------------------------------------------
|
17
|
+
# Signature: (:is, no :contains, no block)
|
18
|
+
#
|
19
|
+
# collection :addresses, :is => AddressList
|
20
|
+
#
|
21
|
+
# Collection class: AddressList
|
22
|
+
# Item class: AddressList.item_class
|
23
|
+
#
|
24
|
+
# ------------------------------------------------
|
25
|
+
# Signature: (no :is, :contains, no block)
|
26
|
+
#
|
27
|
+
# collection :addresses, :contains => Address
|
28
|
+
#
|
29
|
+
# Collection class: one-off subclass of ::AePageObjects::Collection
|
30
|
+
# Item class: Address
|
31
|
+
#
|
32
|
+
# ------------------------------------------------
|
33
|
+
# Signature: (:is, :contains, no block)
|
34
|
+
#
|
35
|
+
# collection :addresses, :is => AddressList, :contains => ExtendedAddress
|
36
|
+
#
|
37
|
+
# Collection class: one-off subclass ofAddressList
|
38
|
+
# Item class: ExtendedAddress
|
39
|
+
#
|
40
|
+
# ------------------------------------------------
|
41
|
+
# Signature: (no :is, no :contains, block)
|
42
|
+
#
|
43
|
+
# collection :addresses do
|
44
|
+
# element :city
|
45
|
+
# element :state
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# Collection class: one-off subclass of ::AePageObjects::Collection
|
49
|
+
# Item class: one-off subclass of ::AePageObjects::Element
|
50
|
+
# Methods defined on item class:
|
51
|
+
# city() # -> instance of ::AePageObjects::Element
|
52
|
+
# state() # -> instance of ::AePageObjects::Element
|
53
|
+
#
|
54
|
+
# ------------------------------------------------
|
55
|
+
# Signature: (:is, no :contains, block)
|
56
|
+
#
|
57
|
+
# collection :addresses, :is => AddressList do
|
58
|
+
# element :longitude
|
59
|
+
# element :latitude
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# Collection class: one-off subclass of AddressList
|
63
|
+
# Item class: one-off subclass of AddressList.item_class
|
64
|
+
# Methods defined on item class:
|
65
|
+
# longitude() # -> instance of ::AePageObjects::Element
|
66
|
+
# latitude() # -> instance of ::AePageObjects::Element
|
67
|
+
#
|
68
|
+
# ------------------------------------------------
|
69
|
+
# Signature: (no :is, :contains, block)
|
70
|
+
#
|
71
|
+
# collection :addresses, :contains => Address do
|
72
|
+
# element :longitude
|
73
|
+
# element :latitude
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Collection class: one-off subclass of ::AePageObjects::Collection element
|
77
|
+
# Item class: one-off subclass of Address
|
78
|
+
# Methods defined on item class:
|
79
|
+
# longitude() # -> instance of ::AePageObjects::Element
|
80
|
+
# latitude() # -> instance of ::AePageObjects::Element
|
81
|
+
#
|
82
|
+
# ------------------------------------------------
|
83
|
+
# Signature: (:is, :contains, block)
|
84
|
+
#
|
85
|
+
# collection :addresses, :is => AddressList, :contains => Address do
|
86
|
+
# element :longitude
|
87
|
+
# element :latitude
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# Collection class: one-off subclass of AddressList
|
91
|
+
# Item class: one-off subclass of Address
|
92
|
+
# Methods defined on item class:
|
93
|
+
# longitude() # -> instance of ::AePageObjects::Element
|
94
|
+
# latitude() # -> instance of ::AePageObjects::Element
|
95
|
+
#
|
96
|
+
def collection(name, options = {}, &block)
|
97
|
+
options ||= {}
|
98
|
+
|
99
|
+
# only a collection class is specified or the item class
|
100
|
+
# specified matches the collection's item class
|
101
|
+
if block.blank? && options[:is] && (
|
102
|
+
options[:contains].blank? || options[:is].item_class == options[:contains]
|
103
|
+
)
|
104
|
+
return element(name, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
options = options.dup
|
108
|
+
|
109
|
+
# create/get the collection class
|
110
|
+
if options[:is]
|
111
|
+
ensure_class_for_param!(:is, options[:is], ::AePageObjects::Collection)
|
112
|
+
else
|
113
|
+
options[:is] = ::AePageObjects::Collection
|
114
|
+
|
115
|
+
raise ArgumentError, "Must specify either a block or a :contains option." if options[:contains].blank? && block.blank?
|
116
|
+
end
|
117
|
+
|
118
|
+
item_class = options.delete(:contains) || options[:is].item_class
|
119
|
+
if block.present?
|
120
|
+
item_class = item_class.new_subclass(&block).tap do |new_item_class|
|
121
|
+
new_item_class.element_attributes.merge!(item_class.element_attributes)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# since we are creating a new item class, we need to subclass the collection class
|
126
|
+
# so we can parameterize the collection class with an item class
|
127
|
+
options[:is] = options[:is].new_subclass
|
128
|
+
options[:is].item_class = item_class
|
129
|
+
|
130
|
+
element(name, options)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
module Dsl
|
3
|
+
module Element
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
def inherited(subclass)
|
9
|
+
subclass.class_eval do
|
10
|
+
class << self
|
11
|
+
def element_attributes
|
12
|
+
@element_attributes ||= {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def element(name, options = {})
|
19
|
+
options = options.dup
|
20
|
+
klass = field_klass(options)
|
21
|
+
|
22
|
+
self.element_attributes[name.to_sym] = klass
|
23
|
+
|
24
|
+
define_method name do |&block|
|
25
|
+
ElementProxy.new(klass, self, name, options, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
klass
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def field_klass(options)
|
34
|
+
field_type = options.delete(:is)
|
35
|
+
|
36
|
+
if field_type.is_a? Class
|
37
|
+
field_type
|
38
|
+
else
|
39
|
+
::AePageObjects::Element
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
module Dsl
|
3
|
+
module FormFor
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dsl::Element
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def form_for(form_name, options = {}, &block)
|
10
|
+
options ||= {}
|
11
|
+
|
12
|
+
raise ArgumentError, ":is option not supported" if options[:is]
|
13
|
+
raise ArgumentError, "Block required." unless block.present?
|
14
|
+
|
15
|
+
klass = ::AePageObjects::Form.new_subclass(&block)
|
16
|
+
|
17
|
+
options = options.dup
|
18
|
+
options[:is] = klass
|
19
|
+
|
20
|
+
element(form_name, options)
|
21
|
+
|
22
|
+
klass.element_attributes.each do |node_name, node_klazz|
|
23
|
+
delegate node_name, :to => form_name
|
24
|
+
self.element_attributes[node_name] = node_klazz
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
module Dsl
|
3
|
+
module NestedElement
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dsl::Element
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def element(name, options = {}, &block)
|
10
|
+
raise ArgumentError, ":is option and block not supported together" if options[:is].present? && block_given?
|
11
|
+
|
12
|
+
if block_given?
|
13
|
+
klass = ::AePageObjects::HasOne.new_subclass(&block)
|
14
|
+
|
15
|
+
options = options.dup
|
16
|
+
options[:is] = klass
|
17
|
+
end
|
18
|
+
|
19
|
+
super(name, options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
module InternalHelpers
|
3
|
+
def ensure_class_for_param!(param_name, klass, ancestor_class)
|
4
|
+
if klass && ! (klass < ancestor_class)
|
5
|
+
raise "#{param_name} <#{klass}> must extend #{ancestor_class}, ->#{klass.ancestors.inspect}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
class RakeRouter
|
3
|
+
|
4
|
+
attr_reader :routes
|
5
|
+
|
6
|
+
def initialize(rake_routes, mounted_prefix = '')
|
7
|
+
@mounted_prefix = mounted_prefix || ""
|
8
|
+
@routes = ActiveSupport::OrderedHash.new
|
9
|
+
route_line_regex = /(\w+)(?:\s[A-Z]+)?\s+(\/.*)\(.:format\).*$/
|
10
|
+
|
11
|
+
rake_routes.split("\n").each do |line|
|
12
|
+
line = line.strip
|
13
|
+
matches = route_line_regex.match(line)
|
14
|
+
if matches
|
15
|
+
@routes[matches[1].to_sym] = Route.new(matches[2], @mounted_prefix)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def path_recognizes_url?(path, url)
|
21
|
+
if path.is_a?(String)
|
22
|
+
path.sub(/\/$/, '') == url.sub(/\/$/, '')
|
23
|
+
elsif path.is_a?(Symbol)
|
24
|
+
route = @routes[path]
|
25
|
+
route && route.matches?(url)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_path(named_route, *args)
|
30
|
+
if named_route.is_a?(String)
|
31
|
+
return Path.new(@mounted_prefix + named_route)
|
32
|
+
end
|
33
|
+
|
34
|
+
if route = @routes[named_route]
|
35
|
+
options = args.extract_options!
|
36
|
+
route.generate_path(options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
class Path < String
|
43
|
+
attr_reader :params, :regex
|
44
|
+
|
45
|
+
def initialize(value)
|
46
|
+
super(value.gsub(/(\/)+/, '/').sub(/\(\.\:format\)$/, ''))
|
47
|
+
|
48
|
+
@params = parse_params
|
49
|
+
@regex = generate_regex
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate(param_values)
|
53
|
+
param_values = param_values.symbolize_keys
|
54
|
+
@params.values.inject(self) do |path, param|
|
55
|
+
param.substitute(path, param_values)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def parse_params
|
61
|
+
# overwrite the required status with the optional
|
62
|
+
{}.merge(required_params).merge(optional_params)
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_params(using_regex)
|
66
|
+
scan(using_regex).flatten.map(&:to_sym)
|
67
|
+
end
|
68
|
+
|
69
|
+
def optional_params
|
70
|
+
{}.tap do |optional_params|
|
71
|
+
find_params(/\(\/\:(\w+)\)/).each do |param_name|
|
72
|
+
optional_params[param_name] = Param.new(param_name, true)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def required_params
|
78
|
+
{}.tap do |required_params|
|
79
|
+
find_params(/\:(\w+)/).each do |param_name|
|
80
|
+
required_params[param_name] = Param.new(param_name, false)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def generate_regex
|
86
|
+
regex_spec = @params.values.inject(self) do |regex_spec, param|
|
87
|
+
param.replace_param_in_url(regex_spec)
|
88
|
+
end
|
89
|
+
Regexp.new regex_spec
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Param < Struct.new(:name, :optional)
|
94
|
+
include Comparable
|
95
|
+
|
96
|
+
alias :optional? :optional
|
97
|
+
|
98
|
+
def <=>(other)
|
99
|
+
name.to_s <=> other.name.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
def eql?(other)
|
103
|
+
name == other.name
|
104
|
+
end
|
105
|
+
|
106
|
+
def hash
|
107
|
+
name.hash
|
108
|
+
end
|
109
|
+
|
110
|
+
def replace_param_in_url(url)
|
111
|
+
if optional?
|
112
|
+
url.gsub("(/:#{name})", '(\/.+)?')
|
113
|
+
else
|
114
|
+
url.gsub(":#{name}", '(.+)')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def substitute(url, values)
|
119
|
+
if optional?
|
120
|
+
if values[name]
|
121
|
+
url.sub("(/:#{name})", "/#{values[name]}")
|
122
|
+
else
|
123
|
+
url.sub("(/:#{name})", '')
|
124
|
+
end
|
125
|
+
else
|
126
|
+
raise ArgumentError, "Missing required parameter '#{name}' for '#{url}' in #{values.inspect}" unless values.key? name
|
127
|
+
url.sub(":#{name}", values[name])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Route
|
133
|
+
def initialize(spec, mounted_prefix)
|
134
|
+
@path = Path.new(mounted_prefix + spec)
|
135
|
+
@path.freeze
|
136
|
+
end
|
137
|
+
|
138
|
+
def matches?(url)
|
139
|
+
url =~ @path.regex
|
140
|
+
end
|
141
|
+
|
142
|
+
def generate_path(options)
|
143
|
+
options = options.symbolize_keys
|
144
|
+
@path.generate(options)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
class Document < Node
|
3
|
+
include Concerns::Visitable
|
4
|
+
|
5
|
+
def document
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
private
|
11
|
+
def application
|
12
|
+
@application ||= begin
|
13
|
+
universe = AePageObjects::DependenciesHook.containing_page_object_universe(self)
|
14
|
+
"#{universe.name}::Application".constantize.instance
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module AePageObjects
|
2
|
+
class Element < Node
|
3
|
+
attr_reader :parent, :default_name
|
4
|
+
|
5
|
+
def initialize(parent, name, options_or_locator = {})
|
6
|
+
@parent = parent
|
7
|
+
@default_name = name.to_s
|
8
|
+
@locator = nil
|
9
|
+
@name = nil
|
10
|
+
|
11
|
+
configure(parse_options(options_or_locator))
|
12
|
+
|
13
|
+
@locator ||= default_locator
|
14
|
+
|
15
|
+
super(scoped_node)
|
16
|
+
end
|
17
|
+
|
18
|
+
def document
|
19
|
+
@document ||= begin
|
20
|
+
node = self.parent
|
21
|
+
|
22
|
+
until node.is_a?(::AePageObjects::Document)
|
23
|
+
node = node.parent
|
24
|
+
end
|
25
|
+
|
26
|
+
node
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def __full_name__
|
31
|
+
if parent.respond_to?(:__full_name__)
|
32
|
+
"#{parent.__full_name__}_#{__name__}"
|
33
|
+
else
|
34
|
+
__name__
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :full_name, :__full_name__
|
39
|
+
|
40
|
+
def __name__
|
41
|
+
@name || default_name
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :name, :__name__
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
super.tap do |str|
|
48
|
+
str << "#name:<#{__name__}>"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def using_default_locator?
|
53
|
+
@locator == default_locator
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def configure(options)
|
59
|
+
@locator = options.delete(:locator)
|
60
|
+
@name = options.delete(:name).try(:to_s)
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_options(options_or_locator)
|
64
|
+
if options_or_locator.is_a?( Hash )
|
65
|
+
options_or_locator.symbolize_keys
|
66
|
+
else
|
67
|
+
{:locator => options_or_locator}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_locator
|
72
|
+
@default_locator ||= Proc.new { "##{__full_name__}" }
|
73
|
+
end
|
74
|
+
|
75
|
+
def scoped_node
|
76
|
+
if @locator
|
77
|
+
locator = eval_locator(@locator)
|
78
|
+
if locator.present?
|
79
|
+
return parent.find(*locator)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
parent.node
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|