page_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /coverage/
2
+ /doc/
3
+ /.yardoc/
4
+ /pkg/
5
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ # - jruby-19mode
6
+ - rbx-19mode
7
+ script: bundle exec rspec spec
8
+ # matrix:
9
+ # allow_failures:
10
+ # -rvm: jruby-19mode
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in page_record.gemspec
4
+ gemspec
5
+ gem 'coveralls', require: false
data/Gemfile.lock ADDED
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ page_record (0.0.1)
5
+ activesupport
6
+ capybara (~> 2.1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (3.2.13)
12
+ i18n (= 0.6.1)
13
+ multi_json (~> 1.0)
14
+ capybara (2.1.0)
15
+ mime-types (>= 1.16)
16
+ nokogiri (>= 1.3.3)
17
+ rack (>= 1.0.0)
18
+ rack-test (>= 0.5.4)
19
+ xpath (~> 2.0)
20
+ colorize (0.5.8)
21
+ coveralls (0.6.7)
22
+ colorize
23
+ multi_json (~> 1.3)
24
+ rest-client
25
+ simplecov (>= 0.7)
26
+ thor
27
+ diff-lcs (1.2.4)
28
+ i18n (0.6.1)
29
+ mime-types (1.23)
30
+ mini_portile (0.5.0)
31
+ multi_json (1.7.7)
32
+ nokogiri (1.6.0)
33
+ mini_portile (~> 0.5.0)
34
+ rack (1.5.2)
35
+ rack-protection (1.5.0)
36
+ rack
37
+ rack-test (0.6.2)
38
+ rack (>= 1.0)
39
+ rake (10.0.4)
40
+ rest-client (1.6.7)
41
+ mime-types (>= 1.16)
42
+ rspec (2.13.0)
43
+ rspec-core (~> 2.13.0)
44
+ rspec-expectations (~> 2.13.0)
45
+ rspec-mocks (~> 2.13.0)
46
+ rspec-core (2.13.1)
47
+ rspec-expectations (2.13.0)
48
+ diff-lcs (>= 1.1.3, < 2.0)
49
+ rspec-mocks (2.13.1)
50
+ simplecov (0.7.1)
51
+ multi_json (~> 1.0)
52
+ simplecov-html (~> 0.7.1)
53
+ simplecov-html (0.7.1)
54
+ sinatra (1.3.6)
55
+ rack (~> 1.4)
56
+ rack-protection (~> 1.3)
57
+ tilt (~> 1.3, >= 1.3.3)
58
+ thor (0.18.1)
59
+ tilt (1.4.1)
60
+ xpath (2.0.0)
61
+ nokogiri (~> 1.3)
62
+
63
+ PLATFORMS
64
+ ruby
65
+
66
+ DEPENDENCIES
67
+ bundler (~> 1.3)
68
+ coveralls
69
+ page_record!
70
+ rake
71
+ rspec
72
+ sinatra
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Bert Hajee
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+ [![Code Climate](https://codeclimate.com/github/appdrones/page_record.png)](https://codeclimate.com/github/appdrones/page_record) [![Build Status](https://travis-ci.org/appdrones/page_record.png)](https://travis-ci.org/appdrones/page_record) [![Dependency Status](https://gemnasium.com/appdrones/page_record.png)](https://gemnasium.com/appdrones/page_record) [![Coverage Status](https://coveralls.io/repos/appdrones/page_record/badge.png)](https://coveralls.io/r/appdrones/page_record)
2
+
3
+ # PageRecord
4
+
5
+ You've probably been there. You're building your killer Web Application. You, being a responsible developer, your code is thoroughly tested. You are using the likes of cucumber and Rspec to do so. Besides your unit-tests, you also have integration tests doing a full stack test. You are using Capybara to read, parse and test the rendered pages. You use all kinds of selectors to select different parts of the page. Slowly but surely your test code becomes less and less readable.
6
+
7
+ ## PageRecord can help.
8
+ There are a lot of ways you can do this. PageRecord is one of these ways. PageRecord is an ActiveRecord like abstraction for information on the HTML page. You can use `TeamRecord.find(1)` and `TeamRecord.find_by_name('Barcelona')` like functions to find a record on an HTML page. When you've found the record you need, you can use easy accessors to access the attributes. `TeamRecord.find(1).name` returns the name of the team.
9
+
10
+ TODO explain how it helps in testing
11
+
12
+ ##Documentation
13
+ Look at {http://rubydoc.info/github/appdrones/page_record the yard documentation} for details.
14
+
15
+ ##Markup
16
+ For {PageRecord} to recognise the data on the page, the page __must__ follow certain `html` formatting guidelines.
17
+
18
+ __Every__ record on the page __must__ have a `data-type-id='x'` attribute.
19
+
20
+ ```html
21
+ <tag data-type-id='x'>
22
+ ...
23
+ <tag>
24
+ ```
25
+ Where `1` is the `id` of the record and `type` is the type of record. See {PageRecord::PageRecord.type} for infrmation regarding the `type`.
26
+
27
+ __Every__ attribute of a record, __must__ have a `data-attribute-for='name'` attribute. This tag __must__ be contained inside a tag contaning the `data-type-id='x'`
28
+
29
+ ```html
30
+ <tag data-team-id='1'>
31
+ <tag data-attribute-for='attr'>Ajax</tag>
32
+ ...
33
+ <tag>
34
+ ```
35
+
36
+ Where `attr` is the name of the attribute.
37
+
38
+ ##attributes
39
+
40
+ ###reading the attributes
41
+ With {PageRecord}, you can easily access the attributes of a record on the page. After you've got a valid instance of {PageRecord::PageRecord}, you can access all attributes in a normal ruby-like way.
42
+
43
+ ```ruby
44
+ champion = TeamPage.find(1)
45
+ expect(champion.name).to eq('Ajax')
46
+ ```
47
+ ###setting the attributes
48
+ Not only can you access the attributes for reading, you can also set the attributes. This only works if the attribute is modifiable, like in an `INPUT` or a `TEXTFIELD` tag.
49
+
50
+ ```ruby
51
+ champion = TeamPage.find(1)
52
+ champion.name = 'Ajax'
53
+ ```
54
+
55
+ ###specifying the attributes
56
+ When you define you page class as a subclass of {PageRecord::PageRecord}, it automagicaly looks for a host class. To get the name of your host class, PageRecord removes the `Page` part of you class name and tries to use that class as a host class.
57
+
58
+ Example:
59
+
60
+ ```ruby
61
+ class TeamPage < PageRecord::PageRecord
62
+ end
63
+ ```
64
+
65
+ will result in a host class of `Team`
66
+
67
+ ####what is the host class
68
+ The host class is a class who's attributes are mirrored by PageRecord. At this point in time PageRecord only has support for `ActiveRecord` host classes. To be more specific: To be used as a host class, the class must support the method `attribute_names` to return an {::Array} of {::String} of {::Symbol}.
69
+
70
+ Example:
71
+
72
+ Given the following host class and PageRecord class
73
+
74
+ ```ruby
75
+ class Team < ActiveRecord::Base
76
+ # The database defines the following records
77
+ # name, position, points
78
+ #
79
+ end
80
+
81
+ class TeamPage < PageRecord::PageRecord
82
+ end
83
+ ```
84
+
85
+ Then a record from the TeamPage responds to the attributes: `name`, `position`, `points`
86
+
87
+ ##actions
88
+ PageRecord support record actions and page actions.
89
+
90
+ ###record actions
91
+ A record action is an action that is found within a `data-type-id=` tag. To define an action, you can use the `data-action-for='create'` tag.
92
+
93
+ An example:
94
+
95
+ ```html
96
+ <form data-team-id='1'>
97
+ Name: <input data-attribute-for='name'>
98
+ Position: <input data-attribute-for='position'>
99
+ <button data-action-for='save'>Create</button>
100
+ </form>
101
+ ```
102
+
103
+ with this markup on your page, you can use the following ruby code:
104
+
105
+ ```ruby
106
+ team = TeamPage.find(1)
107
+ team.name = 'Ajax'
108
+ team.position = 1
109
+ team.save
110
+ ```
111
+
112
+ When you call an action method, PageRecord automagicaly clicks the specified HTML element.
113
+
114
+ Sometimes you don't want to click the element. PageRecord supports this by calling the action routine with a `?`. This returns the {http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Result Capybara Result}. You can do with this element whatever you can do with this element what you want.
115
+
116
+ ```ruby
117
+ TeamPage.save?
118
+ ```
119
+
120
+ ###page actions
121
+ Sometimes you need to have an action that is not related to a record. An example is the creation of a new record. But in fact, any `data-action-for` tag, outside of a record, can be called as a page action.
122
+
123
+ An example:
124
+
125
+ ```html
126
+ <form data-team-id='1'>
127
+ Name: <input data-attribute-for='name'>
128
+ Position: <input data-attribute-for='position'>
129
+ <button data-action-for='save'>
130
+ </form>
131
+ <button data-action-for='create'>New Team</button>
132
+ ```
133
+
134
+ ```ruby
135
+ TeamPage.create
136
+ ```
137
+
138
+ When you call an action method, PageRecord automagicaly clicks the specfied HTML element.
139
+
140
+ Sometimes you don't want to click the element. PageRecord supports this by calling the action routine with a `?`. This returns the Capybara Element. You can do with this element whatever you can do with this element what you want.
141
+
142
+ ```ruby
143
+ TeamPage.save?
144
+ ```
145
+
146
+ ##Using PageRecord together with Rspec
147
+ To use {PageRecord} with Rspec, you need to require it in your `spec_helper.rb` file. To make it work, it needs to be required __after__ capybara.
148
+
149
+ ```ruby
150
+ require 'capybara'
151
+ require 'page_record/rspec'
152
+ ```
153
+
154
+ Also, you need te make sure your page definitions are included. A good place to store them would be in your support directory.
155
+
156
+ ##Using PageRecord together with Cucumber
157
+ To use {PageRecord} with Cucumber, you need to require it in your `env.rb` file. To make it work, it needs to be required __after__ capybara.
158
+
159
+ ```ruby
160
+ require 'capybara'
161
+ require 'page_record/cucumber'
162
+ ```
163
+ Also, you need te make sure your page definitions are included. A good place to store them would be in your support directory.
164
+
165
+ ##Using PageRecord standalone
166
+ If you are using {PageRecord} outside of Rspec or Cucumber, you also need to require the right files.
167
+
168
+ ```ruby
169
+ require 'capybara'
170
+ require 'page_record/rspec'
171
+ ```
172
+
173
+ You also need te make sure, you set the page variable before you start.
174
+
175
+ ```ruby
176
+ PageRecord::PageRecord.page = session
177
+ ```
178
+
179
+ Also, you need te make sure your page definitions are included.
180
+
181
+
182
+ ## Installation
183
+
184
+ Add this line to your application's Gemfile:
185
+
186
+ gem 'page_record', :require => false
187
+
188
+ And then execute:
189
+
190
+ $ bundle
191
+
192
+ Or install it yourself as:
193
+
194
+ $ gem install page_record
195
+
196
+ ## Usage
197
+
198
+ TODO explain how to use it
199
+
200
+ ## Example
201
+
202
+ TODO show an example
203
+
204
+ ## Contributing
205
+
206
+ 1. Fork it
207
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
208
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
209
+ 4. Push to the branch (`git push origin my-new-feature`)
210
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,60 @@
1
+ module PageRecord
2
+ class PageRecord
3
+ ##
4
+ # Searches the record for the specified attribute and returns the {http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Result Capybara Result}.
5
+ # This method is called when you access an attribute with a `?` of a record
6
+ #
7
+ # @return [Capybara::Result] the text content of the specified attribute
8
+ #
9
+ # @raise [AttributeNotFound] when the attribute is not found in the record
10
+ #
11
+ def read_attribute?(attribute)
12
+ begin
13
+ @record.find("[data-attribute-for='#{attribute}']")
14
+ rescue Capybara::ElementNotFound
15
+ raise AttributeNotFound, "#{@type} record with id #{@id} doesn't contain attribute #{attribute}"
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Searches the record for the specified attribute and returns the text content.
21
+ # This method is called when you access an attribute of a record
22
+ #
23
+ # @return [String] the text content of the specified attribute
24
+ #
25
+ # @raise [AttributeNotFound] when the attribute is not found in the record
26
+ #
27
+ def read_attribute( attribute)
28
+ element = self.send("#{attribute}?")
29
+ tag = element.tag_name
30
+ textelement?(tag) ? element.value : element.text
31
+ end
32
+
33
+ ##
34
+ # Searches the record for the specified attribute and sets the value of the attribute
35
+ # This method is called when you set an attribute of a record
36
+ #
37
+ # @return [Capybara::Result] the text content of the specified attribute
38
+ #
39
+ # @raise [AttributeNotFound] when the attribute is not found in the record
40
+ # @raise [NotInputField] when the attribute is not a `TEXTAREA` or `INPUT` tag
41
+ #
42
+ def write_attribute( attribute, value)
43
+ element = self.send("#{attribute}?")
44
+ tag = element.tag_name
45
+ raise NotInputField unless textelement?(tag)
46
+ element.set(value)
47
+ element
48
+ end
49
+
50
+
51
+ private
52
+ # @private
53
+ def textelement?(tag)
54
+ case tag
55
+ when 'textarea', 'input' then true
56
+ else false
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ module PageRecord
2
+ class PageRecord
3
+ ##
4
+ #
5
+ # This is the implementation of the page action routine. It has two variants.
6
+ # it has a `?` variant and a `normal` variant.
7
+ #
8
+ # normal variant:
9
+ # It checks the page for a data-action-for='action' tag somewhere on the page.
10
+ # If it finds it, it clicks it.
11
+ #
12
+ # `?` variant:
13
+ # It checks the page for a data-action-for='action' tag somewhere on the page.
14
+ # If it finds it, returns the Capybara result.
15
+ #
16
+ # @param action [Symbol] this is the name of the action
17
+ #
18
+ # @return [Capybara::Element]
19
+ #
20
+ # @raise [PageRecord::MultipleRecords] when there are more actions with
21
+ # this name on the page
22
+ #
23
+ def self.method_missing(action)
24
+ raw_action = /(.*)\?/.match(action)
25
+ begin
26
+ if raw_action
27
+ action_for?(raw_action[1])
28
+ else
29
+ action_for(action)
30
+ end
31
+ rescue Capybara::Ambiguous
32
+ raise MultipleRecords, "Found multiple #{action} tags for #{@type} on page"
33
+ rescue Capybara::ElementNotFound
34
+ super
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # @private
41
+ def self.action_for(action)
42
+ element = action_for?(action)
43
+ element.click
44
+ element
45
+ end
46
+
47
+
48
+ # @private
49
+ def self.action_for?(action)
50
+ page.find("[data-action-for='#{action}']")
51
+ end
52
+ end
53
+ end
54
+
55
+
@@ -0,0 +1,214 @@
1
+ module PageRecord
2
+ class PageRecord
3
+
4
+ # @private
5
+ def self.inherited(base)
6
+ base.class_eval do
7
+ set_type_name(base)
8
+ get_attribute_names
9
+ end
10
+ define_class_methods(base)
11
+ define_instance_methods(base)
12
+ end
13
+
14
+
15
+ ##
16
+ # Set's the page {PageRecord::PageRecord} uses for all page operations
17
+ #
18
+ # @param new_page [Cabybara::Session] The Capybara page
19
+ #
20
+ # @return [Capybara::Session]
21
+ #
22
+ def self.page=(new_page)
23
+ @@page = new_page
24
+ end
25
+
26
+ ##
27
+ # Returns the page page {PageRecord::PageRecord} uses for all page operations
28
+ #
29
+ # @return [Capybara::Session]
30
+ #
31
+ def self.page
32
+ @@page
33
+ end
34
+
35
+ ##
36
+ # Set's the default selector for this class
37
+ #
38
+ # Example:
39
+ #
40
+ # ```ruby
41
+ # class TeamPage < PageRecord::PageRecord
42
+ # selector "#first-table"
43
+ # end
44
+ #```
45
+ # @param new_selector [String] The default selector to be used for all finders
46
+ #
47
+ def self.selector( new_selector)
48
+ @selector = new_selector
49
+ end
50
+
51
+ ##
52
+ # Set's the default filter for this class
53
+ #
54
+ #
55
+ # Example:
56
+ #
57
+ # ```ruby
58
+ # class TeamPage < PageRecord::PageRecord
59
+ # filter ".champions-league"
60
+ # end
61
+ #```
62
+ #
63
+ # @param new_filter [String] The default filter to be used for all finders
64
+ #
65
+ def self.filter( new_filter)
66
+ @filter = new_filter
67
+ end
68
+
69
+ ##
70
+ # Set's the default type for this class
71
+ #
72
+ # @param new_type [Symbol] The default type to be used for all finders
73
+ #
74
+ # Example:
75
+ #
76
+ # ```ruby
77
+ # class TopDivisonPage < PageRecord::PageRecord
78
+ # type :team
79
+ # end
80
+ #```
81
+ #
82
+ #
83
+ def self.type( new_type)
84
+ @type = new_type
85
+ end
86
+
87
+
88
+ ##
89
+ # Set's the attributes this page recognises. This will override any types
90
+ # inherited from the host class
91
+ #
92
+ # @param new_attributes [Array] The attributes the page regognises
93
+ #
94
+ # Example:
95
+ #
96
+ # ```ruby
97
+ # class TopDivisonPage < PageRecord::PageRecord
98
+ # attributes [:name, :position, :ranking]
99
+ # end
100
+ #```
101
+ #
102
+ #
103
+ def self.attributes(new_attributes)
104
+ undefine_class_methods(self)
105
+ undefine_instance_methods(self)
106
+ @attributes = new_attributes
107
+ define_class_methods(self)
108
+ define_instance_methods(self)
109
+ end
110
+
111
+
112
+ private
113
+
114
+ # @private
115
+ def self.set_type_name(base)
116
+ @base_name = base.to_s.gsub('Page', '')
117
+ @type = @base_name.underscore
118
+ end
119
+
120
+ # @private
121
+ def self.get_attribute_names
122
+ begin
123
+ @host_class = @base_name.constantize
124
+ @attributes = @host_class.attribute_names.clone
125
+ @attributes.delete('id') # id is a special case attribute
126
+ rescue NameError
127
+ @attributes = []
128
+ end
129
+ end
130
+
131
+
132
+ # @private
133
+ def self.define_accessor_methods(base)
134
+ base.instance_eval do
135
+ @attributes.each do | attribute |
136
+ define_method("#{attribute}?") do
137
+ read_attribute?(attribute)
138
+ end
139
+ define_method(attribute) do
140
+ read_attribute(attribute)
141
+ end
142
+ define_method("#{attribute}=") do | value|
143
+ write_attribute(attribute, value)
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+
150
+ # @private
151
+ def self.undefine_accessor_methods(base)
152
+ base.instance_eval do
153
+ @attributes.each do | attribute |
154
+ remove_method("#{attribute}?")
155
+ remove_method(attribute)
156
+ remove_method("#{attribute}=")
157
+ end
158
+ end
159
+ end
160
+
161
+
162
+ # @private
163
+ def self.define_instance_methods(base)
164
+ define_accessor_methods(base)
165
+ end
166
+
167
+ # @private
168
+ def self.undefine_instance_methods(base)
169
+ undefine_accessor_methods(base)
170
+ end
171
+
172
+
173
+ # @private
174
+ def self.define_class_methods(base)
175
+ eigenclass = class << base; self; end
176
+ attributes = base.instance_variable_get('@attributes')
177
+ eigenclass.instance_eval do
178
+ attributes.each do | attribute|
179
+ define_method "find_by_#{attribute}" do | value, selector = "", filter = ""|
180
+ find_by_attribute( attribute, value, selector, filter)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ # @private
187
+ def self.undefine_class_methods(base)
188
+ eigenclass = class << base; self; end
189
+ attributes = base.instance_variable_get('@attributes')
190
+ eigenclass.instance_eval do
191
+ attributes.each do | attribute|
192
+ remove_method "find_by_#{attribute}"
193
+ end
194
+ end
195
+ end
196
+
197
+
198
+ # @private
199
+ def self.context_for_selector(selector)
200
+ if selector.blank?
201
+ page
202
+ else
203
+ begin
204
+ page.find(selector).find(:xpath, "..")
205
+ rescue Capybara::Ambiguous
206
+ raise MultipleRecords, "Found multiple HTML segments with selector #{selector} on page"
207
+ rescue Capybara::ElementNotFound
208
+ raise RecordNotFound, "#{selector} not found on page"
209
+ end
210
+ end
211
+ end
212
+
213
+ end
214
+ end
@@ -0,0 +1,5 @@
1
+ require 'page_record'
2
+
3
+ Before do
4
+ PageRecord::PageRecord.page = page
5
+ end