chemlab 0.2.1 → 0.6.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 +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 +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 +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 +61 -13
  23. data/lib/chemlab/element.rb +5 -0
  24. data/lib/chemlab/page.rb +20 -2
  25. data/lib/chemlab/runtime/browser.rb +19 -25
  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 +68 -58
  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