page_ez 0.1.0

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.
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