page_ez 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.lock +148 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +575 -0
  9. data/Rakefile +10 -0
  10. data/lib/page_ez/configuration.rb +33 -0
  11. data/lib/page_ez/delegates_to.rb +25 -0
  12. data/lib/page_ez/errors.rb +17 -0
  13. data/lib/page_ez/has_many_result.rb +35 -0
  14. data/lib/page_ez/has_one_result.rb +14 -0
  15. data/lib/page_ez/method_generators/define_has_many_result_methods.rb +48 -0
  16. data/lib/page_ez/method_generators/define_has_one_predicate_methods.rb +42 -0
  17. data/lib/page_ez/method_generators/define_has_one_result_methods.rb +35 -0
  18. data/lib/page_ez/method_generators/has_many_dynamic_selector.rb +39 -0
  19. data/lib/page_ez/method_generators/has_many_ordered_dynamic_selector.rb +39 -0
  20. data/lib/page_ez/method_generators/has_many_ordered_selector.rb +47 -0
  21. data/lib/page_ez/method_generators/has_many_static_selector.rb +41 -0
  22. data/lib/page_ez/method_generators/has_one_composed_class.rb +40 -0
  23. data/lib/page_ez/method_generators/has_one_dynamic_selector.rb +39 -0
  24. data/lib/page_ez/method_generators/has_one_static_selector.rb +25 -0
  25. data/lib/page_ez/method_generators/identity_processor.rb +11 -0
  26. data/lib/page_ez/null_logger.rb +12 -0
  27. data/lib/page_ez/options.rb +37 -0
  28. data/lib/page_ez/page.rb +162 -0
  29. data/lib/page_ez/page_visitor.rb +72 -0
  30. data/lib/page_ez/parameters.rb +54 -0
  31. data/lib/page_ez/pluralization.rb +25 -0
  32. data/lib/page_ez/selector_evaluator.rb +76 -0
  33. data/lib/page_ez/version.rb +5 -0
  34. data/lib/page_ez/visitors/debug_visitor.rb +59 -0
  35. data/lib/page_ez/visitors/depth_visitor.rb +44 -0
  36. data/lib/page_ez/visitors/macro_pluralization_visitor.rb +70 -0
  37. data/lib/page_ez/visitors/matcher_collision_visitor.rb +75 -0
  38. data/lib/page_ez/visitors/registered_name_visitor.rb +100 -0
  39. data/lib/page_ez.rb +41 -0
  40. data/page_ez.gemspec +43 -0
  41. metadata +238 -0
