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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0cc8a2ac49a7d7223268f31ec69e960e1fa5bf48fa98a92a5f3adc7b14776f7
4
- data.tar.gz: 73be4bde0f5750a4366814527e890a9425764b885c4c72e5bea3717a9dfb6fa6
3
+ metadata.gz: 83254b2335dc0d99b443c2aad2260adc7c4f56bebaf32847a9e4ce151c03c6bc
4
+ data.tar.gz: 5a444d60e56c014569604e91162943aed5d6cb4f21655aaff8405be62d67b622
5
5
  SHA512:
6
- metadata.gz: 3b67bf8da9159ff69167d5397a6e87f04980df7504cd46946710d8f28e3ead1a2ccf43738060c38552542d841ae76c6df70bda92d0e20fb9d5f9fc3395551f08
7
- data.tar.gz: 5c3cdef286ca10f0f609f3cfd550c138747fa71af31ed9a8ed73006f8b422d46d1b75492383d6f1be83bf6c8ab196e4584522bbeacffa5f9a385920ece28ca06
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
@@ -18,7 +18,7 @@ class Cyperful::Driver
18
18
 
19
19
  @source_filepath =
20
20
  Object.const_source_location(test_class.name).first ||
21
- (raise "Could not find source file for #{test_class.name}")
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 is not empty during setup"
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
- # 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) }
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
- # 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..."
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
- @pause_at_step = true
125
- @step_pausing_queue.enq(:reset)
126
- end
127
- @file_listener.start
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.each do |step|
133
- puts " #{step[:method]}: #{step[:line]}:#{step[:column]}"
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
- @session.switch_to_frame(
238
- @session.find(:css, "iframe#scenario-frame"), # waits for the iframe to load
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
- WATCHER_JS = File.read(File.join(Cyperful::ROOT_DIR, "watcher.js"))
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 watcher script into the page being tested.
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(WATCHER_JS) # ~9ms empirically
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: 3004)
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 "\nPlease ignore the error, we're just resetting the test ;)"
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
- i += 1 if i
359
- break if i && i > 4
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:3004")
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."
@@ -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
- # the children of the `class` node are either:
28
- # - a `begin` node if there's more than 1 child node
29
- # - or the one 0 or 1 child node
30
- system_test_class
31
- .children
32
- .find { |node| node.type == :begin }
33
- &.children || [system_test_class.children[2]].compact
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+/, "_")}".to_sym
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
- test_method,
55
- find_test_steps(block_node)
56
- # sanity check:
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 && Cyperful.step_at_methods.include?(ast.children[1])
66
- out << {
67
- method: ast.children[1],
68
- line: ast.loc.line,
69
- column: ast.loc.column,
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
- ast.children.each { |child| find_test_steps(child, out) }
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
@@ -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.each do |mod_or_method|
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
- class Cyperful::AbstractCommand < StandardError
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"