page_magic 1.2.6 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +19 -3
  3. data/.simplecov +5 -3
  4. data/.zsh_config +6 -0
  5. data/Dockerfile +10 -0
  6. data/Gemfile +12 -12
  7. data/Gemfile.lock +133 -126
  8. data/Makefile +13 -0
  9. data/README.md +78 -14
  10. data/Rakefile +12 -2
  11. data/VERSION +1 -1
  12. data/circle.yml +4 -2
  13. data/lib/active_support/core_ext/object/to_query.rb +84 -0
  14. data/lib/page_magic.rb +7 -4
  15. data/lib/page_magic/class_methods.rb +4 -1
  16. data/lib/page_magic/driver.rb +3 -0
  17. data/lib/page_magic/drivers.rb +6 -5
  18. data/lib/page_magic/drivers/poltergeist.rb +2 -0
  19. data/lib/page_magic/drivers/rack_test.rb +3 -1
  20. data/lib/page_magic/drivers/selenium.rb +4 -2
  21. data/lib/page_magic/element.rb +19 -10
  22. data/lib/page_magic/element/locators.rb +4 -1
  23. data/lib/page_magic/element/query.rb +33 -33
  24. data/lib/page_magic/element/query_builder.rb +61 -0
  25. data/lib/page_magic/element/selector.rb +3 -0
  26. data/lib/page_magic/element/selector_methods.rb +3 -0
  27. data/lib/page_magic/element_context.rb +9 -12
  28. data/lib/page_magic/element_definition_builder.rb +10 -4
  29. data/lib/page_magic/elements.rb +51 -8
  30. data/lib/page_magic/exceptions.rb +4 -33
  31. data/lib/page_magic/instance_methods.rb +6 -1
  32. data/lib/page_magic/matcher.rb +12 -3
  33. data/lib/page_magic/session.rb +7 -5
  34. data/lib/page_magic/session_methods.rb +2 -0
  35. data/lib/page_magic/utils/string.rb +14 -0
  36. data/lib/page_magic/wait_methods.rb +3 -0
  37. data/lib/page_magic/watcher.rb +2 -0
  38. data/lib/page_magic/watchers.rb +4 -1
  39. data/page_magic.gemspec +19 -13
  40. data/spec/element_spec.rb +2 -0
  41. data/spec/lib/active_support/core_ext/object/to_query_test.rb +78 -0
  42. data/spec/page_magic/class_methods_spec.rb +2 -0
  43. data/spec/page_magic/driver_spec.rb +2 -0
  44. data/spec/page_magic/drivers/poltergeist_spec.rb +2 -0
  45. data/spec/page_magic/drivers/rack_test_spec.rb +2 -0
  46. data/spec/page_magic/drivers/selenium_spec.rb +2 -0
  47. data/spec/page_magic/drivers_spec.rb +2 -0
  48. data/spec/page_magic/element/locators_spec.rb +2 -0
  49. data/spec/page_magic/element/query_builder_spec.rb +110 -0
  50. data/spec/page_magic/element/query_spec.rb +35 -77
  51. data/spec/page_magic/element/selector_spec.rb +14 -7
  52. data/spec/page_magic/element_context_spec.rb +5 -10
  53. data/spec/page_magic/element_definition_builder_spec.rb +3 -1
  54. data/spec/page_magic/elements_spec.rb +19 -5
  55. data/spec/page_magic/instance_methods_spec.rb +2 -0
  56. data/spec/page_magic/matcher_spec.rb +3 -1
  57. data/spec/page_magic/session_methods_spec.rb +2 -0
  58. data/spec/page_magic/session_spec.rb +3 -4
  59. data/spec/page_magic/utils/string_spec.rb +36 -0
  60. data/spec/page_magic/wait_methods_spec.rb +5 -3
  61. data/spec/page_magic/watchers_spec.rb +2 -0
  62. data/spec/page_magic_spec.rb +5 -7
  63. data/spec/spec_helper.rb +3 -0
  64. data/spec/support/shared_contexts.rb +3 -1
  65. data/spec/support/shared_contexts/files_context.rb +2 -0
  66. data/spec/support/shared_contexts/nested_elements_html_context.rb +2 -0
  67. data/spec/support/shared_contexts/rack_application_context.rb +2 -0
  68. data/spec/support/shared_contexts/webapp_fixture_context.rb +3 -1
  69. data/spec/support/shared_examples.rb +2 -0
  70. data/spec/watcher_spec.rb +3 -0
  71. metadata +62 -40
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Bundler.require :test, :development
2
4
 
