seleniumrecord 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []