cyperful 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e6cdf194b4606ed00df58d8b28820efb2bcf69f5a3f636ec5fed096fc8ce84e
4
- data.tar.gz: 98ebee3f3b456c6d2fcaa905b19e1408503e1c2341926dd9d2b808c31287ef42
3
+ metadata.gz: e580a31f09fc53e3d99ccbd3b3899d187d61f87eaba1793bf86483660e4c4c72
4
+ data.tar.gz: 89d517bd16f53772d3ea02b27bf0a195f7540958ee67b77ad8b2435b2cd8fe7c
5
5
  SHA512:
6
- metadata.gz: da33579e39387049c628a5d6a1d52dac4d56fffdc754671fd3928d22bb9cdf4149d597637beab80413e68eac719b6ee4607fc88acc39803568365240dac3f2b2
7
- data.tar.gz: b5ee454173523ea078cf592aa2861541522514695bb50c7cdd0a42bb49c9b67359ef5493c55629cd1be2ff7ea3e40946f17e889be009cb27520803f6883ca637
6
+ metadata.gz: fdb95faa6ec8a26130cbfb93e834d200efb5a570907ec958e8361ed14de1956564472bc0b0a63617e518c154da5c0df2d8606ae9abf4484d382bb61daa2fe2b4
7
+ data.tar.gz: 531765dde46ad082559782f4413883ab956b0d9b1b01fe634e723140d5cc15d6b6bbeb88188ad7b5e5091a4f69539876b3be07ee421f414a8c2b54344415cbbd
@@ -0,0 +1,376 @@
1
+ class Cyperful::Driver
2
+ attr_reader :steps, :pausing
3
+
4
+ SCREENSHOTS_DIR = File.join(Cyperful::ROOT_DIR, "public/screenshots")
5
+
6
+ def initialize
7
+ @step_pausing_queue = Queue.new
8
+
9
+ @session = Capybara.current_session
10
+ raise "Could not find Capybara session" unless @session
11
+
12
+ setup_api_server
13
+ end
14
+
15
+ def set_current_test(test_class, test_name)
16
+ @test_class = test_class
17
+ @test_name = test_name.to_sym
18
+
19
+ @source_filepath =
20
+ Object.const_source_location(test_class.name).first ||
21
+ (raise "Could not find source file for #{test_class.name}")
22
+
23
+ reset_steps
24
+
25
+ print_steps
26
+
27
+ @session.visit(@cyperful_origin)
28
+ drive_iframe
29
+
30
+ # after we setup our UI, send the initialization data
31
+ notify_updated_steps
32
+
33
+ setup_tracing
34
+
35
+ setup_file_listener
36
+
37
+ # Sanity check
38
+ unless @step_pausing_queue.empty?
39
+ raise "step_pausing_queue is not empty during setup"
40
+ end
41
+
42
+ # Wait for the user to click "Start"
43
+ step_pausing_dequeue
44
+ end
45
+
46
+ def step_pausing_dequeue
47
+ command = @step_pausing_queue.deq
48
+ if command == :reset
49
+ raise Cyperful::ResetCommand
50
+ elsif command == :exit
51
+ raise Cyperful::ExitCommand
52
+ elsif command == :next
53
+ # just continue
54
+ else
55
+ raise "unknown command: #{command}"
56
+ end
57
+ end
58
+
59
+ def reset_steps
60
+ # TODO: memoize this when there's multiple tests per file
61
+ @steps =
62
+ Cyperful::TestParser.new(@test_class).steps_per_test.fetch(@test_name)
63
+
64
+ editor = "vscode" # TODO: support other editors?
65
+
66
+ @steps.each_with_index do |step, i|
67
+ step.merge!(
68
+ index: i,
69
+ status: "pending",
70
+ start_at: nil,
71
+ end_at: nil,
72
+ paused_at: nil,
73
+ permalink: "#{editor}://file/#{@source_filepath}:#{step.fetch(:line)}",
74
+ )
75
+ end
76
+
77
+ @step_per_line = @steps.index_by { |step| step[:line] }
78
+
79
+ @current_step = nil
80
+
81
+ @pause_at_step = true
82
+
83
+ @test_result = nil
84
+
85
+ # reset SCREENSHOTS_DIR
86
+ FileUtils.rm_rf(SCREENSHOTS_DIR)
87
+ FileUtils.mkdir_p(SCREENSHOTS_DIR)
88
+ end
89
+
90
+ def queue_reset
91
+ # FIXME: there may be other tests that are "queued" to run `at_exit`,
92
+ # so they'll run before this test restarts.
93
+ at_exit { Minitest.run_one_method(@test_class, @test_name) }
94
+ end
95
+
96
+ # subscribe to the execution of each line of code in the test.
97
+ # this let's us notify the frontend of the line's status, and pause execution if needed.
98
+ def setup_tracing
99
+ @tracepoint&.disable
100
+
101
+ @tracepoint =
102
+ TracePoint.new(:line) do |tp|
103
+ next if @source_filepath.nil? || tp.path != @source_filepath
104
+
105
+ finish_current_step
106
+
107
+ step = @step_per_line[tp.lineno]
108
+ pause_on_step(step) if step
109
+ end
110
+ @tracepoint.enable
111
+ end
112
+
113
+ # Every time a file changes the `test/` directory,
114
+ # reset this test
115
+ # TODO: add an option to auto-run
116
+ def setup_file_listener
117
+ test_dir = @source_filepath.match(%r{^/.+/(test|spec)\b})[0]
118
+
119
+ @file_listener&.stop
120
+ @file_listener =
121
+ Listen.to(test_dir) do |_modified, _added, _removed|
122
+ puts "Test files changed, resetting test..."
123
+
124
+ @pause_at_step = true
125
+ @step_pausing_queue.enq(:reset)
126
+ end
127
+ @file_listener.start
128
+ end
129
+
130
+ def print_steps
131
+ puts "#{@steps.length} steps:"
132
+ @steps.each do |step|
133
+ puts " #{step[:method]}: #{step[:line]}:#{step[:column]}"
134
+ end
135
+ puts
136
+ end
137
+
138
+ # pending (i.e. test hasn't started), paused, running, passed, failed
139
+ def test_status
140
+ return @test_result[:status] if @test_result # passed or failed
141
+
142
+ if @pause_at_step
143
+ return "running" if @steps.any? { |step| step[:status] == "running" }
144
+
145
+ return "pending" unless @current_step
146
+ return "paused"
147
+ end
148
+
149
+ "running"
150
+ end
151
+
152
+ def test_duration_ms
153
+ start_at = @steps.first&.[](:start_at)
154
+ return nil unless start_at
155
+ last_ended_step_i = @steps.rindex { |step| step[:end_at] }
156
+ return nil unless last_ended_step_i
157
+
158
+ end_at = @steps[last_ended_step_i][:end_at]
159
+
160
+ duration = end_at - start_at
161
+
162
+ @steps.each_with_index do |step, i|
163
+ next if i == 0 || i > last_ended_step_i
164
+ if step[:paused_at] && step[:start_at]
165
+ duration -= (step[:start_at] - step[:paused_at])
166
+ end
167
+ end
168
+
169
+ duration
170
+ end
171
+
172
+ def steps_updated_data
173
+ status = self.test_status
174
+ {
175
+ event: "steps_updated",
176
+ steps: @steps,
177
+ current_step_index: @current_step&.[](:index),
178
+ pause_at_step: @pause_at_step,
179
+ test_suite: @test_class.name,
180
+ test_name: @test_name,
181
+ test_status: status,
182
+ test_error: @test_result&.[](:error)&.to_s,
183
+ test_duration_ms: test_duration_ms,
184
+ }
185
+ end
186
+
187
+ private def notify_updated_steps
188
+ @ui_server.notify(steps_updated_data)
189
+ end
190
+
191
+ private def finish_current_step(error = nil)
192
+ if @current_step
193
+ @current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
194
+ @current_step[:status] = !error ? "passed" : "failed"
195
+
196
+ # take screenshot after the step has finished
197
+ # path = File.join(SCREENSHOTS_DIR, "#{@current_step[:index]}.png")
198
+
199
+ # FIXME: this adds ~200ms to each step! disabling it for now
200
+ # @session.save_screenshot(path)
201
+
202
+ # this adds ~70ms to each step, but causes a weird flash on the screen
203
+ # @session.find(:css, "body").base.native.save_screenshot(path)
204
+
205
+ @current_step = nil
206
+ end
207
+
208
+ notify_updated_steps
209
+ end
210
+
211
+ def pause_on_step(step)
212
+ @current_step = step
213
+
214
+ puts "STEP: #{step[:as_string]}"
215
+
216
+ if @pause_at_step == true || @pause_at_step == step[:index]
217
+ @current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
218
+ @current_step[:status] = "paused"
219
+ notify_updated_steps
220
+
221
+ # async wait for `continue_next_step`
222
+ step_pausing_dequeue
223
+ end
224
+
225
+ @current_step[:status] = "running"
226
+ @current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
227
+ notify_updated_steps
228
+ end
229
+
230
+ private def continue_next_step
231
+ @step_pausing_queue.enq(:next)
232
+ end
233
+
234
+ def drive_iframe
235
+ puts "Driving iframe..."
236
+
237
+ @session.switch_to_frame(
238
+ @session.find(:css, "iframe#scenario-frame"), # waits for the iframe to load
239
+ )
240
+ @driving_iframe = true
241
+ end
242
+
243
+ # forked from: https://github.com/teamcapybara/capybara/blob/master/lib/capybara/session.rb#L264
244
+ private def make_absolute_url(visit_uri)
245
+ visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
246
+ base_uri =
247
+ ::Addressable::URI.parse(@session.config.app_host || @session.server_url)
248
+
249
+ if base_uri && [nil, "http", "https"].include?(visit_uri.scheme)
250
+ if visit_uri.relative?
251
+ visit_uri_parts = visit_uri.to_hash.compact
252
+
253
+ # Useful to people deploying to a subdirectory
254
+ # and/or single page apps where only the url fragment changes
255
+ visit_uri_parts[:path] = base_uri.path + visit_uri.path
256
+
257
+ visit_uri = base_uri.merge(visit_uri_parts)
258
+ end
259
+ # adjust_server_port(visit_uri)
260
+ end
261
+
262
+ abs_url = visit_uri.to_s
263
+
264
+ display_url = abs_url.sub(base_uri.to_s, "")
265
+
266
+ [abs_url, display_url]
267
+ end
268
+
269
+ WATCHER_JS = File.read(File.join(Cyperful::ROOT_DIR, "watcher.js"))
270
+
271
+ def internal_visit(url)
272
+ return false unless @driving_iframe
273
+
274
+ abs_url, display_url = make_absolute_url(url)
275
+
276
+ # show the actual `visit` url as soon as it's computed
277
+ if @current_step && @current_step[:method] == :visit
278
+ @current_step[:as_string] = "visit #{display_url.to_json}"
279
+ notify_updated_steps
280
+ end
281
+
282
+ @session.execute_script("window.location.href = #{abs_url.to_json}")
283
+
284
+ # inject the watcher script into the page being tested.
285
+ # this script will notify the Cyperful UI for events like:
286
+ # console logs, network requests, client navigations, errors, etc.
287
+ @session.execute_script(WATCHER_JS) # ~9ms empirically
288
+
289
+ true
290
+ end
291
+
292
+ def internal_current_url
293
+ return nil unless @driving_iframe
294
+
295
+ @session.evaluate_script("window.location.href")
296
+ end
297
+
298
+ def setup_api_server
299
+ @ui_server = Cyperful::UiServer.new(port: 3004)
300
+
301
+ @cyperful_origin = @ui_server.url_origin
302
+
303
+ @ui_server.on_command do |command, params|
304
+ case command
305
+ when "start"
306
+ # one of: integer (index of a step), true (pause at every step), or nil (don't pause)
307
+ @pause_at_step = params["pause_at_step"]
308
+
309
+ continue_next_step
310
+ when "reset"
311
+ @pause_at_step = true
312
+ @step_pausing_queue.enq(:reset)
313
+ when "stop"
314
+ @pause_at_step = true # enable pausing
315
+ when "exit"
316
+ @pause_at_step = true
317
+
318
+ # instead of calling `exit` directly, we need to raise a Cyperful::ExitCommand error
319
+ # so Minitest can finish it's teardown e.g. to reset the database
320
+ @step_pausing_queue.enq(:exit)
321
+ else
322
+ raise "unknown command: #{command}"
323
+ end
324
+ end
325
+
326
+ @ui_server.start_async
327
+
328
+ # The server appears to always stop on it's own,
329
+ # so we don't need to stop it within an `at_exit` or `Minitest.after_run`
330
+
331
+ puts "Cyperful server started: #{@cyperful_origin}"
332
+ end
333
+
334
+ def teardown(error = nil)
335
+ @tracepoint&.disable
336
+ @tracepoint = nil
337
+
338
+ @file_listener&.stop
339
+ @file_listener = nil
340
+
341
+ if error&.is_a?(Cyperful::ResetCommand)
342
+ puts "\nPlease ignore the error, we're just resetting the test ;)"
343
+
344
+ @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
345
+
346
+ queue_reset
347
+ return
348
+ end
349
+
350
+ return if error&.is_a?(Cyperful::ExitCommand)
351
+
352
+ if error
353
+ # get the 4 lines following the first line that includes the source file
354
+ i = nil
355
+ backtrace =
356
+ error.backtrace.select do |s|
357
+ i ||= 0 if s.include?(@source_filepath)
358
+ i += 1 if i
359
+ break if i && i > 4
360
+ true
361
+ end
362
+
363
+ warn "\n\nTest failed with error:\n#{error.message}\n#{backtrace.join("\n")}"
364
+ end
365
+
366
+ @test_result = { status: error ? "failed" : "passed", error: error }
367
+
368
+ finish_current_step(error)
369
+
370
+ @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
371
+
372
+ puts "Cyperful teardown complete. Waiting for command..."
373
+ command = @step_pausing_queue.deq
374
+ queue_reset if command == :reset
375
+ end
376
+ end
@@ -0,0 +1,52 @@
1
+ # we need to override the some Capybara::Session methods because they
2
+ # control the top-level browser window, but we want them
3
+ # to control the iframe instead
4
+ module PrependCapybaraSession
5
+ def visit(url)
6
+ return if Cyperful.current&.internal_visit(url)
7
+ super
8
+ end
9
+
10
+ def current_url
11
+ url = Cyperful.current&.internal_current_url
12
+ return url if url
13
+ super
14
+ end
15
+
16
+ def refresh
17
+ return if Cyperful.current&.internal_visit(current_url)
18
+ super
19
+ end
20
+ end
21
+ Capybara::Session.prepend(PrependCapybaraSession)
22
+
23
+ # The Minitest test helper.
24
+ # TODO: support other test frameworks like RSpec
25
+ module Cyperful::SystemTestHelper
26
+ def setup
27
+ Cyperful.setup(self.class, self.method_name)
28
+ super
29
+ end
30
+
31
+ def teardown
32
+ error = passed? ? nil : failure
33
+
34
+ error = error.error if error.is_a?(Minitest::UnexpectedError)
35
+
36
+ Cyperful.teardown(error)
37
+ super
38
+ end
39
+ end
40
+
41
+ # we need to allow the iframe to be embedded in the cyperful server
42
+ # TODO: use Rack middleware instead to support non-Rails apps
43
+ if defined?(Rails)
44
+ Rails.application.config.content_security_policy do |policy|
45
+ policy.frame_ancestors(:self, "localhost:3004")
46
+ end
47
+ else
48
+ warn "Cyperful: Rails not detected, skipping content_security_policy fix.\nThe Cyperful UI may not work correctly."
49
+ end
50
+
51
+ # fix for: Set-Cookie (SameSite=Lax) doesn't work when within an iframe with host 127.0.0.1
52
+ Capybara.server_host = "localhost"
@@ -1,8 +1,8 @@
1
1
  require "webrick/websocket"
