summaryrefslogtreecommitdiff
path: root/qa/qa/factory/base.rb
blob: e28a00c545ba84e23092be1514157897015bbdff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# frozen_string_literal: true

require 'forwardable'
require 'capybara/dsl'

module QA
  module Factory
    class Base
      extend SingleForwardable
      include ApiFabricator
      extend Capybara::DSL

      NoValueError = Class.new(RuntimeError)

      def_delegators :evaluator, :attribute

      def fabricate!(*_args)
        raise NotImplementedError
      end

      def visit!
        visit(web_url)
      end

      def populate(*attributes)
        attributes.each(&method(:public_send))
      end

      private

      def populate_attribute(name, block)
        value = attribute_value(name, block)

        raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value

        value
      end

      def attribute_value(name, block)
        api_value = api_resource&.dig(name)

        if api_value && block
          log_having_both_api_result_and_block(name, api_value)
        end

        api_value || (block && instance_exec(&block))
      end

      def log_having_both_api_result_and_block(name, api_value)
        QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
      end

      def self.fabricate!(*args, &prepare_block)
        fabricate_via_api!(*args, &prepare_block)
      rescue NotImplementedError
        fabricate_via_browser_ui!(*args, &prepare_block)
      end

      def self.fabricate_via_browser_ui!(*args, &prepare_block)
        options = args.extract_options!
        factory = options.fetch(:factory) { new }
        parents = options.fetch(:parents) { [] }

        do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
          log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }

          current_url
        end
      end

      def self.fabricate_via_api!(*args, &prepare_block)
        options = args.extract_options!
        factory = options.fetch(:factory) { new }
        parents = options.fetch(:parents) { [] }

        raise NotImplementedError unless factory.api_support?

        factory.eager_load_api_client!

        do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
          log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! }
        end
      end

      def self.do_fabricate!(factory:, prepare_block:, parents: [])
        prepare_block.call(factory) if prepare_block

        resource_web_url = yield
        factory.web_url = resource_web_url

        factory
      end
      private_class_method :do_fabricate!

      def self.log_fabrication(method, factory, parents, args)
        return yield unless Runtime::Env.debug?

        start = Time.now
        prefix = "==#{'=' * parents.size}>"
        msg = [prefix]
        msg << "Built a #{name}"
        msg << "as a dependency of #{parents.last}" if parents.any?
        msg << "via #{method} with args #{args}"

        yield.tap do
          msg << "in #{Time.now - start} seconds"
          puts msg.join(' ')
          puts if parents.empty?
        end
      end
      private_class_method :log_fabrication

      def self.evaluator
        @evaluator ||= Factory::Base::DSL.new(self)
      end
      private_class_method :evaluator

      def self.dynamic_attributes
        const_get(:DynamicAttributes)
      rescue NameError
        mod = const_set(:DynamicAttributes, Module.new)

        include mod

        mod
      end

      def self.attributes_names
        dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
      end

      class DSL
        def initialize(base)
          @base = base
        end

        def attribute(name, &block)
          @base.dynamic_attributes.module_eval do
            attr_writer(name)

            define_method(name) do
              instance_variable_get("@#{name}") ||
                instance_variable_set(
                  "@#{name}",
                  populate_attribute(name, block))
            end
          end
        end
      end

      attribute :web_url
    end
  end
end