page_record 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rubocop.yml +13 -0
- data/CHANGES.md +5 -0
- data/Gemfile.lock +46 -14
- data/Guardfile +24 -0
- data/README.md +27 -1
- data/bin/autospec +16 -0
- data/bin/guard +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/lib/page_record/attribute_accessors.rb +57 -54
- data/lib/page_record/base.rb +27 -22
- data/lib/page_record/class_actions.rb +47 -50
- data/lib/page_record/class_methods.rb +252 -261
- data/lib/page_record/errors.rb +81 -82
- data/lib/page_record/finders.rb +129 -131
- data/lib/page_record/form_builder.rb +4 -4
- data/lib/page_record/formtastic.rb +20 -9
- data/lib/page_record/helpers.rb +192 -131
- data/lib/page_record/inspector.rb +36 -0
- data/lib/page_record/instance_actions.rb +44 -46
- data/lib/page_record/rspec.rb +1 -1
- data/lib/page_record/validation.rb +46 -0
- data/lib/page_record/version.rb +1 -1
- data/lib/page_record.rb +13 -15
- data/page_record.gemspec +4 -1
- data/spec/.rubocop.yml +4 -0
- data/spec/helpers_spec.rb +109 -100
- data/spec/inspector_spec.rb +70 -0
- data/spec/page_record_spec.rb +357 -388
- data/spec/spec_helper.rb +1 -3
- data/spec/support/shared_contexts.rb +24 -2
- data/spec/support/shared_examples.rb +41 -45
- data/spec/support/team.rb +4 -4
- data/spec/support/test_app.rb +10 -13
- data/spec/support/views/page-with-1-error.erb +5 -0
- data/spec/support/views/page-with-2-errors-on-different-attributes.erb +9 -0
- data/spec/support/views/page-with-2-errors-on-same-attribute.erb +6 -0
- data/spec/support/views/page-without-errors.erb +4 -0
- data/spec/validation_spec.rb +142 -0
- data/tmp/rspec_guard_result +1 -0
- metadata +80 -5
data/lib/page_record/errors.rb
CHANGED
@@ -1,88 +1,87 @@
|
|
1
1
|
module PageRecord
|
2
2
|
|
3
|
+
##
|
4
|
+
# This {::Exception} is raised when the specified record is not found
|
5
|
+
# on the page. Check your selector, filter and HTML code for details.
|
6
|
+
#
|
7
|
+
# ```html
|
8
|
+
# <div data-team-id='10'>
|
9
|
+
# <div data-attribute-for='name'>Ajax</div>
|
10
|
+
# </div>
|
11
|
+
# ```
|
12
|
+
# When the following code is executed, the {RecordNotFound} exception is raised.
|
13
|
+
#
|
14
|
+
# ```ruby
|
15
|
+
# TeamPage.find(11)
|
16
|
+
# ```
|
17
|
+
#
|
18
|
+
class RecordNotFound < Exception
|
19
|
+
end
|
3
20
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
##
|
22
|
+
# This {::Exception} is raised when the specfied attribute is not found
|
23
|
+
# on the page. Check your selector, filter and HTML code for details.
|
24
|
+
#
|
25
|
+
# ```html
|
26
|
+
# <div data-team-id='10'>
|
27
|
+
# <div data-attribute-for='name'>Ajax</div>
|
28
|
+
# </div>
|
29
|
+
# ```
|
30
|
+
# When the following code is executed, the {AttributeNotFound} exception is raised.
|
31
|
+
#
|
32
|
+
# ```ruby
|
33
|
+
# TeamPage.find(10).ranking
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
class AttributeNotFound < Exception
|
37
|
+
end
|
21
38
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
# <div data-attribute-for='name'>Ajax</div>
|
29
|
-
# </div>
|
30
|
-
# ```
|
31
|
-
# When the following code is executed, the {AttributeNotFound} exception is raised.
|
32
|
-
#
|
33
|
-
# ```ruby
|
34
|
-
# TeamPage.find(10).ranking
|
35
|
-
#```
|
36
|
-
#
|
37
|
-
class AttributeNotFound < Exception
|
38
|
-
end
|
39
|
+
##
|
40
|
+
# This {::Exception} is raised when you have not set the page variable
|
41
|
+
# of the class. Check {PageRecord::Base.page} for details
|
42
|
+
#
|
43
|
+
class PageNotSet < Exception
|
44
|
+
end
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
##
|
47
|
+
# This {::Exception} is raised when the page contains multiple instances
|
48
|
+
# of the specfied record type. Use a selector to narrow the search.
|
49
|
+
#
|
50
|
+
# ```html
|
51
|
+
# <div id='first-table' data-team-id='10'>
|
52
|
+
# <div data-attribute-for='name'>Ajax</div>
|
53
|
+
# </div>
|
54
|
+
# <div id='second-table' data-team-id='10'>
|
55
|
+
# <div data-attribute-for='name'>Ajax</div>
|
56
|
+
# </div>
|
57
|
+
# ```
|
58
|
+
# When the following code is executed, the {MultipleRecords} exception is raised.
|
59
|
+
#
|
60
|
+
# ```ruby
|
61
|
+
# TeamPage.find(10)
|
62
|
+
# ```
|
63
|
+
#
|
64
|
+
# To fix this, use the `#first-table` in the selector
|
65
|
+
#
|
66
|
+
# ```ruby
|
67
|
+
# TeamPage.find(10, '#first-table')
|
68
|
+
# ```
|
69
|
+
#
|
70
|
+
class MultipleRecords < Exception
|
71
|
+
end
|
46
72
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# TeamPage.find(10)
|
63
|
-
#```
|
64
|
-
#
|
65
|
-
# To fix this, use the `#first-table` in the selector
|
66
|
-
#
|
67
|
-
# ```ruby
|
68
|
-
# TeamPage.find(10, '#first-table')
|
69
|
-
#```
|
70
|
-
#
|
71
|
-
class MultipleRecords < Exception
|
72
|
-
end
|
73
|
-
|
74
|
-
# This {::Exception} is raised when you try to set a non input field.
|
75
|
-
#
|
76
|
-
# ```ruby
|
77
|
-
# <div data-team-id=1>
|
78
|
-
# <div data-attribute-for='name'>PSV</div>
|
79
|
-
# </div>
|
80
|
-
# ```
|
81
|
-
# When the following code is executed, the {NotInputField} exception is raised.
|
82
|
-
#
|
83
|
-
# ```ruby
|
84
|
-
# team_on_page.name = 'Ajax'
|
85
|
-
# ```
|
86
|
-
class NotInputField < Exception
|
87
|
-
end
|
88
|
-
end
|
73
|
+
# This {::Exception} is raised when you try to set a non input field.
|
74
|
+
#
|
75
|
+
# ```ruby
|
76
|
+
# <div data-team-id=1>
|
77
|
+
# <div data-attribute-for='name'>PSV</div>
|
78
|
+
# </div>
|
79
|
+
# ```
|
80
|
+
# When the following code is executed, the {NotInputField} exception is raised.
|
81
|
+
#
|
82
|
+
# ```ruby
|
83
|
+
# team_on_page.name = 'Ajax'
|
84
|
+
# ```
|
85
|
+
class NotInputField < Exception
|
86
|
+
end
|
87
|
+
end
|
data/lib/page_record/finders.rb
CHANGED
@@ -1,135 +1,133 @@
|
|
1
1
|
module PageRecord
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
2
|
+
##
|
3
|
+
# PageRecord is a specific sort of {http://assertselenium.com/automation-design-practices/page-object-pattern/ PageObject pattern}. Where a "normal"
|
4
|
+
# {http://assertselenium.com/automation-design-practices/page-object-pattern/ PageObject} tries to make the page accessible with business like functions.
|
5
|
+
# We have taken a different approach. We've noticed that a lot of WebPage are
|
6
|
+
# mainly luxury CRUD pages. This means that almost every page shows one or more
|
7
|
+
# records of a certain type and has the ability to create, read update and delete
|
8
|
+
# one or more records. Sounds familiar? Yes, it is the same as an ActiveRecord
|
9
|
+
# pattern. So we tried to make accessing a page as close as possible to accessing
|
10
|
+
# an ActiveRecord.
|
11
|
+
#
|
12
|
+
# To make this work, however, we need to add extra information to the HTML page.
|
13
|
+
# With HTML5, , you can do this easily. HTML-5 supports the data- attributes on any
|
14
|
+
# tag. We use these tags to identify the records on the page.
|
15
|
+
#
|
16
|
+
#
|
17
|
+
class Base
|
18
|
+
##
|
19
|
+
#
|
20
|
+
# Searches the page and returns an {::Array} of {PageRecord::Base} of instances of
|
21
|
+
# that match the selector and the filter. See {file:README.md#markup markup} for more
|
22
|
+
# details about formatting the page.
|
23
|
+
#
|
24
|
+
# example:
|
25
|
+
#
|
26
|
+
# ```ruby
|
27
|
+
# TeamPage.all
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
# returns all records on the page
|
31
|
+
#
|
32
|
+
# @param selector [String] selector to use for searching the table on the page
|
33
|
+
# @param filter [String] filter to use on the records on the page
|
34
|
+
#
|
35
|
+
# @return [Array Pagerecord] The Array containing instances of [PageRecord::Base]
|
36
|
+
# with records that fit the selector and the filter
|
37
|
+
#
|
38
|
+
# @raise [MultipleRecords] if the page contains more then on set of records
|
39
|
+
# @raise [RecordNotFound] if the page does not contain any of the specified records
|
40
|
+
#
|
41
|
+
def self.all(selector = nil, filter = nil)
|
42
|
+
selector ||= @selector
|
43
|
+
filter ||= @filter
|
44
|
+
records = []
|
45
|
+
context = context_for_selector(selector)
|
46
|
+
context.all("[data-#{@type}-id]#{filter}").each do | record|
|
47
|
+
id = record["data-#{@type}-id"]
|
48
|
+
records << new(id, selector)
|
49
|
+
end
|
50
|
+
records
|
51
|
+
end
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
53
|
+
##
|
54
|
+
#
|
55
|
+
# Searches the page and returns an instance of {PageRecord::Base} of instances of
|
56
|
+
# that matches the given id, selector and the filter. See {file:README.md#markup markup} for more
|
57
|
+
# details about formatting the page.
|
58
|
+
#
|
59
|
+
# example:
|
60
|
+
#
|
61
|
+
# ```ruby
|
62
|
+
# TeamPage.find(1)
|
63
|
+
# ```
|
64
|
+
#
|
65
|
+
# returns the record with id
|
66
|
+
#
|
67
|
+
# When you don't specify an id, `find` returns the only record on the page.
|
68
|
+
# If you have more than one record on the page, `find` raises {MultipleRecords}.
|
69
|
+
#
|
70
|
+
# example:
|
71
|
+
#
|
72
|
+
# ```ruby
|
73
|
+
# TeamPage.find()
|
74
|
+
# ```
|
75
|
+
#
|
76
|
+
# @param selector [String] selector to use for searching the table on the page
|
77
|
+
# @param filter [String] filter to use on the records on the page
|
78
|
+
#
|
79
|
+
# @return [Pagerecord] An instance of [PageRecord::Base]
|
80
|
+
#
|
81
|
+
# @raise [MultipleRecords] if the page contains more then on set of records
|
82
|
+
# @raise [RecordNotFound] if the page does not contain any of the specified records
|
83
|
+
#
|
84
|
+
def self.find(id = nil, selector = nil, filter = nil)
|
85
|
+
selector ||= @selector
|
86
|
+
filter ||= @filter
|
87
|
+
new(id, selector, filter)
|
88
|
+
end
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
end
|
134
|
-
end
|
90
|
+
##
|
91
|
+
#
|
92
|
+
# Searches the page and returns an instance of {PageRecord::Base} of instances of
|
93
|
+
# that matches the given attribute. See {file:README.md#markup markup} for more
|
94
|
+
# details about formatting the page.
|
95
|
+
#
|
96
|
+
# Although you can call this yourself, {PageRecord::Base} uses this method for defining a
|
97
|
+
# finder for all attributes when you define your page class, {PageRecord::Base}
|
98
|
+
# See {PageRecord::Base.attributes} for more details.
|
99
|
+
#
|
100
|
+
# example:
|
101
|
+
#
|
102
|
+
# ```ruby
|
103
|
+
# TeamPage.find_by_name('Ajax')
|
104
|
+
# ```
|
105
|
+
#
|
106
|
+
# returns the record where the name is set to Ajax
|
107
|
+
#
|
108
|
+
# @param attribute [String] The attribute name
|
109
|
+
# @param value [String] The value to search for
|
110
|
+
# @param selector [String] selector to use for searching the table on the page
|
111
|
+
# @param filter [String] filter to use on the records on the page
|
112
|
+
#
|
113
|
+
# @return [Pagerecord] An instance of [PageRecord::Base].
|
114
|
+
#
|
115
|
+
# @raise [MultipleRecords] if the page contains more then on set of records
|
116
|
+
# @raise [RecordNotFound] if the page does not contain any of the specified records
|
117
|
+
#
|
118
|
+
def self.find_by_attribute(attribute, value, selector, filter)
|
119
|
+
selector ||= @selector
|
120
|
+
filter ||= @filter
|
121
|
+
|
122
|
+
context = context_for_selector(selector)
|
123
|
+
record = context.find("[data-#{@type}-id]#{filter} > [data-attribute-for='#{attribute}']", text: value)
|
124
|
+
parent = record.find(:xpath, '..')
|
125
|
+
id = parent["data-#{@type}-id"]
|
126
|
+
new(id, selector)
|
127
|
+
rescue Capybara::Ambiguous
|
128
|
+
raise MultipleRecords, "Found multiple #{@type} record with #{attribute} #{value} on page"
|
129
|
+
rescue Capybara::ElementNotFound
|
130
|
+
raise RecordNotFound, "#{@type} record with #{attribute} #{value} not found on page"
|
131
|
+
end
|
132
|
+
end
|
135
133
|
end
|
@@ -3,18 +3,18 @@ module PageRecord
|
|
3
3
|
class FormBuilder < ActionView::Helpers::FormBuilder
|
4
4
|
include PageRecord::Helpers
|
5
5
|
|
6
|
-
helpers = field_helpers +
|
6
|
+
helpers = field_helpers + [:time_zone_select, :date_select] - [:hidden_field, :fields_for, :label]
|
7
7
|
helpers.each do |helper|
|
8
8
|
define_method helper do |field, *args|
|
9
|
-
options = args.
|
9
|
+
options = args.find { |a| a.is_a?(Hash) } || {}
|
10
10
|
super field, options.merge(attribute_for(field))
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
actions =
|
14
|
+
actions = %w(submit button)
|
15
15
|
actions.each do |helper|
|
16
16
|
define_method helper do |field, *args|
|
17
|
-
options = args.
|
17
|
+
options = args.find { |a| a.is_a?(Hash) } || {}
|
18
18
|
super field, options.merge(action_for(field.downcase))
|
19
19
|
end
|
20
20
|
end
|
@@ -2,6 +2,18 @@ module Formtastic
|
|
2
2
|
|
3
3
|
module Inputs
|
4
4
|
module Base
|
5
|
+
|
6
|
+
module Errors
|
7
|
+
|
8
|
+
def error_sentence_html
|
9
|
+
extend(PageRecord::Helpers)
|
10
|
+
error_class = options[:error_class] || builder.default_inline_error_class
|
11
|
+
options = { :class => error_class }.merge(error_for(@method))
|
12
|
+
template.content_tag(:p, Formtastic::Util.html_safe(errors.to_sentence.html_safe), options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
5
17
|
module Html
|
6
18
|
|
7
19
|
alias_method :input_html_options_org, :input_html_options
|
@@ -13,14 +25,13 @@ module Formtastic
|
|
13
25
|
#
|
14
26
|
|
15
27
|
def input_html_options
|
16
|
-
|
17
|
-
input_html_options_org.merge(attribute_for(
|
28
|
+
extend(PageRecord::Helpers)
|
29
|
+
input_html_options_org.merge(attribute_for(input_name))
|
18
30
|
end
|
19
31
|
end
|
20
32
|
end
|
21
33
|
end
|
22
34
|
|
23
|
-
|
24
35
|
module Actions
|
25
36
|
module Buttonish
|
26
37
|
|
@@ -32,7 +43,7 @@ module Formtastic
|
|
32
43
|
# for all action methods
|
33
44
|
#
|
34
45
|
def extra_button_html_options
|
35
|
-
|
46
|
+
extend(PageRecord::Helpers)
|
36
47
|
extra_button_html_options_org.merge(action_for(@method))
|
37
48
|
end
|
38
49
|
|
@@ -46,21 +57,21 @@ module Formtastic
|
|
46
57
|
|
47
58
|
#
|
48
59
|
# This is a replacement method for the original `semantic_form_for` method from `Formtastic`.
|
49
|
-
# The only thing it does, is merge the `form_record_for` into the output.
|
60
|
+
# The only thing it does, is merge the `form_record_for` into the output.
|
50
61
|
#
|
51
62
|
def semantic_form_for(record_or_name_or_array, *args, &proc)
|
52
|
-
|
63
|
+
extend(PageRecord::Helpers)
|
53
64
|
options = args.extract_options!
|
54
|
-
options||= {}
|
65
|
+
options ||= {}
|
55
66
|
case record_or_name_or_array
|
56
67
|
when String, Symbol
|
57
68
|
object_name = record_or_name_or_array
|
58
69
|
else
|
59
70
|
object = record_or_name_or_array.is_a?(Array) ? record_or_name_or_array.last : record_or_name_or_array
|
60
|
-
raise ArgumentError,
|
71
|
+
raise ArgumentError, 'First argument in form cannot contain nil or be empty' unless object
|
61
72
|
object_name = options[:as] || model_name_from_record_or_class(object).param_key
|
62
73
|
end
|
63
|
-
options = options.merge(html:form_record_for(object_name))
|
74
|
+
options = options.merge(html: form_record_for(object_name))
|
64
75
|
args << options
|
65
76
|
semantic_form_for_org(record_or_name_or_array, *args, &proc)
|
66
77
|
end
|