cyperful 0.1.10 → 0.2.0

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: f340215ffcef3e16c1738d36eb2b71461bbd32e2011598d000793326f806b6d4
4
- data.tar.gz: 545e19084294a2d481a7e9d156f6ba5e4758fb52332c17dc2e2574be820b2256
3
+ metadata.gz: f85e5c56fa2637a6533a9fb13e8afd07278704d03f5703ec1c25c30b13a9ae31
4
+ data.tar.gz: 8c96954f38fb306b73b5a3c0423a277aef1d4911e5ca854d47067499e87ab6e4
5
5
  SHA512:
6
- metadata.gz: 524bbb91d707a457adfa888bc8db5a2d97886714dedc026d8647d2f243f8ff381f46a47af8cffe3fb4d46242aacfd515f2dda1b2a794252f758b5899ca312e9a
7
- data.tar.gz: e20223308f8c0df25df5aab64d97f24d4ef6f0b57bb530106e82a3234f5701f1ca434afa87dad9b50155f73043aef6b1afbe328ebb99aed35ad53fac29dc5ef9
6
+ metadata.gz: f39bbdf382d5f93cb7a85ebbcfbb78e3921b9ee7ad156e98320a2beb77c2be8c6247ff78b4bfef8d0be0fecaadfab24f28584a288cc3a5b6aef35e40d50be79e
7
+ data.tar.gz: 5cdb29c74f89f38024aad8ab9cf4d345285bbeacd22a3bc353ee3baf044fdbb1c08684572565deb10b2e380ddb6a0c65a204252fa3370ab86a9f25c7f199a972
@@ -1,6 +1,14 @@
1
1
  class Cyperful::Driver
2
2
  attr_reader :steps, :pausing
3
3
 
4
+ # delegate
5
+ private def logger
6
+ Cyperful.logger
7
+ end
8
+ private def config
9
+ Cyperful.config
10
+ end
11
+
4
12
  SCREENSHOTS_DIR = File.join(Cyperful::ROOT_DIR, "public/screenshots")
5
13
 
6
14
  def initialize
@@ -8,6 +16,12 @@ class Cyperful::Driver
8
16
 
9
17
  @session = Capybara.current_session
10
18
  raise "Could not find Capybara session" unless @session
19
+ unless @session.driver.is_a?(Capybara::Selenium::Driver)
20
+ raise "Cyperful only supports Selenium, got: #{@session.driver}"
21
+ end
22
+ unless @session.driver.browser.browser == :chrome
23
+ raise "Cyperful only supports Chrome, got: #{@session.driver.browser.browser}"
24
+ end
11
25
 
12
26
  setup_api_server
13
27
  end
@@ -17,8 +31,11 @@ class Cyperful::Driver
17
31
  @test_name = test_name.to_sym
18
32
 
19
33
  @source_filepath =
20
- Object.const_source_location(test_class.name).first ||
21
- raise("Could not find source file for #{test_class.name}")
34
+ if Cyperful.rspec?
35
+ test_class.metadata.fetch(:absolute_file_path)
36
+ else
37
+ Object.const_source_location(test_class.name).first
38
+ end || raise("Could not find source file for #{test_class.name}")
22
39
 
23
40
  reset_steps
24
41
 
@@ -61,7 +78,9 @@ class Cyperful::Driver
61
78
  @steps =
62
79
  Cyperful::TestParser.new(@test_class).steps_per_test.fetch(@test_name)
63
80
 
64
- editor_scheme = Cyperful.config.editor_scheme
81
+ raise "No steps found in #{@test_class}:#{@test_name}" if @steps.blank?
82
+
83
+ editor_scheme = config.editor_scheme
65
84
 
66
85
  @steps.each_with_index do |step, i|
