cyperful 0.1.3 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cyperful/commands.rb +19 -0
- data/lib/cyperful/driver.rb +149 -56
- data/lib/cyperful/framework_injections.rb +53 -1
- data/lib/cyperful/test_parser.rb +79 -25
- data/lib/cyperful/ui_server.rb +12 -0
- data/lib/cyperful.rb +23 -23
- data/public/assets/index-CDElGKtz.css +1 -0
- data/public/assets/index-DMlaSnZ7.js +54 -0
- data/public/frame-agent.js +120 -0
- data/public/index.html +3 -5
- metadata +9 -9
- data/public/assets/index-722e2568.js +0 -40
- data/public/assets/index-fb0f529b.css +0 -1
- data/public/assets/logo-169f5e20.svg +0 -72
- data/watcher.js +0 -151
- /data/public/assets/{favicon-9f6bc28c.ico → favicon-DMdBZQlK.ico} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83254b2335dc0d99b443c2aad2260adc7c4f56bebaf32847a9e4ce151c03c6bc
|
4
|
+
data.tar.gz: 5a444d60e56c014569604e91162943aed5d6cb4f21655aaff8405be62d67b622
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 945bad502b63c646fd55f43a997fbb2442d27866600aaac1aeda1aabd2c218841f51a81a000ecb0ede704097bd0e53d4ad67d9b8aa9c5b501ce62dc445d1c3cf
|
7
|
+
data.tar.gz: 32ce9c41a512ebc499b1852d5cf584fdf9b86015906da4c154706f056a5c385cd341aa174388c2b3171b82c1a17f23bd012ccaa62f6382d86c070a7f91dbd467
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cyperful
|
2
|
+
# @abstract
|
3
|
+
class AbstractCommand < StandardError
|
4
|
+
# don't print normal error/backtrace
|
5
|
+
def to_s
|
6
|
+
command_name =
|
7
|
+
self.class.name.split("::").last.chomp("Command").underscore
|
8
|
+
"(Captured cyperful command: #{command_name})"
|
9
|
+
end
|
10
|
+
def backtrace
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ResetCommand < AbstractCommand
|
16
|
+
end
|
17
|
+
class ExitCommand < AbstractCommand
|
18
|
+
end
|
19
|
+
end
|
data/lib/cyperful/driver.rb
CHANGED
@@ -18,7 +18,7 @@ class Cyperful::Driver
|
|
18
18
|
|
19
19
|
@source_filepath =
|
20
20
|
Object.const_source_location(test_class.name).first ||
|
21
|
-
(
|
21
|
+
raise("Could not find source file for #{test_class.name}")
|
22
22
|
|
23
23
|
reset_steps
|
24
24
|
|
@@ -36,11 +36,11 @@ class Cyperful::Driver
|
|
36
36
|
|
37
37
|
# Sanity check
|
38
38
|
unless @step_pausing_queue.empty?
|
39
|
-
raise "step_pausing_queue
|
39
|
+
raise "Unexpected: step_pausing_queue must be empty during setup"
|
40
40
|
end
|
41
41
|
|
42
42
|
# Wait for the user to click "Start"
|
43
|
-
step_pausing_dequeue
|
43
|
+
step_pausing_dequeue if @pause_at_step == true
|
44
44
|
end
|
45
45
|
|
46
46
|
def step_pausing_dequeue
|
@@ -74,12 +74,18 @@ class Cyperful::Driver
|
|
74
74
|
)
|
75
75
|
end
|
76
76
|
|
77
|
+
# NOTE: there can be multiple steps per line, this takes the last instance
|
77
78
|
@step_per_line = @steps.index_by { |step| step[:line] }
|
78
79
|
|
79
80
|
@current_step = nil
|
80
81
|
|
81
82
|
@pause_at_step = true
|
82
83
|
|
84
|
+
run_options = self.class.pop_run_options!
|
85
|
+
if run_options.key?(:pause_at_step)
|
86
|
+
@pause_at_step = run_options[:pause_at_step]
|
87
|
+
end
|
88
|
+
|
83
89
|
@test_result = nil
|
84
90
|
|
85
91
|
# reset SCREENSHOTS_DIR
|
@@ -87,10 +93,39 @@ class Cyperful::Driver
|
|
87
93
|
FileUtils.mkdir_p(SCREENSHOTS_DIR)
|
88
94
|
end
|
89
95
|
|
96
|
+
@next_run_options = {}
|
97
|
+
def self.next_run_options=(options)
|
98
|
+
@next_run_options = options
|
99
|
+
end
|
100
|
+
def self.pop_run_options!
|
101
|
+
opts = @next_run_options
|
102
|
+
@next_run_options = {}
|
103
|
+
opts
|
104
|
+
end
|
105
|
+
|
106
|
+
private def reload_const(class_name, source_path)
|
107
|
+
Object.send(:remove_const, class_name) if Object.const_defined?(class_name)
|
108
|
+
load(source_path) # reload the file
|
109
|
+
unless Object.const_defined?(class_name)
|
110
|
+
raise "Failed to reload test class: #{class_name}"
|
111
|
+
end
|
112
|
+
Object.const_get(class_name)
|
113
|
+
end
|
114
|
+
|
90
115
|
def queue_reset
|
91
|
-
|
92
|
-
|
93
|
-
|
116
|
+
at_exit do
|
117
|
+
# reload test-suite code on reset (for `setup_file_listener`)
|
118
|
+
# TODO: also reload dependent files
|
119
|
+
# NOTE: run_on_method will fail if test_name also changed
|
120
|
+
@test_class = reload_const(@test_class.name, @source_filepath)
|
121
|
+
|
122
|
+
# TODO
|
123
|
+
# if Cyperful.config.reload_source_files && defined?(Rails)
|
124
|
+
# Rails.application.reloader.reload!
|
125
|
+
# end
|
126
|
+
|
127
|
+
Minitest.run_one_method(@test_class, @test_name)
|
128
|
+
end
|
94
129
|
end
|
95
130
|
|
96
131
|
# subscribe to the execution of each line of code in the test.
|
@@ -110,27 +145,41 @@ class Cyperful::Driver
|
|
110
145
|
@tracepoint.enable
|
111
146
|
end
|
112
147
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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..."
|
148
|
+
private def test_directory
|
149
|
+
@source_filepath.match(%r{^/.+/(?:test|spec)\b})&.[](0) ||
|
150
|
+
raise("Could not determine test directory for #{@source_filepath}")
|
151
|
+
end
|
123
152
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
153
|
+
# Every time a file changes the `test/` directory, reset this test
|
154
|
+
# TODO: add an option to auto-run on reload
|
155
|
+
def setup_file_listener
|
156
|
+
# TODO
|
157
|
+
# if Cyperful.config.reload_source_files
|
158
|
+
# @source_file_listener = Listen.to(rails_directory) ...
|
159
|
+
# end
|
160
|
+
|
161
|
+
if Cyperful.config.reload_test_files
|
162
|
+
@file_listener&.stop
|
163
|
+
@file_listener =
|
164
|
+
Listen.to(test_directory) do |_modified, _added, _removed|
|
165
|
+
puts "Test files changed, resetting test..."
|
166
|
+
|
167
|
+
# keep the same pause state after the reload
|
168
|
+
self.class.next_run_options = { pause_at_step: @pause_at_step }
|
169
|
+
|
170
|
+
@pause_at_step = true # pause current test immediately
|
171
|
+
@step_pausing_queue.enq(:reset)
|
172
|
+
end
|
173
|
+
@file_listener.start
|
174
|
+
end
|
128
175
|
end
|
129
176
|
|
130
177
|
def print_steps
|
131
|
-
puts "#{@steps.length} steps:"
|
132
|
-
@steps.
|
133
|
-
puts "
|
178
|
+
puts "Found #{@steps.length} steps:"
|
179
|
+
@steps.each_with_index do |step, i|
|
180
|
+
puts " #{
|
181
|
+
(i + 1).to_s.rjust(2)
|
182
|
+
}. #{step[:method]}: #{step[:line]}:#{step[:column]}"
|
134
183
|
end
|
135
184
|
puts
|
136
185
|
end
|
@@ -149,7 +198,7 @@ class Cyperful::Driver
|
|
149
198
|
"running"
|
150
199
|
end
|
151
200
|
|
152
|
-
def test_duration_ms
|
201
|
+
private def test_duration_ms
|
153
202
|
start_at = @steps.first&.[](:start_at)
|
154
203
|
return nil unless start_at
|
155
204
|
last_ended_step_i = @steps.rindex { |step| step[:end_at] }
|
@@ -188,11 +237,39 @@ class Cyperful::Driver
|
|
188
237
|
@ui_server.notify(steps_updated_data)
|
189
238
|
end
|
190
239
|
|
240
|
+
# called at the start of each step
|
241
|
+
def pause_on_step(step)
|
242
|
+
@current_step = step
|
243
|
+
|
244
|
+
# using `print` so we can append the step's status (see `finish_current_step`)
|
245
|
+
print("STEP #{(step[:index] + 1).to_s.rjust(2)}: #{step[:as_string]}")
|
246
|
+
|
247
|
+
if @pause_at_step == true || @pause_at_step == step[:index]
|
248
|
+
@current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
|
249
|
+
@current_step[:status] = "paused"
|
250
|
+
notify_updated_steps
|
251
|
+
|
252
|
+
# async wait for `continue_next_step`
|
253
|
+
step_pausing_dequeue
|
254
|
+
end
|
255
|
+
|
256
|
+
@current_step[:status] = "running"
|
257
|
+
@current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
|
258
|
+
notify_updated_steps
|
259
|
+
end
|
260
|
+
|
261
|
+
# called at the end of each step
|
191
262
|
private def finish_current_step(error = nil)
|
192
263
|
if @current_step
|
193
264
|
@current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
|
194
265
|
@current_step[:status] = !error ? "passed" : "failed"
|
195
266
|
|
267
|
+
puts(
|
268
|
+
" (#{@current_step[:end_at] - @current_step[:start_at]}ms)#{
|
269
|
+
error ? " FAILED" : ""
|
270
|
+
}",
|
271
|
+
)
|
272
|
+
|
196
273
|
# take screenshot after the step has finished
|
197
274
|
# path = File.join(SCREENSHOTS_DIR, "#{@current_step[:index]}.png")
|
198
275
|
|
@@ -208,25 +285,6 @@ class Cyperful::Driver
|
|
208
285
|
notify_updated_steps
|
209
286
|
end
|
210
287
|
|
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
288
|
private def continue_next_step
|
231
289
|
@step_pausing_queue.enq(:next)
|
232
290
|
end
|
@@ -234,12 +292,27 @@ class Cyperful::Driver
|
|
234
292
|
def drive_iframe
|
235
293
|
puts "Driving iframe..."
|
236
294
|
|
237
|
-
|
238
|
-
|
239
|
-
|
295
|
+
# make sure a `within` block doesn't affect these commands
|
296
|
+
without_finder_scopes do
|
297
|
+
@session.switch_to_frame(
|
298
|
+
# `find` waits for the iframe to load
|
299
|
+
@session.find(:css, "iframe#scenario-frame"),
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
240
303
|
@driving_iframe = true
|
241
304
|
end
|
242
305
|
|
306
|
+
private def without_finder_scopes(&block)
|
307
|
+
scopes = @session.send(:scopes)
|
308
|
+
before_scopes = scopes.dup
|
309
|
+
scopes.reject! { |el| el.is_a?(Capybara::Node::Element) }
|
310
|
+
block.call
|
311
|
+
ensure
|
312
|
+
scopes.clear
|
313
|
+
scopes.push(*before_scopes)
|
314
|
+
end
|
315
|
+
|
243
316
|
# forked from: https://github.com/teamcapybara/capybara/blob/master/lib/capybara/session.rb#L264
|
244
317
|
private def make_absolute_url(visit_uri)
|
245
318
|
visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
|
@@ -266,10 +339,27 @@ class Cyperful::Driver
|
|
266
339
|
[abs_url, display_url]
|
267
340
|
end
|
268
341
|
|
269
|
-
|
342
|
+
def self.load_frame_agent_js
|
343
|
+
return @frame_agent_js if defined?(@frame_agent_js)
|
344
|
+
|
345
|
+
@frame_agent_js =
|
346
|
+
File.read(File.join(Cyperful::ROOT_DIR, "public/frame-agent.js")).sub(
|
347
|
+
"__CYPERFUL_CONFIG__",
|
348
|
+
{ CYPERFUL_ORIGIN: "http://localhost:#{Cyperful.config.port}" }.to_json,
|
349
|
+
)
|
350
|
+
end
|
351
|
+
|
352
|
+
private def skip_multi_sessions
|
353
|
+
unless Capybara.current_session == @session
|
354
|
+
warn "Skipped Cyperful setup in non-default session: #{Capybara.session_name}"
|
355
|
+
return true
|
356
|
+
end
|
357
|
+
false
|
358
|
+
end
|
270
359
|
|
271
360
|
def internal_visit(url)
|
272
361
|
return false unless @driving_iframe
|
362
|
+
return false if skip_multi_sessions
|
273
363
|
|
274
364
|
abs_url, display_url = make_absolute_url(url)
|
275
365
|
|
@@ -281,22 +371,23 @@ class Cyperful::Driver
|
|
281
371
|
|
282
372
|
@session.execute_script("window.location.href = #{abs_url.to_json}")
|
283
373
|
|
284
|
-
# inject the
|
374
|
+
# inject the frame-agent script into the page being tested.
|
285
375
|
# this script will notify the Cyperful UI for events like:
|
286
376
|
# console logs, network requests, client navigations, errors, etc.
|
287
|
-
@session.execute_script(
|
377
|
+
@session.execute_script(Cyperful::Driver.load_frame_agent_js) # ~9ms empirically
|
288
378
|
|
289
379
|
true
|
290
380
|
end
|
291
381
|
|
292
382
|
def internal_current_url
|
293
383
|
return nil unless @driving_iframe
|
384
|
+
return nil if skip_multi_sessions
|
294
385
|
|
295
386
|
@session.evaluate_script("window.location.href")
|
296
387
|
end
|
297
388
|
|
298
389
|
def setup_api_server
|
299
|
-
@ui_server = Cyperful::UiServer.new(port:
|
390
|
+
@ui_server = Cyperful::UiServer.new(port: Cyperful.config.port)
|
300
391
|
|
301
392
|
@cyperful_origin = @ui_server.url_origin
|
302
393
|
|
@@ -335,11 +426,8 @@ class Cyperful::Driver
|
|
335
426
|
@tracepoint&.disable
|
336
427
|
@tracepoint = nil
|
337
428
|
|
338
|
-
@file_listener&.stop
|
339
|
-
@file_listener = nil
|
340
|
-
|
341
429
|
if error&.is_a?(Cyperful::ResetCommand)
|
342
|
-
puts "\
|
430
|
+
puts "\nResetting test (ignore any error logs)..."
|
343
431
|
|
344
432
|
@ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
|
345
433
|
|
@@ -355,9 +443,10 @@ class Cyperful::Driver
|
|
355
443
|
backtrace = []
|
356
444
|
error.backtrace.each do |s|
|
357
445
|
i ||= 0 if s.include?(@source_filepath)
|
358
|
-
|
359
|
-
|
446
|
+
next unless i
|
447
|
+
i += 1
|
360
448
|
backtrace << s
|
449
|
+
break if i >= 6
|
361
450
|
end
|
362
451
|
|
363
452
|
warn "\n\nTest failed with error:\n#{error.message}\n#{backtrace.join("\n")}"
|
@@ -370,7 +459,11 @@ class Cyperful::Driver
|
|
370
459
|
@ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
|
371
460
|
|
372
461
|
puts "Cyperful teardown complete. Waiting for command..."
|
462
|
+
# NOTE: this will raise an `Interrupt` if the user Ctrl+C's here
|
373
463
|
command = @step_pausing_queue.deq
|
374
464
|
queue_reset if command == :reset
|
465
|
+
ensure
|
466
|
+
@file_listener&.stop
|
467
|
+
@file_listener = nil
|
375
468
|
end
|
376
469
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "action_dispatch/system_testing/driver"
|
2
|
+
|
1
3
|
# we need to override the some Capybara::Session methods because they
|
2
4
|
# control the top-level browser window, but we want them
|
3
5
|
# to control the iframe instead
|
@@ -17,9 +19,24 @@ module PrependCapybaraSession
|
|
17
19
|
return if Cyperful.current&.internal_visit(current_url)
|
18
20
|
super
|
19
21
|
end
|
22
|
+
|
23
|
+
def go_back
|
24
|
+
super
|
25
|
+
Cyperful.current&.drive_iframe
|
26
|
+
end
|
20
27
|
end
|
21
28
|
Capybara::Session.prepend(PrependCapybaraSession)
|
22
29
|
|
30
|
+
module PrependCapybaraWindow
|
31
|
+
# this solves a bug in Capybara where it doesn't
|
32
|
+
# return to driving the iframe after a call to `Window#close`
|
33
|
+
def close
|
34
|
+
super
|
35
|
+
Cyperful.current&.drive_iframe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
Capybara::Window.prepend(PrependCapybaraWindow)
|
39
|
+
|
23
40
|
# The Minitest test helper.
|
24
41
|
# TODO: support other test frameworks like RSpec
|
25
42
|
module Cyperful::SystemTestHelper
|
@@ -36,13 +53,48 @@ module Cyperful::SystemTestHelper
|
|
36
53
|
Cyperful.teardown(error)
|
37
54
|
super
|
38
55
|
end
|
56
|
+
|
57
|
+
# Disable default screenshot on failure b/c we handle them ourselves.
|
58
|
+
# https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L156
|
59
|
+
def take_failed_screenshot
|
60
|
+
nil
|
61
|
+
end
|
39
62
|
end
|
40
63
|
|
64
|
+
module PrependSystemTestingDriver
|
65
|
+
def initialize(...)
|
66
|
+
super(...)
|
67
|
+
|
68
|
+
prev_capabilities = @capabilities
|
69
|
+
@capabilities =
|
70
|
+
proc do |driver_opts|
|
71
|
+
prev_capabilities&.call(driver_opts)
|
72
|
+
|
73
|
+
next unless driver_opts.respond_to?(:add_argument)
|
74
|
+
|
75
|
+
# this assumes Selenium and Chrome:
|
76
|
+
|
77
|
+
# so user isn't prompted when we start recording video w/ MediaStream
|
78
|
+
driver_opts.add_argument("--auto-accept-this-tab-capture")
|
79
|
+
driver_opts.add_argument("--use-fake-ui-for-media-stream")
|
80
|
+
|
81
|
+
# make sure we're not in headless mode
|
82
|
+
driver_opts.args.delete("--headless")
|
83
|
+
driver_opts.args.delete("--headless=new")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
ActionDispatch::SystemTesting::Driver.prepend(PrependSystemTestingDriver)
|
88
|
+
|
89
|
+
# if defined?(Minitest::Test)
|
90
|
+
# Minitest::Test::PASSTHROUGH_EXCEPTIONS << Cyperful::AbstractCommand
|
91
|
+
# end
|
92
|
+
|
41
93
|
# we need to allow the iframe to be embedded in the cyperful server
|
42
94
|
# TODO: use Rack middleware instead to support non-Rails apps
|
43
95
|
if defined?(Rails)
|
44
96
|
Rails.application.config.content_security_policy do |policy|
|
45
|
-
policy.frame_ancestors(:self, "localhost
|
97
|
+
policy.frame_ancestors(:self, "localhost:#{Cyperful.config.port}")
|
46
98
|
end
|
47
99
|
else
|
48
100
|
warn "Cyperful: Rails not detected, skipping content_security_policy fix.\nThe Cyperful UI may not work correctly."
|
data/lib/cyperful/test_parser.rb
CHANGED
@@ -1,6 +1,41 @@
|
|
1
1
|
require "parser/current"
|
2
|
+
require "capybara/minitest"
|
2
3
|
|
3
4
|
class Cyperful::TestParser
|
5
|
+
# see docs for methods: https://www.rubydoc.info/github/jnicklas/capybara/Capybara/Session
|
6
|
+
@step_at_methods =
|
7
|
+
Capybara::Session::DSL_METHODS.to_set +
|
8
|
+
Capybara::Minitest::Assertions.instance_methods(false) -
|
9
|
+
# exclude methods that don't have side-effects i.e. don't modify the page:
|
10
|
+
%i[
|
11
|
+
body
|
12
|
+
html
|
13
|
+
source
|
14
|
+
current_url
|
15
|
+
current_host
|
16
|
+
current_path
|
17
|
+
current_scope
|
18
|
+
status_code
|
19
|
+
response_headers
|
20
|
+
]
|
21
|
+
|
22
|
+
def self.step_at_methods
|
23
|
+
@step_at_methods
|
24
|
+
end
|
25
|
+
def self.add_step_at_methods(*mods_or_methods)
|
26
|
+
mods_or_methods.each do |mod_or_method|
|
27
|
+
case mod_or_method
|
28
|
+
when Module
|
29
|
+
@step_at_methods +=
|
30
|
+
mod_or_method.methods(false) + mod_or_method.instance_methods(false)
|
31
|
+
when String, Symbol
|
32
|
+
@step_at_methods << mod_or_method.to_sym
|
33
|
+
else
|
34
|
+
raise "Expected Module or Array of strings/symbols, got #{mod_or_method.class}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
4
39
|
def initialize(test_class)
|
5
40
|
@test_class = test_class
|
6
41
|
@source_filepath = Object.const_source_location(test_class.name).first
|
@@ -24,14 +59,14 @@ class Cyperful::TestParser
|
|
24
59
|
end
|
25
60
|
|
26
61
|
(
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
62
|
+
# the children of the `class` node are either:
|
63
|
+
# - a `begin` node if there's more than 1 child node
|
64
|
+
# - or the one 0 or 1 child node
|
65
|
+
system_test_class
|
66
|
+
.children
|
67
|
+
.find { |node| node.type == :begin }
|
68
|
+
&.children || [system_test_class.children[2]].compact
|
69
|
+
)
|
35
70
|
.map do |node|
|
36
71
|
# e.g. `test "my test" do ... end`
|
37
72
|
if node.type == :block && node.children[0].type == :send &&
|
@@ -39,7 +74,7 @@ class Cyperful::TestParser
|
|
39
74
|
test_string = node.children[0].children[2].children[0]
|
40
75
|
|
41
76
|
# https://github.com/rails/rails/blob/66676ce499a32e4c62220bd05f8ee2cdf0e15f0c/activesupport/lib/active_support/testing/declarative.rb#L14C23-L14C61
|
42
|
-
test_method = "test_#{test_string.gsub(/\s+/, "_")}"
|
77
|
+
test_method = :"test_#{test_string.gsub(/\s+/, "_")}"
|
43
78
|
|
44
79
|
block_node = node.children[2]
|
45
80
|
[test_method, block_node]
|
@@ -50,29 +85,48 @@ class Cyperful::TestParser
|
|
50
85
|
end
|
51
86
|
.compact
|
52
87
|
.to_h do |test_method, block_node|
|
53
|
-
[
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
.uniq { |step| step[:line] },
|
58
|
-
]
|
88
|
+
out = []
|
89
|
+
block_node.children.each { |child| find_test_steps(child, out) }
|
90
|
+
|
91
|
+
[test_method, out]
|
59
92
|
end
|
60
93
|
end
|
61
94
|
|
62
|
-
private def find_test_steps(ast, out = [])
|
95
|
+
private def find_test_steps(ast, out = [], depth = 0)
|
63
96
|
return out unless ast&.is_a?(Parser::AST::Node)
|
64
97
|
|
65
|
-
if ast.type == :send
|
66
|
-
out
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
as_string: ast.loc.expression.source,
|
71
|
-
}
|
72
|
-
end
|
98
|
+
if ast.type == :send
|
99
|
+
add_node(ast, out, depth)
|
100
|
+
ast.children.each { |child| find_test_steps(child, out, depth) }
|
101
|
+
elsif ast.type == :block
|
102
|
+
method, _args, child = ast.children
|
73
103
|
|
74
|
-
|
104
|
+
children = child.type == :begin ? child.children : [child]
|
105
|
+
|
106
|
+
if method.type == :send
|
107
|
+
depth += 1 if add_node(method, out, depth)
|
108
|
+
method.children.each { |child| find_test_steps(child, out, depth) }
|
109
|
+
end
|
110
|
+
|
111
|
+
children.each { |child| find_test_steps(child, out, depth) }
|
112
|
+
end
|
75
113
|
|
76
114
|
out
|
77
115
|
end
|
116
|
+
|
117
|
+
private def add_node(node, out, depth)
|
118
|
+
unless Cyperful::TestParser.step_at_methods.include?(node.children[1])
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
|
122
|
+
out << {
|
123
|
+
method: node.children[1],
|
124
|
+
line: node.loc.line,
|
125
|
+
column: node.loc.column,
|
126
|
+
as_string: node.loc.expression.source,
|
127
|
+
block_depth: depth,
|
128
|
+
}
|
129
|
+
|
130
|
+
true
|
131
|
+
end
|
78
132
|
end
|
data/lib/cyperful/ui_server.rb
CHANGED
@@ -106,6 +106,18 @@ class Cyperful::UiServer
|
|
106
106
|
|
107
107
|
res.status = 204
|
108
108
|
end
|
109
|
+
|
110
|
+
@server.mount_proc("/api/config") do |req, res|
|
111
|
+
if req.request_method != "GET"
|
112
|
+
res.body = "Only POST allowed"
|
113
|
+
res.status = 405
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
res.body = Cyperful.config.to_h.to_json
|
118
|
+
res["Content-Type"] = "application/json"
|
119
|
+
res.status = 200
|
120
|
+
end
|
109
121
|
end
|
110
122
|
|
111
123
|
def start_async
|
data/lib/cyperful.rb
CHANGED
@@ -6,6 +6,27 @@ module Cyperful
|
|
6
6
|
|
7
7
|
@current = nil
|
8
8
|
|
9
|
+
class Config < Struct.new(
|
10
|
+
:port,
|
11
|
+
:auto_run_on_reload,
|
12
|
+
:reload_test_files,
|
13
|
+
# :reload_source_files, # not implemented yet
|
14
|
+
:history_recording, # EXPERIMENTAL
|
15
|
+
keyword_init: true,
|
16
|
+
)
|
17
|
+
def initialize
|
18
|
+
super(
|
19
|
+
port: 3004,
|
20
|
+
auto_run_on_reload: true,
|
21
|
+
reload_test_files: true,
|
22
|
+
history_recording: true,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
def self.config
|
27
|
+
@config ||= Config.new
|
28
|
+
end
|
29
|
+
|
9
30
|
def self.current
|
10
31
|
@current
|
11
32
|
end
|
@@ -29,33 +50,12 @@ module Cyperful
|
|
29
50
|
@current&.teardown(error)
|
30
51
|
end
|
31
52
|
|
32
|
-
# more potential methods: https://www.rubydoc.info/github/jnicklas/capybara/Capybara/Session
|
33
|
-
@step_at_methods = [*Capybara::Session::NODE_METHODS, :visit, :refresh]
|
34
|
-
def self.step_at_methods
|
35
|
-
@step_at_methods
|
36
|
-
end
|
37
53
|
def self.add_step_at_methods(*mods_or_methods)
|
38
|
-
mods_or_methods
|
39
|
-
case mod_or_method
|
40
|
-
when Module
|
41
|
-
@step_at_methods +=
|
42
|
-
mod_or_method.methods(false) + mod_or_method.instance_methods(false)
|
43
|
-
when String, Symbol
|
44
|
-
@step_at_methods << mod_or_method.to_sym
|
45
|
-
else
|
46
|
-
raise "Expected Module or Array of strings/symbols, got #{mod_or_method.class}"
|
47
|
-
end
|
48
|
-
end
|
54
|
+
Cyperful::TestParser.add_step_at_methods(*mods_or_methods)
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
52
|
-
|
53
|
-
end
|
54
|
-
class Cyperful::ResetCommand < Cyperful::AbstractCommand
|
55
|
-
end
|
56
|
-
class Cyperful::ExitCommand < Cyperful::AbstractCommand
|
57
|
-
end
|
58
|
-
|
58
|
+
require "cyperful/commands"
|
59
59
|
require "cyperful/test_parser"
|
60
60
|
require "cyperful/ui_server"
|
61
61
|
require "cyperful/driver"
|