cyperful 0.1.3 → 0.1.7
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 +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"
|