chemlab 0.4.0 → 0.7.1

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 +8 -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 +27 -17
  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 +66 -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 -18
  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 +113 -57
  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,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/core_ext/object/deep_dup'
4
-
5
- module Chemlab
6
- # API Fabricator
7
- module ApiFabricator
8
- ResourceFabricationFailedError = Class.new(RuntimeError)
9
- ResourceNotDeletedError = Class.new(RuntimeError)
10
- ResourceNotFoundError = Class.new(RuntimeError)
11
- ResourceQueryError = Class.new(RuntimeError)
12
- ResourceUpdateFailedError = Class.new(RuntimeError)
13
- ResourceURLMissingError = Class.new(RuntimeError)
14
- InternalServerError = Class.new(RuntimeError)
15
-
16
- attr_reader :api_resource, :api_response
17
- attr_writer :api_client
18
- attr_accessor :user
19
-
20
- def api_support?
21
- respond_to?(:api_get_path) &&
22
- (respond_to?(:api_post_path) && respond_to?(:api_post_body)) ||
23
- (respond_to?(:api_put_path) && respond_to?(:api_put_body))
24
- end
25
-
26
- def fabricate_via_api!
27
- unless api_support?
28
- raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
29
- end
30
-
31
- resource_web_url(api_post)
32
- end
33
-
34
- def reload!
35
- api_get
36
-
37
- self
38
- end
39
-
40
- def remove_via_api!
41
- api_delete
42
- end
43
-
44
- def eager_load_api_client!
45
- return unless api_client.nil?
46
-
47
- api_client.tap do |client|
48
- # Eager-load the API client so that the personal token creation isn't
49
- # taken in account in the actual resource creation timing.
50
- client.user = user
51
- client.personal_access_token
52
- end
53
- end
54
-
55
- include Support::API
56
-
57
- attr_writer :api_resource, :api_response
58
-
59
- def api_put(body = api_put_body)
60
- response = put(
61
- Runtime::API::Request.new(api_client, api_put_path).url,
62
- body)
63
-
64
- unless response.code == HTTP_STATUS_OK
65
- raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
66
- end
67
-
68
- process_api_response(parse_body(response))
69
- end
70
-
71
- private
72
-
73
- def resource_web_url(resource)
74
- resource.fetch(:web_url) do
75
- raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
76
- end
77
- end
78
-
79
- def api_get
80
- process_api_response(parse_body(api_get_from(api_get_path)))
81
- end
82
-
83
- def api_get_from(get_path)
84
- url = Runtime::API::Request.new(api_client, get_path).url
85
- response = get(url)
86
-
87
- if response.code == HTTP_STATUS_SERVER_ERROR
88
- raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`."
89
- elsif response.code != HTTP_STATUS_OK
90
- raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
91
- end
92
-
93
- response
94
- end
95
-
96
- def api_post
97
- response = post(
98
- Runtime::API::Request.new(api_client, api_post_path).url,
99
- api_post_body)
100
-
101
- unless response.code == HTTP_STATUS_CREATED
102
- raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
103
- end
104
-
105
- process_api_response(parse_body(response))
106
- end
107
-
108
- def api_delete
109
- url = Runtime::API::Request.new(api_client, api_delete_path).url
110
- response = delete(url)
111
-
112
- unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
113
- raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
114
- end
115
-
116
- response
117
- end
118
-
119
- def api_client
120
- @api_client ||= begin
121
- Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'), user: user)
122
- end
123
- end
124
-
125
- def process_api_response(parsed_response)
126
- self.api_response = parsed_response
127
- self.api_resource = transform_api_resource(parsed_response.deep_dup)
128
- end
129
-
130
- def transform_api_resource(api_resource)
131
- api_resource
132
- end
133
- end
134
- end
@@ -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