seleniumrecord 0.0.1.beta1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e34d57392ef8365deafdf42fce9a442de4de5b5c
4
+ data.tar.gz: dee1cdc484930e6b3ee7553da9813cd90b728a79
5
+ SHA512:
6
+ metadata.gz: 07a8ce0f127b8de9aa73531a492b18f5913e7665e02be7bd6b7b5236b8bb792461fa684030cc3a9a64df686d26c38d8618d5d788bffbdc0f7e3f93183f40a196
7
+ data.tar.gz: d201431d8b93319963930d5e8aea010f62ca5264118538e9fa55f06c6812513f2f88f55e9c703ec76b32ceecf8c7994ad4dff71a09122f3f49ddbcaa4becd51b
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in selenium_record.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 David Saenz Tagarro
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.
@@ -0,0 +1,65 @@
1
+ [![Gem Version](https://badge.fury.io/rb/selenium-record.svg)](http://badge.fury.io/rb/selenium-record)
2
+ [![Code Climate](https://codeclimate.com/github/dsaenztagarro/selenium-record/badges/gpa.svg)](https://codeclimate.com/github/dsaenztagarro/selenium-record)
3
+
4
+ # SeleniumRecord
5
+
6
+ Selenium Record is a DSL for easily writing acceptance tests. It is a wrapper
7
+ over Selenium ruby bindings to let you easily apply the well known page object
8
+ pattern.
9
+
10
+ ## Why to use
11
+
12
+ Within your web app's UI there are areas that your tests interact with. A
13
+ Selenium Record object simply models these as objects within the test code.
14
+ This reduces the amount of duplicated code and means that if the UI changes,
15
+ the fix need only be applied in one place.
16
+
17
+ ## Warning
18
+
19
+ This gem is still under development! As this gem was born while I was trying to
20
+ right acceptance tests in a more maintainable and productive way, it is not
21
+ already tested!! Check the roadmap for upcoming updates. All pending tasks
22
+ should be completed for '1.0.0' version. Keep up to date!
23
+
24
+ ## Roadmap
25
+
26
+ [ ] Full test coverage
27
+ [ ] Wiki Documentation
28
+ [ ] Basic install generator
29
+ [ ] ComponentAutoload integration in core (Currently present as a framework
30
+ extension)
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ gem 'seleniumrecord'
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ Or install it yourself as:
43
+
44
+ $ gem install seleniumrecord
45
+
46
+ ## Usage
47
+
48
+ TODO: Write usage instructions here
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it ( https://github.com/dsaenztagarro/selenium-record/fork )
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create a new Pull Request
57
+
58
+ ## References
59
+
60
+ - [Selenium Wiki Page](https://code.google.com/p/selenium/wiki/PageObjects)
61
+ - "Selenium 2 Testing Tools" by David Burns
62
+
63
+ ## Thanks
64
+
65
+ Thanks to Hola Internet for let me right this kind of tools
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ Dir["#{File.dirname(__FILE__)}/selenium_record/*.rb"].each { |f| require f }
2
+
3
+ # Selenium Record provides objects for interacting with browser and managing
4
+ # the application during tests. Also joins in every object behaviour and
5
+ # expectations
6
+ module SeleniumRecord
7
+ end
@@ -0,0 +1,41 @@
1
+ module SeleniumRecord
2
+ # Helpers for building complex actions
3
+ module ActionBuilder
4
+ # Class for storing action info
5
+ class Action
6
+ attr_reader :method, :args
7
+ def initialize(method, args = [])
8
+ @method = method
9
+ @args = args
10
+ end
11
+ end
12
+
13
+ # Class for building complex actions
14
+ class Builder
15
+ attr_accessor :actions
16
+
17
+ def initialize(&block)
18
+ @actions = []
19
+ @blk = block
20
+ end
21
+
22
+ def perform
23
+ @actions.each { |action| @blk.call(action.method, action.args) }
24
+ @actions.freeze
25
+ end
26
+
27
+ def method_missing(method, *args)
28
+ @actions << Action.new(method, *args)
29
+ self
30
+ end
31
+ end
32
+
33
+ # @param block [Block] the block over all actions will be performed. It
34
+ # should contain two params: |method, args|
35
+ # @return [SeleniumRecord::Builder::ActionBuilder] a new action builder
36
+ # instance
37
+ def action_builder(&block)
38
+ Builder.new(&block)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ module SeleniumRecord
2
+ # Contains simple actions to play from selenium objects
3
+ module Actions
4
+ def textarea_content(locator, content)
5
+ find(locator).clear # gain focus on textarea
6
+ find(locator).send_keys content
7
+ end
8
+
9
+ # Clicks accept button on popup
10
+ def accept_popup
11
+ popup = browser.switch_to.alert
12
+ popup.accept
13
+ end
14
+
15
+ # Chooses an option from select enhanced by javascript
16
+ # @param id [String] id of the select
17
+ # @param text [String] the option text
18
+ def choose_option(id, text)
19
+ click_wait xpath: select_xpath(id)
20
+ click_wait xpath: select_option_xpath(id, text)
21
+ end
22
+
23
+ # Chooses a menu from dropdown enhanced by javascript
24
+ # @param dropdown [String] text of dropdown
25
+ # @param menu [String] text of menu
26
+ def choose_menu(dropdown, menu)
27
+ click_wait xpath: dropdown_xpath(trans dropdown)
28
+ click_wait xpath: dropdown_menu_xpath(trans menu)
29
+ end
30
+
31
+ # Clicks on element and wait until all jquery events are dispatched
32
+ # @param how [Symbol] (:class, :class_name, :css, :id, :link_text, :link,
33
+ # :partial_link_text, :name, :tag_name, :xpath)
34
+ # @param what [String]
35
+ def click_wait(locator)
36
+ when_present(locator).click
37
+ wait_js_inactive
38
+ end
39
+
40
+ def click(locator)
41
+ find(locator).click
42
+ end
43
+
44
+ def submit
45
+ click(xpath: ".//button[@type='submit']")
46
+ wait_page_load
47
+ load_dom
48
+ end
49
+
50
+ # @param text [String] text of the link to be clicked
51
+ def click_link(text)
52
+ finder = root_el || browser
53
+ finder.find_element(link_text: text).click
54
+ end
55
+
56
+ # Drops the model view on the bottom to the top of the model type view list
57
+ # @param model_type [Symbol] the type of the views to be affected
58
+ def pop_last(model_type)
59
+ elements = send("#{model_type}_elements")
60
+ browser.action
61
+ .drag_and_drop(elements.last, elements.first).perform
62
+ end
63
+
64
+ # @param id [Symbol] id of select element
65
+ # @param text [String] text of the option to be selected
66
+ def select_from_chosen(id, text)
67
+ browser.execute_script <<-script
68
+ var optValue = $("##{id} option:contains('#{text}')").val();
69
+ var value = [optValue];
70
+ if ($('##{id}').val()) {
71
+ $.merge(value, $('##{id}').val());
72
+ }
73
+ $('##{id}').val(value).trigger('chosen:updated');
74
+ script
75
+ end
76
+
77
+ # Remove once all helper references belongs to selenium objects
78
+ module_function :choose_option
79
+ end
80
+ end
@@ -0,0 +1,54 @@
1
+ module SeleniumRecord
2
+ # Helper methods for comparing the relative position in DOM among selenium
3
+ # objects
4
+ module Axis
5
+ # @param view [SeleniumRecord::Base]
6
+ # @return [Boolean] Marks whether the current view is located in DOM
7
+ # after the view passed as parameter
8
+ def after?(other_view)
9
+ preceding_sibling_elements.member? other_view.root_el
10
+ end
11
+
12
+ # @param view [SeleniumRecord::Base]
13
+ # @return [Boolean] Marks whether the current view is located in DOM
14
+ # before the view passed as parameter
15
+ def before?(other_view)
16
+ following_sibling_elements.member? other_view.root_el
17
+ end
18
+
19
+ # @param models [Array<PORO>] list of models associated to views
20
+ # @return [Boolean] Marks whether the model views are ordered in the dom
21
+ def ordered?(*models)
22
+ result = []
23
+ models.reduce(nil) do |prev, current|
24
+ result << (view_for(prev).before? view_for(current)) if prev
25
+ current
26
+ end
27
+ result.any?
28
+ end
29
+
30
+ # Returns all elements belonging to preceding sibling xpath axe
31
+ # @return [Array<Selenium::WebDriver::Element>]
32
+ def preceding_sibling_elements
33
+ find_elements(preceding_sibling_locator)
34
+ end
35
+
36
+ # Returns all elements belonging to following sibling xpath axe
37
+ # @return [Array<Selenium::WebDriver::Element>]
38
+ def following_sibling_elements
39
+ find_elements(following_sibling_locator)
40
+ end
41
+
42
+ private
43
+
44
+ # @return [String] locator for finding preceding sibling dom elements
45
+ def preceding_sibling_locator
46
+ { xpath: "./preceding-sibling::#{tag_name}" }
47
+ end
48
+
49
+ # @return [String] locator for finding following sibling dom elements
50
+ def following_sibling_locator
51
+ { xpath: "./following-sibling::#{tag_name}" }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,93 @@
1
+ require_relative 'core'
2
+ require_relative 'lookup'
3
+ require_relative 'actions'
4
+ require_relative 'action_builder'
5
+ require_relative 'scopes'
6
+ require_relative 'waits'
7
+ require_relative 'preconditions'
8
+ require_relative 'axis'
9
+ require_relative 'html'
10
+ require_relative 'theme'
11
+ require_relative 'translations'
12
+
13
+ # SeleniumRecord provides a framework based on the Selenium Page Object pattern
14
+ #
15
+ # ### More references:
16
+ # - [Selenium Wiki Page](https://code.google.com/p/selenium/wiki/PageObjects)
17
+ # - "Selenium 2 Testing Tools" by David Burns
18
+ module SeleniumRecord
19
+ # @abstract Subclass and override {#run} to implement
20
+ # a custom Selenium object
21
+ class Base
22
+ include Core
23
+ include Lookup
24
+ include Actions
25
+ include ActionBuilder
26
+ include Scopes
27
+ include Waits
28
+ include Preconditions
29
+ include Axis
30
+ include Html
31
+ include Theme
32
+ include Translations
33
+ attr_reader :browser, :parent_el, :root_el, :object
34
+ alias_method :__rootel__, :root_el
35
+
36
+ # @params browser [Selenium::WebDriver::Driver]
37
+ # @param [Hash] opts the options to create a new record
38
+ # @option opts [Selenium::WebDriver::Element] :parent_el The parent element
39
+ # @option opts [Selenium::WebDriver::Element] :root_el The root element
40
+ # @option opts [PORO] :object Plain Old Ruby Object contains main info
41
+ # related to the record
42
+ def initialize(browser, opts = {})
43
+ @browser = browser
44
+ @parent_el = opts[:parent_el]
45
+ @root_el = opts[:root_el]
46
+ @object = opts[:object]
47
+ end
48
+
49
+ # Creates a view in the scope of current instance based on object model
50
+ # passed as parameter
51
+ # @param object [ActiveRecord::Base] the object related to the new view
52
+ # @param [Hash] opts the options for the new view created
53
+ # @option opts [Module] :namespace The namespace in which the new view
54
+ # should be created
55
+ # @option opts [String] :suffix The suffix to be appended to the new view
56
+ # class name
57
+ def create_record(object, opts = {})
58
+ subject = opts[:subject] || object.class.name
59
+ klass = self.class.extract_klass(subject, opts)
60
+ klass.new(@browser, parent_el: root_el, object: object)
61
+ end
62
+
63
+ # Creates a view in the scope of current instance based on the action name
64
+ # @param action [Symbol]
65
+ # @param [Hash] opts the options for the new view created
66
+ # @option opts [Module] :namespace The namespace in which the new view
67
+ # should be created
68
+ # @option opts [String] :suffix The suffix to be appended to the new view
69
+ # class name
70
+ def create_record_for_action(action, opts = {})
71
+ klass = self.class.extract_klass(action.to_s.camelize, opts)
72
+ klass.new(@browser, parent_el: root_el)
73
+ end
74
+
75
+ # @return [Boolean] returns whether the view is attached to the dom
76
+ def exist?
77
+ load_dom if respond_to? :lookup_sequence
78
+ root_el != nil
79
+ end
80
+
81
+ # @param [Hash] opts the options for the new view created
82
+ # @option opts [Module] :namespace The namespace in which the new view
83
+ # should be created
84
+ # @option opts [String] :suffix The suffix to be appended to the new view
85
+ # class name
86
+ # @return [SeleniumRecord::Base]
87
+ def self.extract_klass(subject, opts)
88
+ namespace = opts[:namespace] || Object
89
+ suffix = opts[:suffix] || ''
90
+ namespace.const_get("#{subject}#{suffix}")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,11 @@
1
+ module SeleniumRecord
2
+ # Defines configuration options
3
+ class Configuration
4
+ @js_library = :jquery
5
+ @choose_option_max_retries = 10
6
+
7
+ class << self
8
+ attr_accessor :js_library, :choose_option_max_retries
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module SeleniumRecord
2
+ # Core helpers to get easier access to selenium api
3
+ module Core
4
+ SUBCLASS_RESPONSABILITY = 'SubclassResponsibilityError'
5
+
6
+ def load_dom!(attrs = {})
7
+ @load_attributes = attrs
8
+ before_load_dom if respond_to? :before_load_dom
9
+ before_lookup if respond_to? :before_lookup
10
+ lookup
11
+ after_load_dom if respond_to? :after_load_dom
12
+ self
13
+ end
14
+
15
+ def load_dom(attrs = {})
16
+ load_dom! attrs
17
+ rescue
18
+ false
19
+ end
20
+
21
+ def click_on(locator)
22
+ find(locator).click
23
+ end
24
+
25
+ def find(locator)
26
+ root_el.find_element(locator)
27
+ end
28
+
29
+ def find_elements(locator)
30
+ root_el.find_elements(locator)
31
+ end
32
+
33
+ def first_last(list)
34
+ blk = ->(first, *_, last) { [first, last] }
35
+ blk.call(*list)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module SeleniumRecord
2
+ # Helpers for getting html related info to selenium objects
3
+ module Html
4
+ # @return [String] the html content for the root element
5
+ def to_html
6
+ return root_el.attribute('innerHTML') if exist?
7
+ nil
8
+ end
9
+
10
+ # @return [String] the tag name for the DOM element at the root of the
11
+ # object. Used to define xpath locators.
12
+ # @see preceding_sibling_locator
13
+ def tag_name
14
+ @tag_name ||= root_el.attribute('tagName')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,123 @@
1
+ module SeleniumRecord
2
+ # Responsible methods for looking up the root element for each selenium object
3
+ module Lookup
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Searchs for root element of current object based on other element
9
+ # @return [Webdriver::Element]
10
+ def lookup
11
+ @root_el = parent_el || browser
12
+ lookup_sequence.each { |locator| @root_el = lookup_step(locator) }
13
+ rescue
14
+ @root_el = nil
15
+ raise
16
+ end
17
+
18
+ # Given the current root element for the view, applies a scoped search for
19
+ # the web element identified by the locator passed as parameter
20
+ #
21
+ # @raise [LookupMultipleElementsError] if it is found multiple web elements
22
+ # in the scope of the current root element for the locator passed
23
+ # @raise [LookupUndefinedElementError] if it isn't found any web elements
24
+ # in the scope of the current root element for the locator passed
25
+ #
26
+ # @param locator [Hash] contains unique {key: value} where the key is the
27
+ # locator_type (:class, :class_name, :css, :id, :link_text, :link,
28
+ # :partial_link_text, :name, :tag_name, :xpath)
29
+ # @return element [Selenium::WebDriver::Element]
30
+ def lookup_step(locator)
31
+ lookup_elements = find_elements(locator)
32
+ size = lookup_elements.size
33
+ fail 'LookupMultipleElementsError' if size > 1
34
+ fail 'LookupUndefinedElementError' if size == 0
35
+ lookup_elements.first
36
+ end
37
+
38
+ # Clasess extending SeleniumRecord::Base should overwrite this method or
39
+ # call to class method `lookup_strategy` defined in `SeleniumRecord::Lookup`
40
+ # in order to search for Selenium::WebDriver::Element used as scope for
41
+ # finding elements inside the instance object
42
+ def lookup_sequence
43
+ fail 'LookupUndefinedSequenceError'
44
+ end
45
+
46
+ # Contains class method helpers and definition of classes for lookup
47
+ # strategy
48
+ module ClassMethods
49
+ # @param strategy_sym [Symbol] lookup strategy corresponding with the
50
+ # name of a lookup strategy locator
51
+ def lookup_strategy(strategy_sym, opts = {})
52
+ locator_klass = "Lookup#{strategy_sym.to_s.camelize}Strategy"
53
+ Module.nesting.shift.const_get(locator_klass).new(self, opts).run
54
+ end
55
+
56
+ # Base class for all lookup strategies
57
+ class LookupStrategy
58
+ attr_reader :lookup_attributes
59
+
60
+ # @param klass [SeleniumObject::Base] class on top over it is defined
61
+ # the lookup strategy
62
+ # @param attrs [Hash] attributes used while it is the defined the
63
+ # lookup sequence
64
+ def initialize(klass, attrs = {})
65
+ @klass = klass
66
+ @attributes_blk = -> { attrs }
67
+ end
68
+
69
+ # Defines for the class the instance methods required for the lookup
70
+ # sequence. Inside the block you have access to the "lookup_attributes"
71
+ # specified in the constructor call
72
+ # @param [Block] block defining the lookup sequence
73
+ def lookup_sequence(&block)
74
+ attributes_blk = @attributes_blk
75
+ before_lookup_blk = before_run if respond_to? :before_run
76
+ @klass.instance_eval do
77
+ define_method :lookup_attributes, attributes_blk
78
+ define_method :lookup_sequence, &block
79
+ define_method :before_lookup, before_lookup_blk if before_lookup_blk
80
+ end
81
+ end
82
+ end
83
+
84
+ # Defines a lookup sequence relative to the title xpath
85
+ class LookupRelativeTitleStrategy < LookupStrategy
86
+ def run
87
+ lookup_sequence do
88
+ [title_locator, lookup_attributes[:locator]]
89
+ end
90
+ end
91
+ end
92
+
93
+ # Defines a lookup sequence relative to the xpath of an element present in
94
+ # the selenium object
95
+ class LookupRelativePathStrategy < LookupStrategy
96
+ def run
97
+ lookup_sequence do
98
+ [send("#{lookup_attributes[:to]}_locator"),
99
+ lookup_attributes[:locator]]
100
+ end
101
+ end
102
+ end
103
+
104
+ # Defines a lookup sequence matching an element path
105
+ class LookupMatchingStrategy < LookupStrategy
106
+ def run
107
+ lookup_sequence { [lookup_attrs[:locator]] }
108
+ end
109
+ end
110
+
111
+ # Defines a lookup sequence matching the whole document body
112
+ class LookupRootStrategy < LookupStrategy
113
+ def run
114
+ lookup_sequence { [{ xpath: '//body' }] }
115
+ end
116
+
117
+ def before_run
118
+ -> { @parent_el = nil }
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'base'
2
+
3
+ module SeleniumRecord
4
+ # Base model to be extended by all Selenium page objects
5
+ class NavigationItem < Base
6
+ lookup_strategy :root
7
+
8
+ def before_load_dom
9
+ before_navigate if respond_to? :before_navigate
10
+ navigate unless current?
11
+ end
12
+
13
+ def click_link(*args)
14
+ super
15
+ self
16
+ end
17
+
18
+ def reload
19
+ find(link_active_locator).click
20
+ wait_page_load
21
+ self
22
+ rescue => error
23
+ if error.is_a? Selenium::WebDriver::Error::StaleElementReferenceError
24
+ load_dom
25
+ retry
26
+ else
27
+ raise
28
+ end
29
+ end
30
+
31
+ # Class method to create the 'before_navigate' hook defining the title
32
+ # of the page
33
+ # @param key [String] key to lookup text translation for menu title of
34
+ # the page
35
+ def self.navigate_to(key)
36
+ define_method :before_navigate do
37
+ @title = trans key
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Checks whether the current page corresponds to the instance page and
44
+ # in case of mismatch clicks on the navigation menu for loading the
45
+ # instance page
46
+ def navigate
47
+ when_present(link_inactive_locator).click
48
+ wait_displayed(link_active_locator)
49
+ end
50
+
51
+ # @param [String] Returns the xpath to be used to identify if the current
52
+ # browser page matches this object
53
+ # @raise [SubclassResponsabilityError] if subclasses don't implement this
54
+ # method
55
+ def link_inactive_locator
56
+ fail SUBCLASS_RESPONSABILITY
57
+ end
58
+
59
+ def link_active_locator
60
+ fail SUBCLASS_RESPONSABILITY
61
+ end
62
+
63
+ # @return [Boolean] Marks whether the current browser page matches the new
64
+ # new instantiated page object
65
+ def current?
66
+ browser.find_element(link_active_locator)
67
+ rescue Selenium::WebDriver::Error::NoSuchElementError
68
+ false
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module SeleniumRecord
2
+ # Selenium helpers for doing an action after a precondition takes place
3
+ module Preconditions
4
+ # Returns the first element matching the given arguments once this
5
+ # element is displayed in the DOM
6
+ # @param how [Symbol] (:class, :class_name, :css, :id, :link_text, :link,
7
+ # :partial_link_text, :name, :tag_name, :xpath)
8
+ # @param what [String]
9
+ # @return element [Selenium::WebDriver::Element]
10
+ def when_present(locator)
11
+ element = wait_displayed(locator)
12
+ yield if block_given?
13
+ element
14
+ end
15
+
16
+ # @raise [Selenium::WebDriver::Error::TimeOutError] whether the element
17
+ # stays no clickable after time out period
18
+ # @param locator [Hash] contains unique {key: value} where the key is the
19
+ # locator_type (:class, :class_name, :css, :id, :link_text, :link,
20
+ # :partial_link_text, :name, :tag_name, :xpath)
21
+ # @return [Selenium::WebDriver::Element] once the element is clickable
22
+ def when_clickable(locator)
23
+ element = wait_clickable(locator)
24
+ yield if block_given?
25
+ element
26
+ end
27
+
28
+ def when_modal_present(title, &block)
29
+ when_present(:xpath, modal_header_xpath(title), &block)
30
+ end
31
+
32
+ # @raise [Selenium::WebDriver::Error::TimeOutError] whether the element
33
+ # stays visible after time out period
34
+ # @param locator [Hash] contains unique {key: value} where the key is the
35
+ # locator_type (:class, :class_name, :css, :id, :link_text, :link,
36
+ # :partial_link_text, :name, :tag_name, :xpath)
37
+ # @return [Selenium::WebDriver::Element] once the element is hidden
38
+ def when_hidden(locator)
39
+ self.class.wait_for do
40
+ begin
41
+ element = root_el.find_element(locator)
42
+ element unless element.displayed?
43
+ rescue => error
44
+ raise if error.is_a? Selenium::WebDriver::Error::TimeOutError
45
+ true
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ module SeleniumRecord
2
+ # Helpers for executing actions in custom scope
3
+ module Scopes
4
+ # Class for giving custom scope to selenium objects
5
+ class LocatorScope < SimpleDelegator
6
+ include Actions
7
+ attr_accessor :scoped_locator
8
+
9
+ def root_el
10
+ @root_el ||= __rootel__.find_element(scoped_locator)
11
+ end
12
+
13
+ def find(locator)
14
+ root_el.find_element(locator)
15
+ end
16
+
17
+ def find_elements(locator)
18
+ root_el.find_elements(locator)
19
+ end
20
+
21
+ def run(&block)
22
+ instance_eval(&block)
23
+ end
24
+
25
+ def class
26
+ __getobj__.class
27
+ end
28
+ end
29
+
30
+ # Executes the block passed as parameter in the scope associated to the
31
+ # locator referenced by the scope name
32
+ # @param name [Symbol] The name of the scope. Adding the suffix '_locator'
33
+ # it should match a locator name
34
+ def scope(scope_name, &block)
35
+ scope_obj = LocatorScope.new(self)
36
+ scope_obj.scoped_locator = send "#{scope_name}_locator"
37
+ scope_obj.run(&block)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ module SeleniumRecord
2
+ # Helpers specific for the bootstrap theme
3
+ module Theme
4
+ def modal_header_xpath(key)
5
+ "//div[@class='modal-header']/h3[text()='#{trans(key)}')]"
6
+ end
7
+
8
+ def find_headline(text)
9
+ xpath = "//div[contains(@class,'headline')]//div[contains(.,'#{text}')]"
10
+ find(:xpath, xpath)
11
+ end
12
+
13
+ # Returns xpath for select decorated with chosen.jquery.js
14
+ # @param [String] id of the select
15
+ def select_xpath(id)
16
+ ".//div[@id='#{id}_chosen']/a"
17
+ end
18
+
19
+ def select_option_xpath(id, text)
20
+ ".//div[@id='#{id}_chosen']//li[text()='#{text}']"
21
+ end
22
+
23
+ # Returns xpath for the dropdown button that matches the text
24
+ # @param key [String] text of dropdown button
25
+ def dropdown_xpath(text)
26
+ ".//button[contains(.,'#{text}')]"
27
+ end
28
+
29
+ # Returns xpath for the select option that matches the text
30
+ # @param key [String] text of select option
31
+ def dropdown_menu_xpath(text)
32
+ ".//ul[@class='dropdown-menu']/li[contains(.,'#{text}')]/a"
33
+ end
34
+
35
+ def section_xpath(text)
36
+ ".//section//div[@class='panel-header']
37
+ /h3[@class='suspended area-name']/strong/a[contains(.,'#{text}')]"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ module SeleniumRecord
2
+ # Contains helper methods for translations
3
+ module Translations
4
+ # @param key [String] the key to be used to lookup text translations
5
+ # @return [String] the translation for the given key
6
+ def trans(key)
7
+ I18n.t(key)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module SeleniumRecord
2
+ VERSION = '0.0.1.beta1'
3
+ end
@@ -0,0 +1,106 @@
1
+ #:nodoc:
2
+ module SeleniumRecord
3
+ # Helpers to make easy waiting for something to happen
4
+ module Waits
5
+ DEFAULT_WAITING_TIME = 20
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ # Wait selenium execution until no ajax request is pending in the browser
12
+ # @param seconds [Integer] number of seconds to wait
13
+ def wait_js_inactive(seconds = DEFAULT_WAITING_TIME)
14
+ klass = self.class
15
+ yield if block_given?
16
+ klass.wait_for(seconds) do
17
+ browser.execute_script(klass.js_inactive_script) == 0
18
+ end
19
+ end
20
+
21
+ def wait_page_load
22
+ self.class.wait_for do
23
+ browser.execute_script('return document.readyState;') == 'complete'
24
+ end
25
+ load_dom
26
+ end
27
+
28
+ # Wait selenium execution until the element is displayed
29
+ # @param locator [Hash] contains unique {key: value} where the key is the
30
+ # locator_type (:class, :class_name, :css, :id, :link_text, :link,
31
+ # :partial_link_text, :name, :tag_name, :xpath)
32
+ def wait_displayed(locator, opts = {})
33
+ klass = self.class
34
+ klass.wait_for(klass.seconds_for(opts)) do
35
+ begin
36
+ evaluate_displayed(locator)
37
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
38
+ lookup unless parent_el
39
+ false
40
+ end
41
+ end
42
+ end
43
+
44
+ # Waits until the 'model_view' corresponding to the model is completely
45
+ # visible
46
+ # @param model[PORO] plain old ruby object with a related 'model_view'
47
+ def wait_fade_in(model)
48
+ web_el = view_for(model).root_el
49
+ self.class.wait_for { web_el.css_value('opacity').to_i == 1 }
50
+ end
51
+
52
+ # Waits until the 'model_view' corresponding to the model is completely
53
+ # hidden
54
+ # @param locator [Hash] contains unique {key: value} where the key is the
55
+ # locator_type (:class, :class_name, :css, :id, :link_text, :link,
56
+ # :partial_link_text, :name, :tag_name, :xpath)
57
+ def wait_hidden(locator)
58
+ self.class.wait_for do
59
+ begin
60
+ finder = root_el || browser
61
+ element = finder.find_element(locator)
62
+ !element.displayed?
63
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
64
+ true
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # @return element [Selenium::WebDriver::Element] if the element is visible.
72
+ # Otherwise returns nil.
73
+ def evaluate_displayed(locator)
74
+ finder = root_el || browser
75
+ element = finder.find_element(locator)
76
+ element if element.displayed?
77
+ end
78
+
79
+ # Utilities for waiting methods
80
+ module ClassMethods
81
+ # Wait selenium execution until a condition take place
82
+ # @raise [Selenium::WebDriver::Error::TimeOutError] if the precondition we
83
+ # are waiting for doesn't take place after completing the wait period
84
+ # @param seconds [Integer] number of seconds to wait
85
+ # @yieldreturn [Boolean] marks whether the condition we are waiting for
86
+ # passes
87
+ def wait_for(seconds = DEFAULT_WAITING_TIME)
88
+ Selenium::WebDriver::Wait.new(timeout: seconds).until { yield }
89
+ end
90
+
91
+ # @return [String] the string containing javascript code to be evaluated
92
+ # to check if there are ajax calls pending
93
+ def js_inactive_script
94
+ {
95
+ jquery: 'return $.active'
96
+ }[Configuration.js_library]
97
+ end
98
+
99
+ def seconds_for(opts)
100
+ seconds = opts[:seconds]
101
+ return seconds if seconds
102
+ 20
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'selenium_record/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'seleniumrecord'
8
+ spec.version = SeleniumRecord::VERSION
9
+ spec.authors = ['David Saenz Tagarro']
10
+ spec.email = ['david.saenz.tagarro@gmail.com']
11
+ spec.summary = <<-summary
12
+ Selenium Record is a DSL for easily writing acceptance tests.
13
+ summary
14
+ spec.description = <<-desc
15
+ Selenium Record is a wrapper over Selenium ruby bindings to let you easily
16
+ apply the well known page object pattern.
17
+ desc
18
+ spec.homepage = 'https://github.com/dsaenztagarro/selenium-record'
19
+ spec.license = 'MIT'
20
+
21
+ spec.files = `git ls-files -z`.split("\x0")
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.6'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'codeclimate-test-reporter'
30
+ spec.add_development_dependency 'simplecov'
31
+ spec.add_development_dependency 'coveralls'
32
+ spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'reek'
34
+ spec.add_development_dependency 'cane'
35
+ end
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seleniumrecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.beta1
5
+ platform: ruby
6
+ authors:
7
+ - David Saenz Tagarro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: codeclimate-test-reporter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: reek
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: cane
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: |
140
+ Selenium Record is a wrapper over Selenium ruby bindings to let you easily
141
+ apply the well known page object pattern.
142
+ email:
143
+ - david.saenz.tagarro@gmail.com
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".gitignore"
149
+ - Gemfile
150
+ - LICENSE.txt
151
+ - README.md
152
+ - Rakefile
153
+ - lib/selenium_record.rb
154
+ - lib/selenium_record/action_builder.rb
155
+ - lib/selenium_record/actions.rb
156
+ - lib/selenium_record/axis.rb
157
+ - lib/selenium_record/base.rb
158
+ - lib/selenium_record/configuration.rb
159
+ - lib/selenium_record/core.rb
160
+ - lib/selenium_record/html.rb
161
+ - lib/selenium_record/lookup.rb
162
+ - lib/selenium_record/navigation_item.rb
163
+ - lib/selenium_record/preconditions.rb
164
+ - lib/selenium_record/scopes.rb
165
+ - lib/selenium_record/theme.rb
166
+ - lib/selenium_record/translations.rb
167
+ - lib/selenium_record/version.rb
168
+ - lib/selenium_record/waits.rb
169
+ - selenium_record.gemspec
170
+ homepage: https://github.com/dsaenztagarro/selenium-record
171
+ licenses:
172
+ - MIT
173
+ metadata: {}
174
+ post_install_message:
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">"
186
+ - !ruby/object:Gem::Version
187
+ version: 1.3.1
188
+ requirements: []
189
+ rubyforge_project:
190
+ rubygems_version: 2.2.2
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: Selenium Record is a DSL for easily writing acceptance tests.
194
+ test_files: []