chemlab 0.5.0 → 0.6.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 +4 -4
  2. data/Rakefile +6 -0
  3. data/bin/chemlab +10 -0
  4. data/bin/chemlab-suite +1 -0
  5. data/bin/chemlab-test +1 -0
  6. data/lib/chemlab.rb +11 -13
  7. data/lib/chemlab/attributable.rb +16 -10
  8. data/lib/chemlab/cli/fixtures/new_library/.gitignore +63 -0
  9. data/lib/chemlab/cli/fixtures/new_library/Gemfile +5 -0
  10. data/lib/chemlab/cli/fixtures/new_library/README.md.erb +1 -0
  11. data/lib/chemlab/cli/fixtures/new_library/lib/new_library.rb.erb +7 -0
  12. data/lib/chemlab/cli/fixtures/new_library/lib/page/sample.rb.erb +9 -0
  13. data/lib/chemlab/cli/fixtures/new_library/new_library.gemspec.erb +23 -0
  14. data/lib/chemlab/cli/fixtures/new_library/spec/integration/page/sample_spec.rb.erb +17 -0
  15. data/lib/chemlab/cli/fixtures/new_library/spec/unit/page/sample_spec.rb.erb +19 -0
  16. data/lib/chemlab/cli/generator.rb +46 -0
  17. data/lib/chemlab/cli/generator/templates/page.erb +3 -0
  18. data/lib/chemlab/cli/new_library.rb +62 -0
  19. data/lib/chemlab/cli/stub.erb +65 -0
  20. data/lib/chemlab/cli/stubber.rb +74 -0
  21. data/lib/chemlab/component.rb +78 -8
  22. data/lib/chemlab/configuration.rb +60 -12
  23. data/lib/chemlab/element.rb +4 -0
  24. data/lib/chemlab/page.rb +19 -1
  25. data/lib/chemlab/runtime/browser.rb +13 -17
  26. data/lib/chemlab/runtime/env.rb +13 -9
  27. data/lib/chemlab/runtime/logger.rb +16 -13
  28. data/lib/chemlab/version.rb +1 -1
  29. data/lib/tasks/generate.rake +22 -0
  30. data/lib/tasks/generate_stubs.rake +20 -0
  31. data/lib/tasks/help.rake +24 -0
  32. data/lib/tasks/new.rake +19 -0
  33. data/lib/tasks/version.rake +8 -0
  34. metadata +87 -49
  35. data/lib/chemlab/api_fabricator.rb +0 -134
  36. data/lib/chemlab/resource.rb +0 -169
  37. data/lib/chemlab/runtime/api_client.rb +0 -18
  38. data/lib/chemlab/support/api.rb +0 -71
  39. data/lib/chemlab/support/logging.rb +0 -176
  40. data/lib/chemlab/support/repeater.rb +0 -65
  41. data/lib/chemlab/support/waiter.rb +0 -39
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/core_ext/array/extract_options'
4
-
5
- module Chemlab
6
- class Resource
7
- extend SingleForwardable
8
- include ApiFabricator
9
-
10
- NoValueError = Class.new(RuntimeError)
11
-
12
- def_delegators :evaluator, :attribute
13
-
14
- def initialize(api_client)
15
- @api_client = api_client
16
- end
17
-
18
- def self.fabricate!(*args, &prepare_block)
19
- fabricate_via_api!(*args, &prepare_block)
20
- rescue NotImplementedError
21
- fabricate_via_browser_ui!(*args, &prepare_block)
22
- end
23
-
24
- def self.fabricate_via_browser_ui!(*args, &prepare_block)
25
- options = args.extract_options!
26
- resource = options.fetch(:resource) { new }
27
- parents = options.fetch(:parents) { [] }
28
-
29
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
30
- log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
31
-
32
- current_url
33
- end
34
- end
35
-
36
- def self.fabricate_via_api!(*args, &prepare_block)
37
- options = args.extract_options!
38
- resource = options.fetch(:resource) { new }
39
- parents = options.fetch(:parents) { [] }
40
-
41
- raise NotImplementedError unless resource.api_support?
42
-
43
- resource.eager_load_api_client!
44
-
45
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
46
- log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
47
- end
48
- end
49
-
50
- def self.remove_via_api!(*args, &prepare_block)
51
- options = args.extract_options!
52
- resource = options.fetch(:resource) { new }
53
- parents = options.fetch(:parents) { [] }
54
-
55
- resource.eager_load_api_client!
56
-
57
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
58
- log_fabrication(:api, resource, parents, args) { resource.remove_via_api! }
59
- end
60
- end
61
-
62
- def fabricate!(*_args)
63
- raise NotImplementedError
64
- end
65
-
66
- def visit!
67
- Runtime::Logger.debug(%Q[Visiting #{self.class.name} at "#{web_url}"])
68
-
69
- # Just in case an async action is not yet complete
70
- Support::WaitForRequests.wait_for_requests
71
-
72
- Support::Retrier.retry_until do
73
- visit(web_url)
74
- wait_until { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
75
- end
76
- end
77
-
78
- def populate(*attributes)
79
- attributes.each(&method(:public_send))
80
- end
81
-
82
- def wait_until(max_duration: 60, sleep_interval: 0.1)
83
- Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval) do
84
- yield
85
- end
86
- end
87
-
88
- private
89
-
90
- def populate_attribute(name, block)
91
- value = attribute_value(name, block)
92
-
93
- raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
94
-
95
- value
96
- end
97
-
98
- def attribute_value(name, block)
99
- api_value = api_resource&.dig(name)
100
-
101
- if api_value && block
102
- log_having_both_api_result_and_block(name, api_value)
103
- end
104
-
105
- api_value || (block && instance_exec(&block))
106
- end
107
-
108
- def log_having_both_api_result_and_block(name, api_value)
109
- 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."
110
- end
111
-
112
- def self.do_fabricate!(resource:, prepare_block:, parents: [])
113
- prepare_block.call(resource) if prepare_block
114
-
115
- resource_web_url = yield
116
- resource.web_url = resource_web_url
117
-
118
- resource
119
- end
120
-
121
- private_class_method :do_fabricate!
122
-
123
- def self.log_fabrication(method, resource, parents, args)
124
- return yield unless Runtime::Env.debug?
125
-
126
- start = Time.now
127
- prefix = "==#{'=' * parents.size}>"
128
- msg = [prefix]
129
- msg << "Built a #{name}"
130
- msg << "as a dependency of #{parents.last}" if parents.any?
131
- msg << "via #{method}"
132
-
133
- yield.tap do
134
- msg << "in #{Time.now - start} seconds"
135
- puts msg.join(' ')
136
- puts if parents.empty?
137
- end
138
- end
139
-
140
- private_class_method :log_fabrication
141
-
142
- def self.evaluator
143
- @evaluator ||= DSL.new(self)
144
- end
145
-
146
- private_class_method :evaluator
147
-
148
- class DSL
149
- def initialize(base)
150
- @base = base
151
- end
152
-
153
- def attribute(name, &block)
154
- @base.module_eval do
155
- attr_writer(name)
156
-
157
- define_method(name) do
158
- instance_variable_get("@#{name}") ||
159
- instance_variable_set(
160
- "@#{name}",
161
- populate_attribute(name, block))
162
- end
163
- end
164
- end
165
- end
166
-
167
- attribute :web_url
168
- end
169
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'airborne'
4
-
5
- module Chemlab
6
- module Runtime
7
- module API
8
- class Client
9
- attr_reader :address, :user
10
-
11
- def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil, ip_limits: false)
12
- @address = address
13
- @user = user
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Chemlab
4
- module Support
5
- module API
6
- HTTP_STATUS_OK = 200
7
- HTTP_STATUS_CREATED = 201
8
- HTTP_STATUS_NO_CONTENT = 204
9
- HTTP_STATUS_ACCEPTED = 202
10
- HTTP_STATUS_SERVER_ERROR = 500
11
-
12
- def post(url, payload)
13
- RestClient::Request.execute(
14
- method: :post,
15
- url: url,
16
- payload: payload,
17
- verify_ssl: false)
18
- rescue RestClient::ExceptionWithResponse => e
19
- return_response_or_raise(e)
20
- end
21
-
22
- def get(url, raw_response: false)
23
- RestClient::Request.execute(
24
- method: :get,
25
- url: url,
26
- verify_ssl: false,
27
- raw_response: raw_response)
28
- rescue RestClient::ExceptionWithResponse => e
29
- return_response_or_raise(e)
30
- end
31
-
32
- def put(url, payload)
33
- RestClient::Request.execute(
34
- method: :put,
35
- url: url,
36
- payload: payload,
37
- verify_ssl: false)
38
- rescue RestClient::ExceptionWithResponse => e
39
- return_response_or_raise(e)
40
- end
41
-
42
- def delete(url)
43
- RestClient::Request.execute(
44
- method: :delete,
45
- url: url,
46
- verify_ssl: false)
47
- rescue RestClient::ExceptionWithResponse => e
48
- return_response_or_raise(e)
49
- end
50
-
51
- def head(url)
52
- RestClient::Request.execute(
53
- method: :head,
54
- url: url,
55
- verify_ssl: false)
56
- rescue RestClient::ExceptionWithResponse => e
57
- return_response_or_raise(e)
58
- end
59
-
60
- def parse_body(response)
61
- JSON.parse(response.body, symbolize_names: true)
62
- end
63
-
64
- def return_response_or_raise(error)
65
- raise error unless error.respond_to?(:response) && error.response
66
-
67
- error.response
68
- end
69
- end
70
- end
71
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Chemlab
4
- module Support
5
- module Logging
6
- def assert_no_element(name)
7
- log("asserting no element :#{name}")
8
-
9
- super
10
- end
11
-
12
- def refresh
13
- log("refreshing #{current_url}")
14
-
15
- super
16
- end
17
-
18
- def scroll_to(selector, text: nil)
19
- msg = "scrolling to :#{selector}"
20
- msg += " with text: #{text}" if text
21
- log(msg)
22
-
23
- super
24
- end
25
-
26
- def asset_exists?(url)
27
- exists = super
28
-
29
- log("asset_exists? #{url} returned #{exists}")
30
-
31
- exists
32
- end
33
-
34
- def find_element(name, **kwargs)
35
- log("finding :#{name} with args #{kwargs}")
36
-
37
- element = super
38
-
39
- log("found :#{name}") if element
40
-
41
- element
42
- end
43
-
44
- def all_elements(name, **kwargs)
45
- log("finding all :#{name} with args #{kwargs}")
46
-
47
- elements = super
48
-
49
- log("found #{elements.size} :#{name}") if elements
50
-
51
- elements
52
- end
53
-
54
- def check_element(name)
55
- log("checking :#{name}")
56
-
57
- super
58
- end
59
-
60
- def uncheck_element(name)
61
- log("unchecking :#{name}")
62
-
63
- super
64
- end
65
-
66
- def click_element(name, page = nil, **kwargs)
67
- msg = ["clicking :#{name}"]
68
- msg << ", expecting to be at #{page.class}" if page
69
- msg << "with args #{kwargs}"
70
-
71
- log(msg.compact.join(' '))
72
-
73
- super
74
- end
75
-
76
- def fill_element(name, content)
77
- masked_content = name.to_s.include?('password') ? '*****' : content
78
-
79
- log(%Q(filling :#{name} with "#{masked_content}"))
80
-
81
- super
82
- end
83
-
84
- def select_element(name, value)
85
- log(%Q(selecting "#{value}" in :#{name}))
86
-
87
- super
88
- end
89
-
90
- def has_element?(name, **kwargs)
91
- found = super
92
-
93
- log_has_element_or_not('has_element?', name, found, **kwargs)
94
-
95
- found
96
- end
97
-
98
- def has_no_element?(name, **kwargs)
99
- found = super
100
-
101
- log_has_element_or_not('has_no_element?', name, found, **kwargs)
102
-
103
- found
104
- end
105
-
106
- def has_text?(text, **kwargs)
107
- found = super
108
-
109
- log(%Q{has_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}})
110
-
111
- found
112
- end
113
-
114
- def has_no_text?(text, **kwargs)
115
- found = super
116
-
117
- log(%Q{has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}})
118
-
119
- found
120
- end
121
-
122
- def finished_loading?
123
- log('waiting for loading to complete...')
124
- now = Time.now
125
-
126
- loaded = super
127
-
128
- log("loading complete after #{Time.now - now} seconds")
129
-
130
- loaded
131
- end
132
-
133
- def wait_for_animated_element(name)
134
- log("waiting for animated element: #{name}")
135
-
136
- super
137
- end
138
-
139
- def within_element(name, text: nil)
140
- log("within element :#{name}")
141
-
142
- element = super
143
-
144
- log("end within element :#{name}")
145
-
146
- element
147
- end
148
-
149
- def within_element_by_index(name, index)
150
- log("within elements :#{name} at index #{index}")
151
-
152
- element = super
153
-
154
- log("end within elements :#{name} at index #{index}")
155
-
156
- element
157
- end
158
-
159
- private
160
-
161
- def log(msg)
162
- Runtime::Logger.debug(msg)
163
- end
164
-
165
- def log_has_element_or_not(method, name, found, **kwargs)
166
- msg = ["#{method} :#{name}"]
167
- msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text]
168
- msg << "class: #{kwargs[:class]}" if kwargs[:class]
169
- msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})"
170
- msg << "returned: #{found}"
171
-
172
- log(msg.compact.join(' '))
173
- end
174
- end
175
- end
176
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/inflector'
4
-
5
- module Chemlab
6
- module Support
7
- module Repeater
8
- DEFAULT_MAX_WAIT_TIME = 60
9
-
10
- RetriesExceededError = Class.new(RuntimeError)
11
- WaitExceededError = Class.new(RuntimeError)
12
-
13
- def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
14
- attempts = 0
15
- start = Time.now
16
-
17
- begin
18
- while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
19
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts && log
20
-
21
- result = yield
22
- return result if result
23
-
24
- sleep_and_reload_if_needed(sleep_interval, reload_page)
25
- attempts += 1
26
- end
27
- rescue StandardError, RSpec::Expectations::ExpectationNotMetError
28
- raise unless retry_on_exception
29
-
30
- attempts += 1
31
- if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
32
- sleep_and_reload_if_needed(sleep_interval, reload_page)
33
-
34
- retry
35
- else
36
- raise
37
- end
38
- end
39
-
40
- if raise_on_failure
41
- raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts)
42
-
43
- raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
44
- end
45
-
46
- false
47
- end
48
-
49
- private
50
-
51
- def sleep_and_reload_if_needed(sleep_interval, reload_page)
52
- sleep(sleep_interval)
53
- reload_page.refresh if reload_page
54
- end
55
-
56
- def remaining_attempts?(attempts, max_attempts)
57
- max_attempts ? attempts < max_attempts : true
58
- end
59
-
60
- def remaining_time?(start, max_duration)
61
- max_duration ? Time.now - start < max_duration : true
62
- end
63
- end
64
- end
65
- end