3
5
  RuboCop::RakeTask.new
@@ -12,11 +14,19 @@ Jeweler::Tasks.new do |gem|
12
14
  gem.license = 'ruby'
13
15
  gem.summary = 'Framework for modeling and interacting with webpages'
14
16
  gem.description = 'Framework for modeling and interacting with webpages which wraps capybara'
15
- gem.email = 'info@lad-tech.com'
17
+ gem.email = 'info@lvl-up.uk'
16
18
  gem.authors = ['Leon Davis']
17
19
  gem.required_ruby_version = '>= 2.1'
18
20
  end
19
21
 
20
22
  Jeweler::RubygemsDotOrgTasks.new
21
23
 
22
- task default: [:spec, 'rubocop:auto_correct']
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new do |t|
26
+ t.libs << 'spec'
27
+ t.pattern = 'spec/**/*_test.rb'
28
+ t.warning = true
29
+ t.verbose = true
30
+ end
31
+
32
+ task default: [:spec, :test, 'rubocop:auto_correct']
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.6
1
+ 2.0.0.alpha1
data/circle.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  machine:
2
2
  ruby:
3
- version: '2.1'
3
+ version: '2.2'
4
4
  test:
5
5
  override:
6
- - bundle exec rake
6
+ - bundle exec rake
7
+ post:
8
+ - CODECLIMATE_REPO_TOKEN=$CODECLIMATE_REPO_TOKEN bundle exec codeclimate-test-reporter
@@ -0,0 +1,84 @@
1
+ require 'cgi'
2
+
3
+ class Object
4
+ # Alias of <tt>to_s</tt>.
5
+ def to_param
6
+ to_s
7
+ end
8
+
9
+ # Converts an object into a string suitable for use as a URL query string,
10
+ # using the given <tt>key</tt> as the param name.
11
+ def to_query(key)
12
+ "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
13
+ end
14
+ end
15
+
16
+ class NilClass
17
+ # Returns +self+.
18
+ def to_param
19
+ self
20
+ end
21
+ end
22
+
23
+ class TrueClass
24
+ # Returns +self+.
25
+ def to_param
26
+ self
27
+ end
28
+ end
29
+
30
+ class FalseClass
31
+ # Returns +self+.
32
+ def to_param
33
+ self
34
+ end
35
+ end
36
+
37
+ class Array
38
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
39
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
40
+ def to_param
41
+ collect(&:to_param).join '/'
42
+ end
43
+
44
+ # Converts an array into a string suitable for use as a URL query string,
45
+ # using the given +key+ as the param name.
46
+ #
47
+ # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
48
+ def to_query(key)
49
+ prefix = "#{key}[]"
50
+
51
+ if empty?
52
+ nil.to_query(prefix)
53
+ else
54
+ collect { |value| value.to_query(prefix) }.join '&'
55
+ end
56
+ end
57
+ end
58
+
59
+ class Hash
60
+ # Returns a string representation of the receiver suitable for use as a URL
61
+ # query string:
62
+ #
63
+ # {name: 'David', nationality: 'Danish'}.to_query
64
+ # # => "name=David&nationality=Danish"
65
+ #
66
+ # An optional namespace can be passed to enclose key names:
67
+ #
68
+ # {name: 'David', nationality: 'Danish'}.to_query('user')
69
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
70
+ #
71
+ # The string pairs "key=value" that conform the query string
72
+ # are sorted lexicographically in ascending order.
73
+ #
74
+ # This method is also aliased as +to_param+.
75
+ def to_query(namespace = nil)
76
+ collect do |key, value|
77
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
78
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
79
+ end
80
+ end.compact.sort! * '&'
81
+ end
82
+
83
+ alias to_param to_query
84
+ end
data/lib/page_magic.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $LOAD_PATH.unshift(File.dirname(__FILE__).to_s)
2
4
  require 'capybara'
3
5
  require 'page_magic/exceptions'
@@ -31,7 +33,8 @@ module PageMagic
31
33
  def included(clazz)
32
34
  clazz.class_eval do
33
35
  include(InstanceMethods)
34
- extend(Elements, ClassMethods)
36
+ extend ClassMethods
37
+ extend Elements
35
38
  end
36
39
  end
37
40
 