2
2
 
3
+ # fix for: webrick-websocket incorrectly assumes `Upgrade` header is always present
3
4
  module FixWebrickWebsocketServer
4
5
  def service(req, res)
5
- # fix for: webrick-websocket incorrectly assumes `Upgrade` header is always present
6
6
  req.header["upgrade"] = [""] if req["upgrade"].nil?
7
7
  super
8
8
  end
@@ -34,7 +34,7 @@ class Cyperful::UiServer
34
34
  @server =
35
35
  WEBrick::Websocket::HTTPServer.new(
36
36
  Port: @port,
37
- DocumentRoot: File.expand_path("../../public", __dir__),
37
+ DocumentRoot: File.join(Cyperful::ROOT_DIR, "public"),
38
38
  Logger: WEBrick::Log.new("/dev/null"),
39
39
  AccessLog: [],
40
40
  )
@@ -53,7 +53,7 @@ class Cyperful::UiServer
53
53
  # at the moment it's not possible to have more than one client connected.
54
54
  # TODO: handle the client unexpectedly disconnecting e.g. user changes browser url
55
55
  if open_sockets.length > 0
56
- warn "Warning: websockets already open: #{open_sockets}. You probably need to restart."
56
+ raise "websockets already open: #{open_sockets}. You probably need to restart."
57
57
  end
