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 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"