@@ -48,10 +51,10 @@ module PageMagic
48
51
  # @param [Symbol] browser name of browser
49
52
  # @param [String] url url to start the session on
50
53
  # @param [Hash] options browser driver specific options
51
- # @return [Session] configured sessoin
52
- def session(application: nil, browser: :rack_test, url:, options: {})
54
+ # @return [Session] configured session
55
+ def session(url: nil, application: nil, browser: :rack_test, options: {})
53
56
  driver = drivers.find(browser)
54
- raise UnspportedBrowserException unless driver
57
+ raise UnsupportedBrowserException unless driver
55
58
 
56
59
  Capybara.register_driver browser do |app|
57
60
  driver.build(app, browser: browser, options: options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # module ClassMethods - contains class level methods for PageObjects
3
5
  module ClassMethods
@@ -15,6 +17,7 @@ module PageMagic
15
17
  # if one has not been set on the page object class it will return a default block that does nothing
16
18
  def on_load(&block)
17
19
  return @on_load || DEFAULT_ON_LOAD unless block
20
+
18
21
  @on_load = block
19
22
  end
20
23
 
@@ -34,7 +37,7 @@ module PageMagic
34
37
  session_options = { browser: browser, options: options, url: url }
35
38
  session_options[:application] = application if application
36
39
 
37
- PageMagic.session(session_options).tap do |session|
40
+ PageMagic.session(**session_options).tap do |session|
38
41
  session.visit(self, url: url)
39
42
  end
40
43
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # class Driver - instances are factories for drivers used by PageMagic
3
5
  class Driver
4
6
  attr_reader :supported_browsers, :handler
7
+
5
8
  # Creates a driver definition
6
9
  # @example
7
10
  # Driver.new do |rack_application, options|
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'page_magic/driver'
4
+ require 'page_magic/utils/string'
2
5
  module PageMagic
3
6
  # class Drivers - creates an object that can be used to hold driver definitions
4
7
  # These PageMagic gets the user's chosen driver from this object.
@@ -16,12 +19,10 @@ module PageMagic
16
19
  # Loads drivers defined in files at the given path
17
20
  # @param [String] path where the drivers are located
18
21
  def load(path = "#{__dir__}/drivers")
19
- require 'active_support/inflector'
20
-
21
- Dir["#{path}/*.rb"].each do |driver_file|
22
+ Dir["#{path}/*.rb"].sort.each do |driver_file|
22
23
  require driver_file
23
24
  driver_name = File.basename(driver_file)[/(.*)\.rb$/, 1]
24
- register self.class.const_get(driver_name.classify)
25
+ register self.class.const_get(Utils::String.classify(driver_name))
25
26
  end
26
27
  end
27
28
 
@@ -35,7 +36,7 @@ module PageMagic
35
36
  # @param [Object] other subject of equality check
36
37
  # @return [Boolean] true if the object is a match
37
38
  def ==(other)
38
- other.is_a?(Drivers) && all == other.all
39
+ other.respond_to?(:all) && all == other.all
39
40
  end
40
41
  end
41
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Poltergeist = PageMagic::Driver.new(:poltergeist) do |app, options|
2
4
  require 'capybara/poltergeist'
3
5
  Capybara::Poltergeist::Driver.new(app, options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::RackTest = PageMagic::Driver.new(:rack_test) do |app, options|
2
- Capybara::RackTest::Driver.new(app, options)
4
+ Capybara::RackTest::Driver.new(app, **options)
3
5
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  PageMagic::Drivers::Selenium = PageMagic::Driver.new(:chrome, :firefox) do |app, options, browser|
2
- require 'watir-webdriver'
3
- Capybara::Selenium::Driver.new(app, options.dup.merge(browser: browser))
4
+ require 'watir'
5
+ Capybara::Selenium::Driver.new(app, **options.dup.merge(browser: browser))
4
6
  end
@@ -1,17 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'page_magic/element/selector_methods'
3
5
  require 'page_magic/element/locators'
4
6
  require 'page_magic/element/selector'
5
- require 'page_magic/element/query'
7
+ require 'page_magic/element/query_builder'
6
8
  module PageMagic
7
9
  # class Element - represents an element in a html page.
8
10
  class Element
9
- EVENT_TYPES = [:set, :select, :select_option, :unselect_option, :click].freeze
11
+ EVENT_TYPES = %i[set select select_option unselect_option click].freeze
10
12
  DEFAULT_HOOK = proc {}.freeze
11
- EVENT_NOT_SUPPORTED_MSG = '%s event not supported'.freeze
13
+ EVENT_NOT_SUPPORTED_MSG = '%s event not supported'
12
14
 
13
- include SelectorMethods, Watchers, SessionMethods, WaitMethods, Locators
14
- extend Elements, SelectorMethods, Forwardable
15
+ include Locators
16
+ include WaitMethods
17
+ include SessionMethods
18
+ include Watchers
19
+ include SelectorMethods
20
+ extend Forwardable
21
+ extend SelectorMethods
22
+ extend Elements
15
23
 
16
24
  attr_reader :type, :name, :parent_element, :browser_element, :before_events, :after_events
17
25
 
@@ -24,7 +32,7 @@ module PageMagic
24
32
  # @!method before_events
25
33
  # If a block is passed in, it adds it to be run before an event is triggered on an element.
26
34
  # @see .after_events
27
- %i(after_events before_events).each do |method|
35
+ %i[after_events before_events].each do |method|
28
36
  define_method method do |&block|
29
37
  instance_variable_name = "@#{method}".to_sym
30
38
  instance_variable_value = instance_variable_get(instance_variable_name) || [DEFAULT_HOOK]
@@ -38,6 +46,7 @@ module PageMagic
38
46
  # @return [Element]
39
47
  def parent_element(page_element = nil)
40
48
  return @parent_page_element unless page_element
49
+
41
50
  @parent_page_element = page_element
42
51
  end
43
52
 
@@ -78,9 +87,7 @@ module PageMagic
78
87
  # @raise [NotSupportedException] if the wrapped Capybara element does not support the method
79
88
  EVENT_TYPES.each do |method|
80
89
  define_method method do |*args|
81
- unless browser_element.respond_to?(method)
82
- raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method
83
- end
90
+ raise NotSupportedException, EVENT_NOT_SUPPORTED_MSG % method unless browser_element.respond_to?(method)
84
91
 
85
92
  browser_element.send(method, *args)
86
93
  end
@@ -91,7 +98,8 @@ module PageMagic
91
98
  rescue ElementMissingException
92
99
  begin
93
100
  return browser_element.send(method, *args, &block) if browser_element.respond_to?(method)
94
- return parent_element.send(method, *args, &block)
101
+
102
+ parent_element.send(method, *args, &block)
95
103
  rescue NoMethodError
96
104
  super
97
105
  end
@@ -131,6 +139,7 @@ module PageMagic
131
139
  def wrap_events(raw_element)
132
140
  EVENT_TYPES.each do |action_method|
133
141
  next unless raw_element.respond_to?(action_method)
142
+
134
143
  apply_hooks(raw_element: raw_element,
135
144
  capybara_method: action_method,
136
145
  before_events: before_events,
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PageMagic
2
4
  # for the benefit of pull review :@
3
5
  class Element
4
6
  # contains method for finding element definitions
5
7
  module Locators
6
8
  # message used when raising {ElementMissingException} from methods within this module
7
- ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'.freeze
9
+ ELEMENT_NOT_DEFINED_MSG = 'Element not defined: %s'
8
10
 
9
11
  # find an element definition based on its name
10
12
  # @param [Symbol] name name of the element
@@ -13,6 +15,7 @@ module PageMagic
13
15
  def element_by_name(name, *args)
14
16
  defintion = element_definitions[name]
15
17
  raise ElementMissingException, (ELEMENT_NOT_DEFINED_MSG % name) unless defintion
18
+
16
19
  defintion.call(self, *args)
17
20
  end
18
21
 
@@ -1,45 +1,45 @@
1
- require 'capybara/query'
1
+ # frozen_string_literal: true
2
+
2
3
  module PageMagic
3
4
  class Element
4
- # class Query - models overall queries for Capybara, queries can include:
5
- # - requirements on element type
6
- # - selection criteria, modeled through the Selector class
7
- # - options
5
+ # class Query - executes query on capybara driver
8
6
  class Query
9
- class << self
10
- # Find a query using it's name
11
- # @param [Symbol] type the name of the required query in snakecase format
12
- # @return [Query] returns the predefined query with the given name
13
- def find(type)
14
- query = constants.find { |constant| constant.to_s.casecmp(type.to_s).zero? }
15
- return ELEMENT unless query
16
- const_get(query)
17
- end
18
- end
7
+ # Message template for execptions raised as a result of calling method_missing
8
+ ELEMENT_NOT_FOUND_MSG = 'Unable to find %s'
9
+
10
+ attr_reader :args, :multiple_results
19
11
 
20
- attr_reader :type
12
+ alias multiple_results? multiple_results
21
13
 
22
- # @param type -
23
- def initialize(type = nil)
24
- @type = type
14
+ def initialize(args, multiple_results: false)
15
+ @args = args
16
+ @multiple_results = multiple_results
25
17
  end
26
18
 
27
- # Build query parameters for Capybara's find method
28
- # @param [Hash] locator the location method e.g. text: 'button text'
29
- # @param [Hash] options additional options to be provided to Capybara. e.g. count: 3
30
- # @return [Array] list of compatible capybara query parameters.
31
- def build(locator, options = {})
32
- [].tap do |array|
33
- selector = Selector.find(locator.keys.first)
34
- array << selector.build(type, locator.values.first)
35
- array << options unless options.empty?
36
- end.flatten
19
+ def execute(capybara_element)
20
+ if multiple_results
21
+ capybara_element.all(*args).to_a.tap do |result|
22
+ raise Capybara::ElementNotFound if result.empty?
23
+ end
24
+ else
25
+ # TODO - make sure there is a test around this.
26
+ if args.last.is_a?(Hash)
27
+ capybara_element.find(*args[0...-1], **args.last)
28
+ else
29
+ capybara_element.find(*args)
30
+ end
31
+
32
+
33
+ end
34
+ rescue Capybara::Ambiguous => e
35
+ raise AmbiguousQueryException, e.message
36
+ rescue Capybara::ElementNotFound => e
37
+ raise ElementMissingException, e.message
37
38
  end
38
39
 
39
- ELEMENT = Query.new
40
- TEXT_FIELD = CHECKBOX = SELECT_LIST = RADIOS = TEXTAREA = Query.new(:field)
41
- LINK = Query.new(:link)
42
- BUTTON = Query.new(:button)
40
+ def ==(other)
41
+ other.respond_to?(:args) && args == other.args
42
+ end
43
43
  end
44
44
  end
45
45
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'page_magic/element/query'
4
+ module PageMagic
5
+ class Element
6
+ # class QueryBuilder - builds query to be executed on capybara driver, queries can include:
7
+ # - requirements on element type
8
+ # - selection criteria, modeled through the Selector class
9
+ # - options
10
+ class QueryBuilder
11
+ class << self
12
+ # Find a query using it's name
13
+ # @param [Symbol] type the name of the required query in snakecase format
14
+ # @return [QueryBuilder] returns the predefined query with the given name
15
+ def find(type)
16
+ query = constants.find { |constant| constant.to_s.casecmp(type.to_s).zero? }
17
+ return ELEMENT unless query
18
+
19
+ const_get(query)
20
+ end
21
+ end
22
+
23
+ attr_reader :type
24
+
25
+ # @param type -
26
+ def initialize(type = nil)
27
+ @type = type
28
+ end
29
+
30
+ # Build query parameters for Capybara's find method
31
+ # @param [Hash] locator the location method e.g. text: 'button text'
32
+ # @param [Hash] capybara_options additional options to be provided to Capybara. e.g. count: 3
33
+ # @return [Array] list of compatible capybara query parameters.
34
+ def build(locator, capybara_options = {}, multiple_results: false)
35
+ args = [].tap do |array|
36
+ selector = Selector.find(locator.keys.first)
37
+ array << selector.build(type, locator.values.first)
38
+ array << capybara_options unless capybara_options.empty?
39
+ end.flatten
40
+
41
+ Query.new(args, multiple_results: multiple_results)
42
+ end
43
+
44
+ ELEMENT = QueryBuilder.new
45
+ TEXT_FIELD =
46
+ CHECKBOX =
47
+ SELECT_LIST =
48
+ RADIO =
49
+ TEXTAREA =
50
+ FIELD =
51
+ FILE_FIELD =
52
+ FILLABLE_FIELD =
53
+ RADIO_BUTTON =
54
+ SELECT = QueryBuilder.new(:field)
55
+
56
+ LINK = QueryBuilder.new(:link)
57
+ LABEL = QueryBuilder.new(:label)
58
+ BUTTON = QueryBuilder.new(:button)
59
+ end
60
+ end
61
+ end