58
58
 
59
59
  sock_num = sock_num_counter
data/lib/cyperful.rb CHANGED
@@ -2,6 +2,8 @@ require "capybara"
2
2
  require "listen"
3
3
 
4
4
  module Cyperful
5
+ ROOT_DIR = File.expand_path("..", __dir__)
6
+
5
7
  @current = nil
6
8
 
7
9
  def self.current
@@ -11,7 +13,7 @@ module Cyperful
11
13
  puts "Setting up Cyperful for: #{test_class}##{test_name}"
12
14
 
13
15
  # must set `Cyperful.current` before calling `async_setup`
14
- @current ||= Cyperful::SystemSteps.new
16
+ @current ||= Cyperful::Driver.new
15
17
  @current.set_current_test(test_class, test_name)
16
18
 
17
19
  nil
@@ -47,9 +49,6 @@ module Cyperful
47
49
  end
48
50
  end
49
51
 
50
- require "cyperful/test_parser"
51
- require "cyperful/ui_server"
52
-
53
52
  class Cyperful::AbstractCommand < StandardError
54
53
  end
55
54
  class Cyperful::ResetCommand < Cyperful::AbstractCommand
@@ -57,419 +56,7 @@ end
57
56
  class Cyperful::ExitCommand < Cyperful::AbstractCommand