@@ -0,0 +1,72 @@
1
+ module PageEz
2
+ class PageVisitor
3
+ def initialize
4
+ @visitors = [
5
+ Visitors::DebugVisitor.new,
6
+ Visitors::RegisteredNameVisitor.new,
7
+ Visitors::MacroPluralizationVisitor.new,
8
+ Visitors::MatcherCollisionVisitor.new
9
+ ]
10
+ end
11
+
12
+ def begin_block_evaluation
13
+ @visitors.each do |visitor|
14
+ visitor.begin_block_evaluation
15
+ end
16
+ end
17
+
18
+ def end_block_evaluation
19
+ @visitors.each do |visitor|
20
+ visitor.end_block_evaluation
21
+ end
22
+ end
23
+
24
+ def define_method(name)
25
+ @visitors.each do |visitor|
26
+ visitor.define_method(name)
27
+ end
28
+ end
29
+
30
+ def inherit_from(subclass)
31
+ @visitors.each do |visitor|
32
+ visitor.inherit_from(subclass)
33
+ end
34
+ end
35
+
36
+ def track_method_added(name, construction_strategy)
37
+ @visitors.each do |visitor|
38
+ visitor.track_method_added(name, construction_strategy)
39
+ end
40
+ end
41
+
42
+ def track_method_undefined(name)
43
+ @visitors.each do |visitor|
44
+ visitor.track_method_undefined(name)
45
+ end
46
+ end
47
+
48
+ def track_method_renamed(from, to)
49
+ @visitors.each do |visitor|
50
+ visitor.track_method_renamed(from, to)
51
+ end
52
+ end
53
+
54
+ def track_method_delegated(name)
55
+ @visitors.each do |visitor|
56
+ visitor.track_method_delegated(name)
57
+ end
58
+ end
59
+
60
+ def process_macro(macro, name, construction_strategy)
61
+ @visitors.each do |visitor|
62
+ visitor.process_macro(macro, name, construction_strategy)
63
+ end
64
+ end
65
+
66
+ def reset
67
+ @visitors.each do |visitor|
68
+ visitor.reset
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ module PageEz
2
+ class Parameters
3
+ def self.build(block)
4
+ if block&.respond_to?(:parameters) && block&.respond_to?(:arity)
5
+ new(block)
6
+ else
7
+ NullParameters.new
8
+ end
9
+ end
10
+
11
+ def keyword_args
12
+ @block.parameters.filter_map do |type, name|
13
+ param = Parameter.new(type, name)
14
+ param.name if param.kwarg?
15
+ end
16
+ end
17
+
18
+ def non_keyword_args
19
+ @block.parameters.filter_map do |type, name|
20
+ param = Parameter.new(type, name)
21
+ param.name if !param.kwarg?
22
+ end
23
+ end
24
+
25
+ private_class_method :new
26
+
27
+ def initialize(block)
28
+ @block = block
29
+ end
30
+
31
+ class NullParameters
32
+ def keyword_args
33
+ []
34
+ end
35
+
36
+ def non_keyword_args
37
+ []
38
+ end
39
+ end
40
+
41
+ class Parameter
42
+ attr_reader :name
43
+
44
+ def initialize(type, name)
45
+ @type = type
46
+ @name = name
47
+ end
48
+
49
+ def kwarg?
50
+ @type == :keyreq || @type == :key
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
3
+ module PageEz
4
+ class Pluralization
5
+ def initialize(word)
6
+ @word = word.to_s
7
+ end
8
+
9
+ def singularize
10
+ @word.singularize
11
+ end
12
+
13
+ def pluralize
14
+ @word.pluralize
15
+ end
16
+
17
+ def plural?
18
+ @word == pluralize && @word != singularize
19
+ end
20
+
21
+ def singular?
22
+ !plural?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,76 @@
1
+ module PageEz
2
+ class SelectorEvaluator
3
+ def self.build(name, dynamic_options:, options:, selector:)
4
+ run_def = ->(args, target:) do
5
+ PageEz::SelectorEvaluator.new(name, args, dynamic_options: dynamic_options, options: options, selector: selector, target: target)
6
+ end
7
+
8
+ name_def = -> { name }
9
+
10
+ Class.new.tap do |klass|
11
+ klass.define_singleton_method(:run, &run_def)
12
+ klass.define_singleton_method(:name, &name_def)
13
+ end
14
+ end
15
+
16
+ def initialize(name, args, dynamic_options:, options:, selector:, target:)
17
+ @name = name
18
+ @args = args
19
+ @dynamic_options = dynamic_options
20
+ @options = options
21
+ @selector = if selector.respond_to?(:bind)
22
+ selector.bind(target)
23
+ else
24
+ selector
25
+ end
26
+ end
27
+
28
+ def selector
29
+ if dynamic_selector?
30
+ if selector_args.none?
31
+ @selector.call(**kwargs.slice(*selector_kwargs))
32
+ else
33
+ @selector.call(*args[0..selector_args.length - 1], **kwargs.slice(*selector_kwargs))
34
+ end
35
+ else
36
+ @selector
37
+ end
38
+ end
39
+
40
+ def options
41
+ Options.merge(@options, @dynamic_options, *args[selector_args.length..])
42
+ end
43
+
44
+ private
45
+
46
+ def args
47
+ if @args.any? && kwargs.any?
48
+ cloned_args = @args.dup
49
+ cloned_args[-1] = kwargs.except(*selector_kwargs).merge(kwargs.slice(*dynamic_kwargs))
50
+ cloned_args
51
+ else
52
+ @args
53
+ end
54
+ end
55
+
56
+ def dynamic_selector?
57
+ @selector.respond_to?(:parameters) && @selector.respond_to?(:call)
58
+ end
59
+
60
+ def dynamic_kwargs
61
+ Parameters.build(@dynamic_options).keyword_args
62
+ end
63
+
64
+ def selector_kwargs
65
+ Parameters.build(@selector).keyword_args
66
+ end
67
+
68
+ def selector_args
69
+ Parameters.build(@selector).non_keyword_args
70
+ end
71
+
72
+ def kwargs
73
+ @args.last.is_a?(Hash) ? @args.last : {}
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageEz
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,59 @@
1
+ module PageEz
2
+ module Visitors
3
+ class DebugVisitor
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ def begin_block_evaluation
9
+ @depth_visitor.begin_block_evaluation
10
+ end
11
+
12
+ def end_block_evaluation
13
+ @depth_visitor.end_block_evaluation
14
+ end
15
+
16
+ def define_method(name)
17
+ @depth_visitor.define_method(name)
18
+ debug("* #{name}")
19
+ end
20
+
21
+ def inherit_from(subclass)
22
+ @depth_visitor.inherit_from(subclass)
23
+ debug("Declaring page object: #{subclass.name || "{anonymous page object}"}")
24
+ end
25
+
26
+ def track_method_added(name, construction_strategy)
27
+ @depth_visitor.track_method_added(name, construction_strategy)
28
+ end
29
+
30
+ def track_method_undefined(name)
31
+ @depth_visitor.track_method_undefined(name)
32
+ end
33
+
34
+ def track_method_renamed(from, to)
35
+ @depth_visitor.track_method_renamed(from, to)
36
+ end
37
+
38
+ def track_method_delegated(name)
39
+ @depth_visitor.track_method_delegated(name)
40
+ debug("* #{name} (delegated)")
41
+ end
42
+
43
+ def process_macro(macro, name, construction_strategy)
44
+ @depth_visitor.process_macro(macro, name, construction_strategy)
45
+ debug("#{macro} :#{name}, \"#{construction_strategy.selector}\"")
46
+ end
47
+
48
+ def reset
49
+ @depth_visitor = DepthVisitor.new
50
+ end
51
+
52
+ private
53
+
54
+ def debug(message)
55
+ PageEz.configuration.logger.debug("#{" " * @depth_visitor.depth}#{message}")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ module PageEz
2
+ module Visitors
3
+ class DepthVisitor
4
+ attr_reader :depth
5
+
6
+ def initialize
7
+ reset
8
+ end
9
+
10
+ def begin_block_evaluation
11
+ @depth += 1
12
+ end
13
+
14
+ def end_block_evaluation
15
+ @depth -= 1
16
+ end
17
+
18
+ def define_method(name)
19
+ end
20
+
21
+ def inherit_from(subclass)
22
+ end
23
+
24
+ def track_method_added(*)
25
+ end
26
+
27
+ def track_method_undefined(*)
28
+ end
29
+
30
+ def track_method_renamed(*)
31
+ end
32
+
33
+ def track_method_delegated(*)
34
+ end
35
+
36
+ def process_macro(*)
37
+ end
38
+
39
+ def reset
40
+ @depth = 0
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,70 @@
1
+ module PageEz
2
+ module Visitors
3
+ class MacroPluralizationVisitor
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ def begin_block_evaluation
9
+ @depth_visitor.begin_block_evaluation
10
+ end
11
+
12
+ def end_block_evaluation
13
+ @depth_visitor.end_block_evaluation
14
+ end
15
+
16
+ def define_method(name)
17
+ @depth_visitor.define_method(name)
18
+ end
19
+
20
+ def inherit_from(subclass)
21
+ @depth_visitor.inherit_from(subclass)
22
+ end
23
+
24
+ def track_method_added(name, construction_strategy)
25
+ @depth_visitor.track_method_added(name, construction_strategy)
26
+ end
27
+
28
+ def track_method_undefined(name)
29
+ @depth_visitor.track_method_undefined(name)
30
+ end
31
+
32
+ def track_method_renamed(from, to)
33
+ @depth_visitor.track_method_renamed(from, to)
34
+ end
35
+
36
+ def track_method_delegated(name)
37
+ @depth_visitor.track_method_delegated(name)
38
+ end
39
+
40
+ def process_macro(macro, name, construction_strategy)
41
+ @depth_visitor.process_macro(macro, name, construction_strategy)
42
+ rendered = "#{macro} :#{name}, \"#{construction_strategy.selector}\""
43
+
44
+ message = case [macro, Pluralization.new(name).singular? ? :singular : :plural]
45
+ in [:has_one, :plural]
46
+ "consider singularizing :#{name} in #{rendered}"
47
+ in [:has_many, :singular]
48
+ "consider pluralizing :#{name} in #{rendered}"
49
+ in [:has_many_ordered, :singular]
50
+ "consider pluralizing :#{name} in #{rendered}"
51
+ in _
52
+ end
53
+
54
+ if message
55
+ message = "#{" " * @depth_visitor.depth}#{message}"
56
+ case PageEz.configuration.on_pluralization_mismatch
57
+ when :warn
58
+ PageEz.configuration.logger.warn(message)
59
+ when :raise
60
+ raise PluralizationMismatchError, message
61
+ end
62
+ end
63
+ end
64
+
65
+ def reset
66
+ @depth_visitor = DepthVisitor.new
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,75 @@
1
+ module PageEz
2
+ module Visitors
3
+ class MatcherCollisionVisitor
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ def begin_block_evaluation
9
+ @depth_visitor.begin_block_evaluation
10
+ end
11
+
12
+ def end_block_evaluation
13
+ @depth_visitor.end_block_evaluation
14
+ end
15
+
16
+ def define_method(name)
17
+ @depth_visitor.define_method(name)
18
+ end
19
+
20
+ def inherit_from(subclass)
21
+ @depth_visitor.inherit_from(subclass)
22
+ end
23
+
24
+ def track_method_added(name, construction_strategy)
25
+ @depth_visitor.track_method_added(name, construction_strategy)
26
+ end
27
+
28
+ def track_method_undefined(name)
29
+ @depth_visitor.track_method_undefined(name)
30
+ end
31
+
32
+ def track_method_renamed(from, to)
33
+ @depth_visitor.track_method_renamed(from, to)
34
+ end
35
+
36
+ def track_method_delegated(name)
37
+ @depth_visitor.track_method_delegated(name)
38
+ end
39
+
40
+ def process_macro(macro, name, construction_strategy)
41
+ @depth_visitor.process_macro(macro, name, construction_strategy)
42
+ if existing_matchers.include?(name.to_s)
43
+ rendered_macro = "#{macro} :#{name}, \"#{construction_strategy.selector}\""
44
+ whitespace = " " * @depth_visitor.depth
45
+ message = "#{whitespace}#{rendered_macro} will conflict with Capybara's `have_#{name}` matcher"
46
+
47
+ case PageEz.configuration.on_matcher_collision
48
+ when :warn
49
+ PageEz.configuration.logger.warn(message)
50
+ when :raise
51
+ raise MatcherCollisionError, message
52
+ end
53
+ end
54
+ end
55
+
56
+ def reset
57
+ @depth_visitor = DepthVisitor.new
58
+ end
59
+
60
+ private
61
+
62
+ def existing_matchers
63
+ if defined?(Capybara::RSpecMatchers)
64
+ Capybara::RSpecMatchers.instance_methods.filter_map do |method_name|
65
+ if (match = method_name.to_s.match(/^have_(?!no_)(.+)$/))
66
+ match[1]
67
+ end
68
+ end
69
+ else
70
+ []
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,100 @@
1
+ module PageEz
2
+ module Visitors
3
+ class RegisteredNameVisitor
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ def begin_block_evaluation
9
+ @parents.push(@current_strategy)
10
+ end
11
+
12
+ def end_block_evaluation
13
+ @parents.pop
14
+ end
15
+
16
+ def define_method(*)
17
+ end
18
+
19
+ def inherit_from(*)
20
+ end
21
+
22
+ def track_method_undefined(name)
23
+ current_all_methods.delete(name)
24
+ end
25
+
26
+ def track_method_renamed(from, to)
27
+ current_renamed_methods.push(from)
28
+ end
29
+
30
+ def track_method_delegated(name)
31
+ if current_all_methods.count { _1 == name } > 1
32
+ raise DuplicateElementDeclarationError, "duplicate element :#{name} declared"
33
+ end
34
+
35
+ current_delegated_methods.push(name)
36
+ end
37
+
38
+ def track_method_added(name, construction_strategy)
39
+ @declared_constructors[parent_id] ||= []
40
+
41
+ found = @declared_constructors[parent_id].find { _1 == [name, construction_strategy] }
42
+
43
+ if found && found[1]&.selector_type != :dynamic
44
+ raise DuplicateElementDeclarationError, "duplicate element :#{name} declared"
45
+ end
46
+
47
+ if current_renamed_methods.include?(name) && current_all_methods.include?(name)
48
+ raise DuplicateElementDeclarationError, "duplicate element :#{name} declared"
49
+ end
50
+
51
+ if current_delegated_methods.include?(name) && current_all_methods.include?(name)
52
+ raise DuplicateElementDeclarationError, "duplicate element :#{name} declared"
53
+ end
54
+
55
+ current_all_methods.push(name)
56
+ end
57
+
58
+ def process_macro(_, name, construction_strategy)
59
+ @declared_constructors[parent_id] ||= []
60
+
61
+ if @declared_constructors[parent_id].map { _1.first }.include?(name)
62
+ raise DuplicateElementDeclarationError, "duplicate element :#{name} declared"
63
+ end
64
+
65
+ @declared_constructors[parent_id] += [[name, construction_strategy]]
66
+ @current_strategy = construction_strategy
67
+ end
68
+
69
+ def reset
70
+ @parents = []
71
+ @current_strategy = nil
72
+ @declared_constructors = {}
73
+ @all_methods = {}
74
+ @delegated_methods = {}
75
+ @renamed_methods = {}
76
+ end
77
+
78
+ private
79
+
80
+ def parent_id
81
+ @parents.last.object_id
82
+ end
83
+
84
+ def current_renamed_methods
85
+ @renamed_methods[parent_id] ||= []
86
+ @renamed_methods[parent_id]
87
+ end
88
+
89
+ def current_all_methods
90
+ @all_methods[parent_id] ||= []
91
+ @all_methods[parent_id]
92
+ end
93
+
94
+ def current_delegated_methods
95
+ @delegated_methods[parent_id] ||= []
96
+ @delegated_methods[parent_id]
97
+ end
98
+ end
99
+ end
100
+ end
data/lib/page_ez.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "page_ez/version"
4
+ require_relative "page_ez/errors"
5
+ require_relative "page_ez/configuration"
6
+ require_relative "page_ez/null_logger"
7
+ require_relative "page_ez/delegates_to"
8
+ require_relative "page_ez/parameters"
9
+ require_relative "page_ez/selector_evaluator"
10
+ require_relative "page_ez/method_generators/define_has_many_result_methods"
11
+ require_relative "page_ez/method_generators/define_has_one_result_methods"
12
+ require_relative "page_ez/method_generators/define_has_one_predicate_methods"
13
+ require_relative "page_ez/method_generators/identity_processor"
14
+ require_relative "page_ez/method_generators/has_one_static_selector"
15
+ require_relative "page_ez/method_generators/has_one_dynamic_selector"
16
+ require_relative "page_ez/method_generators/has_one_composed_class"
17
+ require_relative "page_ez/method_generators/has_many_dynamic_selector"
18
+ require_relative "page_ez/method_generators/has_many_ordered_dynamic_selector"
19
+ require_relative "page_ez/method_generators/has_many_ordered_selector"
20
+ require_relative "page_ez/method_generators/has_many_static_selector"
21
+ require_relative "page_ez/visitors/matcher_collision_visitor"
22
+ require_relative "page_ez/visitors/depth_visitor"
23
+ require_relative "page_ez/visitors/debug_visitor"
24
+ require_relative "page_ez/visitors/registered_name_visitor"
25
+ require_relative "page_ez/visitors/macro_pluralization_visitor"
26
+ require_relative "page_ez/page_visitor"
27
+ require_relative "page_ez/page"
28
+ require_relative "page_ez/pluralization"
29
+ require_relative "page_ez/options"
30
+ require_relative "page_ez/has_one_result"
31
+ require_relative "page_ez/has_many_result"
32
+
33
+ module PageEz
34
+ def self.configuration
35
+ @configuration ||= Configuration.new
36
+ end
37
+
38
+ def self.configure
39
+ yield configuration if block_given?
40
+ end
41
+ end
data/page_ez.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/page_ez/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "page_ez"
7
+ spec.version = PageEz::VERSION
8
+ spec.authors = ["Josh Clayton"]
9
+ spec.email = ["joshua.clayton@gmail.com"]
10
+
11
+ spec.summary = "PageEz is a tool to define page objects with Capybara"
12
+ spec.description = "PageEz is a tool to define page objects with Capybara"
13
+ spec.homepage = "https://github.com/joshuaclayton/page_ez"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/joshuaclayton/page_ez"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_runtime_dependency "capybara", "~> 3.0"
32
+ spec.add_runtime_dependency "activesupport", ">= 5.0"
33
+
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ spec.add_development_dependency "standard", "~> 1.3"
37
+ spec.add_development_dependency "sinatra", "~> 3.0"
38
+ spec.add_development_dependency "selenium-webdriver", "~> 4.10"
39
+ spec.add_development_dependency "puma", "~> 6.3"
40
+ spec.add_development_dependency "launchy", "~> 2.5"
41
+ spec.add_development_dependency "simplecov", "~> 0.22"
42
+ spec.add_development_dependency "simplecov-lcov", "~> 0.8"
43
+ end