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