58
57
  end
59
58
 
60
- class Cyperful::SystemSteps
61
- attr_reader :steps, :pausing
62
-
63
- SCREENSHOTS_DIR = File.expand_path("../public/screenshots", __dir__)
64
-
65
- def initialize
66
- @step_pausing_queue = Queue.new
67
-
68
- @session = Capybara.current_session
69
- raise "Could not find Capybara session" unless @session
70
-
71
- setup_api_server
72
- end
73
-
74
- def set_current_test(test_class, test_name)
75
- @test_class = test_class
76
- @test_name = test_name.to_sym
77
-
78
- @source_filepath =
79
- Object.const_source_location(test_class.name).first ||
80
- (raise "Could not find source file for #{test_class.name}")
81
-
82
- reset_steps
83
-
84
- print_steps
85
-
86
- @session.visit(@cyperful_origin)
87
- drive_iframe
88
-
89
- # after we setup our UI, send the initialization data
90
- notify_updated_steps
91
-
92
- setup_tracing
93
-
94
- setup_file_listener
95
-
96
- # Sanity check
97
- unless @step_pausing_queue.empty?
98
- raise "step_pausing_queue is not empty during setup"
99
- end
100
-
101
- # Wait for the user to click "Start"
102
- step_pausing_dequeue
103
- end
104
-
105
- def step_pausing_dequeue
106
- command = @step_pausing_queue.deq
107
- if command == :reset
108
- raise Cyperful::ResetCommand
109
- elsif command == :exit
110
- raise Cyperful::ExitCommand
111
- elsif command == :next
112
- # just continue
113
- else
114
- raise "unknown command: #{command}"
115
- end
116
- end
117
-
118
- def reset_steps
119
- # TODO: memoize this when there's multiple tests per file
120
- @steps =
121
- Cyperful::TestParser.new(@test_class).steps_per_test.fetch(@test_name)
122
-
123
- editor = "vscode" # TODO: support other editors?
124
-
125
- @steps.each_with_index do |step, i|
126
- step.merge!(
127
- index: i,
128
- status: "pending",
129
- start_at: nil,
130
- end_at: nil,
131
- paused_at: nil,
132
- permalink: "#{editor}://file/#{@source_filepath}:#{step.fetch(:line)}",
133
- )
134
- end
135
-
136
- @step_per_line = @steps.index_by { |step| step[:line] }
137
-
138
- @current_step = nil
139
-
140
- @pause_at_step = true
141
-
142
- @test_result = nil
143
-
144
- # reset SCREENSHOTS_DIR
145
- FileUtils.rm_rf(SCREENSHOTS_DIR)
146
- FileUtils.mkdir_p(SCREENSHOTS_DIR)
147
- end
148
-
149
- # subscribe to the execution of each line of code in the test.
150
- # this let's us notify the frontend of the line's status, and pause execution if needed.
151
- def setup_tracing
152
- @tracepoint&.disable
153
-
154
- @tracepoint =
155
- TracePoint.new(:line) do |tp|
156
- next if @source_filepath.nil? || tp.path != @source_filepath
157
-
158
- finish_current_step
159
-
160
- step = @step_per_line[tp.lineno]
161
- pause_on_step(step) if step
162
- end
163
- @tracepoint.enable
164
- end
165
-
166
- # Every time a file changes the `test/` directory,
167
- # reset this test
168
- # TODO: add an option to auto-run
169
- def setup_file_listener
170
- test_dir = @source_filepath.match(%r{^/.+/(test|spec)\b})[0]
171
-
172
- @file_listener&.stop
173
- @file_listener =
174
- Listen.to(test_dir) do |_modified, _added, _removed|
175
- puts "Test files changed, resetting test..."
176
-
177
- @pause_at_step = true
178
- @step_pausing_queue.enq(:reset)
179
- end
180
- @file_listener.start
181
- end
182
-
183
- def print_steps
184
- puts "#{@steps.length} steps:"
185
- @steps.each do |step|
186
- puts " #{step[:method]}: #{step[:line]}:#{step[:column]}"
187
- end
188
- puts
189
- end
190
-
191
- # pending (i.e. test hasn't started), paused, running, passed, failed
192
- def test_status
193
- return @test_result[:status] if @test_result # passed or failed
194
-
195
- if @pause_at_step
196
- return "running" if @steps.any? { |step| step[:status] == "running" }
197
-
198
- return "pending" unless @current_step
199
- return "paused"
200
- end
201
-
202
- "running"
203
- end
204
-
205
- def test_duration_ms
206
- start_at = @steps.first&.[](:start_at)
207
- return nil unless start_at
208
- last_ended_step_i = @steps.rindex { |step| step[:end_at] }
209
- return nil unless last_ended_step_i
210
-
211
- end_at = @steps[last_ended_step_i][:end_at]
212
-
213
- duration = end_at - start_at
214
-
215
- @steps.each_with_index do |step, i|
216
- next if i == 0 || i > last_ended_step_i
217
- if step[:paused_at] && step[:start_at]
218
- duration -= (step[:start_at] - step[:paused_at])
219
- end
220
- end
221
-
222
- duration
223
- end
224
-
225
- def steps_updated_data
226
- status = self.test_status
227
- {
228
- event: "steps_updated",
229
- steps: @steps,
230
- current_step_index: @current_step&.[](:index),
231
- pause_at_step: @pause_at_step,
232
- test_suite: @test_class.name,
233
- test_name: @test_name,
234
- test_status: status,
235
- test_error: @test_result&.[](:error)&.to_s,
236
- test_duration_ms: test_duration_ms,
237
- }
238
- end
239
-
240
- private def notify_updated_steps
241
- @ui_server.notify(steps_updated_data)
242
- end
243
-
244
- private def finish_current_step(error = nil)
245
- if @current_step
246
- @current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
247
- @current_step[:status] = !error ? "passed" : "failed"
248
-
249
- # take screenshot after the step has finished
250
- # path = File.join(SCREENSHOTS_DIR, "#{@current_step[:index]}.png")
251
-
252
- # FIXME: this adds ~200ms to each step! disabling it for now
253
- # @session.save_screenshot(path)
254
-
255
- # this adds ~70ms to each step, but causes a weird flash on the screen
256
- # @session.find(:css, "body").base.native.save_screenshot(path)
257
-
258
- @current_step = nil
259
- end
260
-
261
- notify_updated_steps
262
- end
263
-
264
- def pause_on_step(step)
265
- @current_step = step
266
-
267
- puts "STEP: #{step[:as_string]}"
268
-
269
- if @pause_at_step == true || @pause_at_step == step[:index]
270
- @current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
271
- @current_step[:status] = "paused"
272
- notify_updated_steps
273
-
274
- # async wait for `continue_next_step`
275
- step_pausing_dequeue
276
- end
277
-
278
- @current_step[:status] = "running"
279
- @current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
280
- notify_updated_steps
281
- end
282
-
283
- private def continue_next_step
284
- @step_pausing_queue.enq(:next)
285
- end
286
-
287
- def drive_iframe
288
- puts "Driving iframe..."
289
-
290
- @session.switch_to_frame(
291
- @session.find(:css, "iframe#scenario-frame"), # waits for the iframe to load
292
- )
293
- @driving_iframe = true
294
- end
295
-
296
- # forked from: https://github.com/teamcapybara/capybara/blob/master/lib/capybara/session.rb#L264
297
- private def make_absolute_url(visit_uri)
298
- visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
299
- base_uri =
300
- ::Addressable::URI.parse(@session.config.app_host || @session.server_url)
301
-
302
- if base_uri && [nil, "http", "https"].include?(visit_uri.scheme)
303
- if visit_uri.relative?
304
- visit_uri_parts = visit_uri.to_hash.compact
305
-
306
- # Useful to people deploying to a subdirectory
307
- # and/or single page apps where only the url fragment changes
308
- visit_uri_parts[:path] = base_uri.path + visit_uri.path
309
-
310
- visit_uri = base_uri.merge(visit_uri_parts)
311
- end
312
- # adjust_server_port(visit_uri)
313
- end
314
-
315
- abs_url = visit_uri.to_s
316
-
317
- display_url = abs_url.sub(base_uri.to_s, "")
318
-
319
- [abs_url, display_url]
320
- end
321
-
322
- WATCHER_JS = File.read(File.join(__dir__, "../watcher.js"))
323
-
324
- def internal_visit(url)
325
- return false unless @driving_iframe
326
-
327
- abs_url, display_url = make_absolute_url(url)
328
-
329
- # show the actual `visit` url as soon as it's computed
330
- if @current_step && @current_step[:method] == :visit
331
- @current_step[:as_string] = "visit #{display_url.to_json}"
332
- notify_updated_steps
333
- end
334
-
335
- @session.execute_script("window.location.href = #{abs_url.to_json}")
336
-
337
- # inject the watcher script into the page being tested.
338
- # this script will notify the Cyperful UI for events like:
339
- # console logs, network requests, client navigations, errors, etc.
340
- @session.execute_script(WATCHER_JS) # ~9ms empirically
341
-
342
- true
343
- end
344
-
345
- def internal_current_url
346
- return nil unless @driving_iframe
347
-
348
- @session.evaluate_script("window.location.href")
349
- end
350
-
351
- def setup_api_server
352
- @ui_server = Cyperful::UiServer.new(port: 3004)
353
-
354
- @cyperful_origin = @ui_server.url_origin
355
-
356
- @ui_server.on_command do |command, params|
357
- case command
358
- when "start"
359
- # one of: integer (index of a step), true (pause at every step), or nil (don't pause)
360
- @pause_at_step = params["pause_at_step"]
361
-
362
- continue_next_step
363
- when "reset"
364
- @pause_at_step = true
365
- @step_pausing_queue.enq(:reset)
366
- when "stop"
367
- @pause_at_step = true # enable pausing
368
- when "exit"
369
- @pause_at_step = true
370
-
371
- # instead of calling `exit` directly, we need to raise a Cyperful::ExitCommand error
372
- # so Minitest can finish it's teardown e.g. to reset the database
373
- @step_pausing_queue.enq(:exit)
374
- else
375
- raise "unknown command: #{command}"
376
- end
377
- end
378
-
379
- @ui_server.start_async
380
-
381
- # The server appears to always stop on it's own,
382
- # so we don't need to stop it within an `at_exit` or `Minitest.after_run`
383
-
384
- puts "Cyperful server started: #{@cyperful_origin}"
385
- end
386
-
387
- def teardown(error = nil)
388
- @tracepoint&.disable
389
- @tracepoint = nil
390
-
391
- @file_listener&.stop
392
- @file_listener = nil
393
-
394
- if error&.is_a?(Cyperful::ResetCommand)
395
- puts "\nPlease ignore the error, we're just resetting the test ;)"
396
-
397
- @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
398
-
399
- at_exit { Minitest.run_one_method(@test_class, @test_name) }
400
- return
401
- end
402
-
403
- return if error&.is_a?(Cyperful::ExitCommand)
404
-
405
- if error
406
- # backtrace = error.backtrace.select { |s| s.include?(@source_filepath) }
407
- backtrace = error.backtrace.slice(0, 4)
408
- warn "\n\nTest failed with error:\n#{error.message}\n#{backtrace.join("\n")}"
409
- end
410
-
411
- @test_result = { status: error ? "failed" : "passed", error: error }
412
-
413
- finish_current_step(error)
414
-
415
- @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
416
-
417
- puts "Cyperful teardown complete. Waiting for command..."
418
- command = @step_pausing_queue.deq
419
- if command == :reset
420
- at_exit { Minitest.run_one_method(@test_class, @test_name) }
421
- end
422
- end
423
- end
424
-
425
- module PrependCapybaraSession
426
- # we need to override the following methods because they
427
- # control the top-level browser window, but we want them
428
- # to control the iframe instead
429
-
430
- def visit(url)
431
- return if Cyperful.current&.internal_visit(url)
432
- super
433
- end
434
-
435
- def current_url
436
- url = Cyperful.current&.internal_current_url
437
- return url if url
438
- super
439
- end
440
-
441
- def refresh
442
- return if Cyperful.current&.internal_visit(current_url)
443
- super
444
- end
445
- end
446
- Capybara::Session.prepend(PrependCapybaraSession)
447
-
448
- module Cyperful::SystemTestHelper
449
- def setup
450
- Cyperful.setup(self.class, self.method_name)
451
- super
452
- end
453
-
454
- def teardown
455
- error = passed? ? nil : failure
456
-
457
- error = error.error if error.is_a?(Minitest::UnexpectedError)
458
-
459
- Cyperful.teardown(error)
460
- super
461
- end
462
- end
463
-
464
- # we need to allow the iframe to be embedded in the cyperful server
465
- # TODO: use Rack middleware instead to support non-Rails apps
466
- if const_defined?(:Rails)
467
- Rails.application.config.content_security_policy do |policy|
468
- policy.frame_ancestors(:self, "localhost:3004")
469
- end
470
- else
471
- warn "Cyperful: Rails not detected, skipping content_security_policy"
472
- end
473
-
474
- # fix for: Set-Cookie (SameSite=Lax) doesn't work when within an iframe with host 127.0.0.1
475
- Capybara.server_host = "localhost"
59
+ require "cyperful/test_parser"
60
+ require "cyperful/ui_server"
61
+ require "cyperful/driver"
62
+ require "cyperful/framework_injections"
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cyperful
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
- - me@wyattades.com
7
+ - Wyatt Ades
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
@@ -59,11 +59,14 @@ extensions: []
59
59
  extra_rdoc_files: []
60
60
  files:
61
61
  - lib/cyperful.rb
62
+ - lib/cyperful/driver.rb
63
+ - lib/cyperful/framework_injections.rb
62
64
  - lib/cyperful/test_parser.rb
63
65
  - lib/cyperful/ui_server.rb
64
66
  - watcher.js
65
- homepage:
66
- licenses: []
67
+ homepage: https://github.com/stepful/cyperful
68
+ licenses:
69
+ - MIT
67
70
  metadata: {}
68
71
  post_install_message:
69
72
  rdoc_options: []