67
86
  step.merge!(
@@ -115,19 +134,29 @@ class Cyperful::Driver
115
134
  Object.const_get(class_name)
116
135
  end
117
136
 
118
- def queue_reset
137
+ def enqueue_reset
119
138
  at_exit do
120
- # reload test-suite code on reset (for `setup_file_listener`)
121
- # TODO: also reload dependent files
122
- # NOTE: run_on_method will fail if test_name also changed
123
- @test_class = reload_const(@test_class.name, @source_filepath)
124
-
125
- # TODO
126
- # if Cyperful.config.reload_source_files && defined?(Rails)
127
- # Rails.application.reloader.reload!
128
- # end
129
-
130
- Minitest.run_one_method(@test_class, @test_name)
139
+ case Cyperful.test_framework
140
+ when :rspec
141
+ RSpec.configuration.reset # private API. this resets the test reporter
142
+ RSpec.configuration.start_time = RSpec::Core::Time.now # this needs to be reset
143
+ RSpec.world.reset # private API. this unloads constants and clears examples
144
+ RSpec::Core::Runner.invoke # this reloads and starts the test suite
145
+ when :minitest
146
+ # reload test-suite code on reset (for `setup_file_listener`)
147
+ # TODO: also reload dependent files
148
+ # NOTE: run_on_method will fail if test_name also changed
149
+ @test_class = reload_const(@test_class.name, @source_filepath)
150
+
151
+ # TODO
152
+ # if Cyperful.config.reload_source_files && defined?(Rails)
153
+ # Rails.application.reloader.reload!
154
+ # end
155
+
156
+ Minitest.run_one_method(@test_class, @test_name)
157
+ else
158
+ raise "Unsupported test framework: #{Cyperful.test_framework}"
159
+ end
131
160
  end
132
161
  end
133
162
 
@@ -161,11 +190,11 @@ class Cyperful::Driver
161
190
  # @source_file_listener = Listen.to(rails_directory) ...
162
191
  # end
163
192
 
164
- if Cyperful.config.reload_test_files
193
+ if config.reload_test_files
165
194
  @file_listener&.stop
166
195
  @file_listener =
167
196
  Listen.to(test_directory) do |_modified, _added, _removed|
168
- puts "Test files changed, resetting test..."
197
+ logger.puts "Test files changed, resetting test..."
169
198
 
170
199
  # keep the same pause state after the reload
171
200
  self.class.next_run_options = { pause_at_step: @pause_at_step }
@@ -178,13 +207,15 @@ class Cyperful::Driver
178
207
  end
179
208
 
180
209
  def print_steps
181
- puts "Found #{@steps.length} steps:"
210
+ logger.plain("Found #{@steps.length} steps:")
182
211
  @steps.each_with_index do |step, i|
183
- puts " #{
184
- (i + 1).to_s.rjust(2)
185
- }. #{step[:method]}: #{step[:line]}:#{step[:column]}"
212
+ logger.plain(
213
+ " #{
214
+ (i + 1).to_s.rjust(2)
215
+ }. #{step[:method]}: #{step[:line]}:#{step[:column]}",
216
+ )
186
217
  end
187
- puts
218
+ logger.plain
188
219
  end
189
220
 
190
221
  # pending (i.e. test hasn't started), paused, running, passed, failed
@@ -267,7 +298,7 @@ class Cyperful::Driver
267
298
  @current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
268
299
  @current_step[:status] = !error ? "passed" : "failed"
269
300
 
270
- puts(
301
+ logger.plain(
271
302
  " (#{@current_step[:end_at] - @current_step[:start_at]}ms)#{
272
303
  error ? " FAILED" : ""
273
304
  }",
@@ -293,7 +324,7 @@ class Cyperful::Driver
293
324
  end
294
325
 
295
326
  def drive_iframe
296
- puts "Driving iframe..."
327
+ logger.puts "Driving iframe..."
297
328
 
298
329
  # make sure a `within` block doesn't affect these commands
299
330
  without_finder_scopes do
@@ -353,8 +384,13 @@ class Cyperful::Driver
353
384
  end
354
385
 
355
386
  private def skip_multi_sessions
356
- unless Capybara.current_session == @session
357
- warn "Skipped Cyperful setup in non-default session: #{Capybara.session_name}"
387
+ if Capybara.current_session != @session
388
+ logger.warn "Skipped setup in non-default session"
389
+ # for debugging: {
390
+ # "current_session.mode": Capybara.current_session.mode,
391
+ # "session.mode": @session.mode,
392
+ # current_driver: Capybara.current_driver,
393
+ # }.to_json
358
394
  return true
359
395
  end
360
396
  false
@@ -390,7 +426,7 @@ class Cyperful::Driver
390
426
  end
391
427
 
392
428
  def setup_api_server
393
- @ui_server = Cyperful::UiServer.new(port: Cyperful.config.port)
429
+ @ui_server = Cyperful::UiServer.new(port: config.port)
394
430
 
395
431
  @cyperful_origin = @ui_server.url_origin
396
432
 
@@ -422,7 +458,7 @@ class Cyperful::Driver
422
458
  # The server appears to always stop on it's own,
423
459
  # so we don't need to stop it within an `at_exit` or `Minitest.after_run`
424
460
 
425
- puts "Cyperful server started: #{@cyperful_origin}"
461
+ logger.puts "server started: #{@cyperful_origin}"
426
462
  end
427
463
 
428
464
  def teardown(error = nil)
@@ -430,11 +466,11 @@ class Cyperful::Driver
430
466
  @tracepoint = nil
431
467
 
432
468
  if error&.is_a?(Cyperful::ResetCommand)
433
- puts "\nResetting test (ignore any error logs)..."
469
+ logger.puts "Resetting test (ignore any error logs)..."
434
470
 
435
471
  @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
436
472
 
437
- queue_reset
473
+ enqueue_reset
438
474
  return
439
475
  end
440
476
 
@@ -461,10 +497,10 @@ class Cyperful::Driver
461
497
 
462
498
  @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
463
499
 
464
- puts "Cyperful teardown complete. Waiting for command..."
500
+ logger.puts "teardown complete. Waiting for command..."
465
501
  # NOTE: this will raise an `Interrupt` if the user Ctrl+C's here
466
502
  command = @step_pausing_queue.deq
467
- queue_reset if command == :reset
503
+ enqueue_reset if command == :reset
468
504
  ensure
469
505
  @file_listener&.stop
470
506
  @file_listener = nil
@@ -0,0 +1,7 @@
1
+ module Cyperful::FrameworkHelper
2
+ # Disable default screenshot on failure b/c we handle them ourselves.
3
+ # https://github.com/rails/rails/blob/v7.0.1/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L156
4
+ def take_failed_screenshot
5
+ nil
6
+ end
7
+ end
@@ -1,29 +1,5 @@
1
1
  require "action_dispatch/system_testing/driver"
2
2
 
3
- # The Minitest test helper.
4
- # TODO: support other test frameworks like RSpec
5
- module Cyperful::SystemTestHelper
6
- def setup
7
- Cyperful.setup(self.class, self.method_name)
8
- super
9
- end
10
-
11
- def teardown
12
- error = passed? ? nil : failure
13
-
14
- error = error.error if error.is_a?(Minitest::UnexpectedError)
15
-
16
- Cyperful.teardown(error)
17
- super
18
- end
19
-
20
- # Disable default screenshot on failure b/c we handle them ourselves.
21
- # https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L156
22
- def take_failed_screenshot
23
- nil
24
- end
25
- end
26
-
27
3
  # we need to override the some Capybara::Session methods because they
28
4
  # control the top-level browser window, but we want them
29
5
  # to control the iframe instead
@@ -94,9 +70,5 @@ module PrependSystemTestingDriver
94
70
  end
95
71
  ActionDispatch::SystemTesting::Driver.prepend(PrependSystemTestingDriver)
96
72
 
97
- # if defined?(Minitest::Test)
98
- # Minitest::Test::PASSTHROUGH_EXCEPTIONS << Cyperful::AbstractCommand
99
- # end
100
-
101
73
  # fix for: Set-Cookie (SameSite=Lax) doesn't work when within an iframe with host 127.0.0.1
102
74
  Capybara.server_host = "localhost"
@@ -0,0 +1,14 @@
1
+ class Cyperful::Logger
2
+ def puts(message = nil)
3
+ # call puts method from Kernel module
4
+ Kernel.puts("[cyperful] #{message}")
5
+ end
6
+
7
+ def warn(message = nil)
8
+ Kernel.warn("[cyperful] #{message}")
9
+ end
10
+
11
+ def plain(message = nil)
12
+ Kernel.puts(message)
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ require "cyperful"
2
+ require "capybara/minitest"
3
+
4
+ module Cyperful::Minitest # rubocop:disable Style/ClassAndModuleChildren
5
+ module SystemTestHelper
6
+ include Cyperful::FrameworkHelper
7
+
8
+ def setup
9
+ Cyperful.setup(self.class, self.method_name)
10
+ super
11
+ end
12
+
13
+ def teardown
14
+ error = passed? ? nil : failure
15
+
16
+ error = error.error if error.is_a?(Minitest::UnexpectedError)
17
+
18
+ Cyperful.teardown(error)
19
+ super
20
+ end
21
+ end
22
+ end
23
+
24
+ Cyperful.add_step_at_methods(
25
+ Capybara::Minitest::Assertions.instance_methods(false),
26
+ )
27
+
28
+ # if defined?(Minitest::Test)
29
+ # Minitest::Test::PASSTHROUGH_EXCEPTIONS << Cyperful::AbstractCommand
30
+ # end
@@ -0,0 +1,24 @@
1
+ require "cyperful"
2
+
3
+ RSpec.configure do |rspec_conf|
4
+ rspec_conf.include(Cyperful::FrameworkHelper)
5
+
6
+ rspec_conf.before(:example, type: :system) do
7
+ # e.g. class = `RSpec::ExampleGroups::MyTest`
8
+ # e.g. full_description = "my_test can visit root"
9
+ example = RSpec.current_example
10
+ Cyperful.setup(self.class, example.full_description)
11
+ end
12
+
13
+ rspec_conf.after(:example, type: :system) do
14
+ example = RSpec.current_example
15
+ error = example.exception # RSpec::Expectations::ExpectationNotMetError | nil
16
+
17
+ Cyperful.teardown(error)
18
+ end
19
+ end
20
+
21
+ Cyperful.add_step_at_methods(
22
+ Capybara::RSpecMatchers.instance_methods(false),
23
+ # Capybara::RSpecMatcherProxies.instance_methods(false),
24
+ )
@@ -1,11 +1,9 @@
1
- require "parser/current"
2
- require "capybara/minitest"
1
+ require "parser/current" # TODO: switch to Prism?
3
2
 
4
3
  class Cyperful::TestParser
5
4
  # see docs for methods: https://www.rubydoc.info/github/jnicklas/capybara/Capybara/Session
6
5
  @step_at_methods =
7
- Capybara::Session::DSL_METHODS.to_set +
8
- Capybara::Minitest::Assertions.instance_methods(false) -
6
+ Capybara::Session::DSL_METHODS -
9
7
  # exclude methods that don't have side-effects i.e. don't modify the page:
10
8
  %i[
11
9
  body
@@ -25,7 +23,7 @@ class Cyperful::TestParser
25
23
  @step_at_methods_set ||= @step_at_methods.to_set
26
24
  end
27
25
  def self.add_step_at_methods(*mods_or_methods)
28
- mods_or_methods.each do |mod_or_method|
26
+ mods_or_methods.flatten.each do |mod_or_method|
29
27
  case mod_or_method
30
28
  when Module
31
29
  @step_at_methods +=
@@ -33,17 +31,81 @@ class Cyperful::TestParser
33
31
  when String, Symbol
34
32
  @step_at_methods << mod_or_method.to_sym
35
33
  else
36
- raise "Expected Module or Array of strings/symbols, got #{mod_or_method.class}"
34
+ raise "Expected Module or string/symbol, got: #{mod_or_method.class.name}"
37
35
  end
38
36
  end
39
37
  end
40
38
 
41
39
  def initialize(test_class)
42
40
  @test_class = test_class
43
- @source_filepath = Object.const_source_location(test_class.name).first
41
+
42
+ @source_filepath =
43
+ if Cyperful.rspec?
44
+ test_class.metadata.fetch(:absolute_file_path)
45
+ else
46
+ Object.const_source_location(test_class.name).first
47
+ end
44
48
  end
45
49
 
46
50
  def steps_per_test
51
+ case Cyperful.test_framework
52
+ when :rspec
53
+ rspec_steps_per_test
54
+ when :minitest
55
+ minitest_steps_per_test
56
+ else
57
+ raise "Unsupported test framework: #{Cyperful.test_framework}"
58
+ end
59
+ end
60
+
61
+ private def rspec_steps_per_test
62
+ ast = Parser::CurrentRuby.parse(File.read(@source_filepath))
63
+
64
+ example_per_line =
65
+ @test_class.examples.to_h do |example|
66
+ # file_path = example.metadata.fetch(:absolute_file_path)
67
+ [example.metadata.fetch(:line_number) || -1, example]
68
+ end
69
+
70
+ example_asts =
71
+ search_nodes(ast) do |node|
72
+ next nil unless node.type == :block
73
+
74
+ # assumption: the block is on the same line as the example, and there's at most one example per line
75
+ example = example_per_line[node.loc.begin.line]
76
+ next nil unless example
77
+
78
+ # "#{@test_class.name} #{example.description} #{}"
79
+
80
+ [example, node]
81
+ end
82
+
83
+ example_asts.to_h do |(example, block_node)|
84
+ out = []
85
+ block_node.children.each { |child| find_test_steps(child, out) }
86
+
87
+ # de-duplicate steps by line number
88
+ # TODO: support multiple steps on the same line. `step_per_line = ...` needs to be refactored
89
+ out = out.reverse.uniq { |step| step[:line] }.reverse
90
+
91
+ [example.full_description.to_sym, out]
92
+ end
93
+ end
94
+
95
+ private def search_nodes(parent, out = [], &block)
96
+ parent.children.each do |node|
97
+ next unless node.is_a?(Parser::AST::Node)
98
+ if (ret = block.call(node))
99
+ #
100
+ out << ret
101
+ else
102
+ search_nodes(node, out, &block)
103
+ end
104
+ end
105
+ out
106
+ end
107
+
108
+ private def minitest_steps_per_test
47
109
  ast = Parser::CurrentRuby.parse(File.read(@source_filepath))
48
110
 
49
111
  test_class_name = @test_class.name.to_sym
@@ -57,7 +119,7 @@ class Cyperful::TestParser
57
119
  end
58
120
  end
59
121
  unless system_test_class
60
- raise "Could not find class #{test_class.name} in #{@source_filepath}"
122
+ raise "Could not find class #{@test_class.name} in #{@source_filepath}"
61
123
  end
62
124
 
63
125
  (
@@ -76,21 +138,21 @@ class Cyperful::TestParser
76
138
  test_string = node.children[0].children[2].children[0]
77
139
 
78
140
  # https://github.com/rails/rails/blob/66676ce499a32e4c62220bd05f8ee2cdf0e15f0c/activesupport/lib/active_support/testing/declarative.rb#L14C23-L14C61
79
- test_method = :"test_#{test_string.gsub(/\s+/, "_")}"
141
+ test_name = :"test_#{test_string.gsub(/\s+/, "_")}"
80
142
 
81
143
  block_node = node.children[2]
82
- [test_method, block_node]
144
+ [test_name, block_node]
83
145
 
84
146
  # e.g. `def test_my_test; ... end`
85
147
  elsif node.type == :def && node.children[0].to_s.start_with?("test_")
86
- test_method = node.children[0]
148
+ test_name = node.children[0]
87
149
 
88
150
  block_node = node.children[2]
89
- [test_method, block_node]
151
+ [test_name, block_node]
90
152
  end
91
153
  end
92
154
  .compact
93
- .to_h do |test_method, block_node|
155
+ .to_h do |test_name, block_node|
94
156
  out = []
95
157
  block_node.children.each { |child| find_test_steps(child, out) }
96
158
 
@@ -98,7 +160,7 @@ class Cyperful::TestParser
98
160
  # TODO: support multiple steps on the same line. `step_per_line = ...` needs to be refactored
99
161
  out = out.reverse.uniq { |step| step[:line] }.reverse
100
162
 
101
- [test_method, out]
163
+ [test_name, out]
102
164
  end
103
165
  end
104
166
 
@@ -122,6 +184,8 @@ class Cyperful::TestParser
122
184
  end
123
185
 
124
186
  children.each { |child| find_test_steps(child, out, depth) }
187
+ elsif ast.type == :begin || ast.type == :kwbegin || ast.type == :ensure
188
+ ast.children.each { |child| find_test_steps(child, out, depth) }
125
189
  end
126
190
 
127
191
  out
data/lib/cyperful.rb CHANGED
@@ -33,11 +33,36 @@ module Cyperful
33
33
  @config ||= Config.new
34
34
  end
35
35
 
36
+ def self.logger
37
+ @logger ||= Cyperful::Logger.new
38
+ end
39
+
40
+ def self.test_framework
41
+ @test_framework || raise("Cyperful not set up yet")
42
+ end
43
+ def self.rspec?
44
+ @test_framework == :rspec
45
+ end
46
+ def self.minitest?
47
+ @test_framework == :minitest
48
+ end
49
+
36
50
  def self.current
37
51
  @current
38
52
  end
39
53
  def self.setup(test_class, test_name)
40
- puts "Setting up Cyperful for: #{test_class}##{test_name}"
54
+ @test_framework =
55
+ if defined?(RSpec::Core::ExampleGroup) &&
56
+ test_class < RSpec::Core::ExampleGroup
57
+ :rspec
58
+ elsif defined?(ActiveSupport::TestCase) &&
59
+ test_class < ActiveSupport::TestCase
60
+ :minitest
61
+ else
62
+ raise "Unsupported test framework for class: #{test_class.name}"
63
+ end
64
+
65
+ logger.puts "init test: \"#{rspec? ? test_name : "#{test_class}##{test_name}"}\""
41
66
 
42
67
  # must set `Cyperful.current` before calling `async_setup`
43
68
  @current ||= Cyperful::Driver.new
@@ -62,8 +87,10 @@ module Cyperful
62
87
  end
63
88
 
64
89
  require "cyperful/commands"
90
+ require "cyperful/logger"
65
91
  require "cyperful/test_parser"
66
92
  require "cyperful/ui_server"
67
93
  require "cyperful/driver"
94
+ require "cyperful/framework_helper"
68
95
  require "cyperful/framework_injections"
69
96
  require "cyperful/railtie" if defined?(Rails::Railtie)