Class: Cyperful::SystemSteps
- Inherits:
-
Object
- Object
- Cyperful::SystemSteps
- Defined in:
- lib/cyperful.rb
Constant Summary collapse
- SCREENSHOTS_DIR =
File.("../public/screenshots", __dir__)
- WATCHER_JS =
File.read(File.join(__dir__, "../watcher.js"))
Instance Attribute Summary collapse
-
#pausing ⇒ Object
readonly
Returns the value of attribute pausing.
-
#steps ⇒ Object
readonly
Returns the value of attribute steps.
Instance Method Summary collapse
- #drive_iframe ⇒ Object
-
#initialize ⇒ SystemSteps
constructor
A new instance of SystemSteps.
- #internal_current_url ⇒ Object
- #internal_visit(url) ⇒ Object
- #pause_on_step(step) ⇒ Object
- #print_steps ⇒ Object
- #reset_steps ⇒ Object
- #set_current_test(test_class, test_name) ⇒ Object
- #setup_api_server ⇒ Object
-
#setup_file_listener ⇒ Object
Every time a file changes the ‘test/` directory, reset this test TODO: add an option to auto-run.
-
#setup_tracing ⇒ Object
subscribe to the execution of each line of code in the test.
- #step_pausing_dequeue ⇒ Object
- #steps_updated_data ⇒ Object
- #teardown(error = nil) ⇒ Object
- #test_duration_ms ⇒ Object
-
#test_status ⇒ Object
pending (i.e. test hasn’t started), paused, running, passed, failed.
Constructor Details
#initialize ⇒ SystemSteps
Returns a new instance of SystemSteps.
65 66 67 68 69 70 71 72 |
# File 'lib/cyperful.rb', line 65 def initialize @step_pausing_queue = Queue.new @session = Capybara.current_session raise "Could not find Capybara session" unless @session setup_api_server end |
Instance Attribute Details
#pausing ⇒ Object (readonly)
Returns the value of attribute pausing.
61 62 63 |
# File 'lib/cyperful.rb', line 61 def pausing @pausing end |
#steps ⇒ Object (readonly)
Returns the value of attribute steps.
61 62 63 |
# File 'lib/cyperful.rb', line 61 def steps @steps end |
Instance Method Details
#drive_iframe ⇒ Object
287 288 289 290 291 292 293 294 |
# File 'lib/cyperful.rb', line 287 def drive_iframe puts "Driving iframe..." @session.switch_to_frame( @session.find(:css, "iframe#scenario-frame"), # waits for the iframe to load ) @driving_iframe = true end |
#internal_current_url ⇒ Object
345 346 347 348 349 |
# File 'lib/cyperful.rb', line 345 def internal_current_url return nil unless @driving_iframe @session.evaluate_script("window.location.href") end |
#internal_visit(url) ⇒ Object
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/cyperful.rb', line 324 def internal_visit(url) return false unless @driving_iframe abs_url, display_url = make_absolute_url(url) # show the actual `visit` url as soon as it's computed if @current_step && @current_step[:method] == :visit @current_step[:as_string] = "visit #{display_url.to_json}" notify_updated_steps end @session.execute_script("window.location.href = #{abs_url.to_json}") # inject the watcher script into the page being tested. # this script will notify the Cyperful UI for events like: # console logs, network requests, client navigations, errors, etc. @session.execute_script(WATCHER_JS) # ~9ms empirically true end |
#pause_on_step(step) ⇒ Object
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/cyperful.rb', line 264 def pause_on_step(step) @current_step = step puts "STEP: #{step[:as_string]}" if @pause_at_step == true || @pause_at_step == step[:index] @current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i @current_step[:status] = "paused" notify_updated_steps # async wait for `continue_next_step` step_pausing_dequeue end @current_step[:status] = "running" @current_step[:start_at] = (Time.now.to_f * 1000.0).to_i notify_updated_steps end |
#print_steps ⇒ Object
183 184 185 186 187 188 189 |
# File 'lib/cyperful.rb', line 183 def print_steps puts "#{@steps.length} steps:" @steps.each do |step| puts " #{step[:method]}: #{step[:line]}:#{step[:column]}" end puts end |
#reset_steps ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/cyperful.rb', line 118 def reset_steps # TODO: memoize this when there's multiple tests per file @steps = Cyperful::TestParser.new(@test_class).steps_per_test.fetch(@test_name) editor = "vscode" # TODO: support other editors? @steps.each_with_index do |step, i| step.merge!( index: i, status: "pending", start_at: nil, end_at: nil, paused_at: nil, permalink: "#{editor}://file/#{@source_filepath}:#{step.fetch(:line)}", ) end @step_per_line = @steps.index_by { |step| step[:line] } @current_step = nil @pause_at_step = true @test_result = nil # reset SCREENSHOTS_DIR FileUtils.rm_rf(SCREENSHOTS_DIR) FileUtils.mkdir_p(SCREENSHOTS_DIR) end |
#set_current_test(test_class, test_name) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/cyperful.rb', line 74 def set_current_test(test_class, test_name) @test_class = test_class @test_name = test_name.to_sym @source_filepath = Object.const_source_location(test_class.name).first || (raise "Could not find source file for #{test_class.name}") reset_steps print_steps @session.visit(@cyperful_origin) drive_iframe # after we setup our UI, send the initialization data notify_updated_steps setup_tracing setup_file_listener # Sanity check unless @step_pausing_queue.empty? raise "step_pausing_queue is not empty during setup" end # Wait for the user to click "Start" step_pausing_dequeue end |
#setup_api_server ⇒ Object
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/cyperful.rb', line 351 def setup_api_server @ui_server = Cyperful::UiServer.new(port: 3004) @cyperful_origin = @ui_server.url_origin @ui_server.on_command do |command, params| case command when "start" # one of: integer (index of a step), true (pause at every step), or nil (don't pause) @pause_at_step = params["pause_at_step"] continue_next_step when "reset" @pause_at_step = true @step_pausing_queue.enq(:reset) when "stop" @pause_at_step = true # enable pausing when "exit" @pause_at_step = true # instead of calling `exit` directly, we need to raise a Cyperful::ExitCommand error # so Minitest can finish it's teardown e.g. to reset the database @step_pausing_queue.enq(:exit) else raise "unknown command: #{command}" end end @ui_server.start_async # The server appears to always stop on it's own, # so we don't need to stop it within an `at_exit` or `Minitest.after_run` puts "Cyperful server started: #{@cyperful_origin}" end |
#setup_file_listener ⇒ Object
Every time a file changes the ‘test/` directory, reset this test TODO: add an option to auto-run
169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/cyperful.rb', line 169 def setup_file_listener test_dir = @source_filepath.match(%r{^/.+/(test|spec)\b})[0] @file_listener&.stop @file_listener = Listen.to(test_dir) do |_modified, _added, _removed| puts "Test files changed, resetting test..." @pause_at_step = true @step_pausing_queue.enq(:reset) end @file_listener.start end |
#setup_tracing ⇒ Object
subscribe to the execution of each line of code in the test. this let’s us notify the frontend of the line’s status, and pause execution if needed.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/cyperful.rb', line 151 def setup_tracing @tracepoint&.disable @tracepoint = TracePoint.new(:line) do |tp| next if @source_filepath.nil? || tp.path != @source_filepath finish_current_step step = @step_per_line[tp.lineno] pause_on_step(step) if step end @tracepoint.enable end |
#step_pausing_dequeue ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/cyperful.rb', line 105 def step_pausing_dequeue command = @step_pausing_queue.deq if command == :reset raise Cyperful::ResetCommand elsif command == :exit raise Cyperful::ExitCommand elsif command == :next # just continue else raise "unknown command: #{command}" end end |
#steps_updated_data ⇒ Object
225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/cyperful.rb', line 225 def steps_updated_data status = self.test_status { event: "steps_updated", steps: @steps, current_step_index: @current_step&.[](:index), pause_at_step: @pause_at_step, test_suite: @test_class.name, test_name: @test_name, test_status: status, test_error: @test_result&.[](:error)&.to_s, test_duration_ms: test_duration_ms, } end |
#teardown(error = nil) ⇒ Object
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 |
# File 'lib/cyperful.rb', line 387 def teardown(error = nil) @tracepoint&.disable @tracepoint = nil @file_listener&.stop @file_listener = nil if error&.is_a?(Cyperful::ResetCommand) puts "\nPlease ignore the error, we're just resetting the test ;)" @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`) at_exit { Minitest.run_one_method(@test_class, @test_name) } return end return if error&.is_a?(Cyperful::ExitCommand) if error # backtrace = error.backtrace.select { |s| s.include?(@source_filepath) } backtrace = error.backtrace.slice(0, 4) warn "\n\nTest failed with error:\n#{error.}\n#{backtrace.join("\n")}" end @test_result = { status: error ? "failed" : "passed", error: error } finish_current_step(error) @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`) puts "Cyperful teardown complete. Waiting for command..." command = @step_pausing_queue.deq if command == :reset at_exit { Minitest.run_one_method(@test_class, @test_name) } end end |
#test_duration_ms ⇒ Object
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/cyperful.rb', line 205 def test_duration_ms start_at = @steps.first&.[](:start_at) return nil unless start_at last_ended_step_i = @steps.rindex { |step| step[:end_at] } return nil unless last_ended_step_i end_at = @steps[last_ended_step_i][:end_at] duration = end_at - start_at @steps.each_with_index do |step, i| next if i == 0 || i > last_ended_step_i if step[:paused_at] && step[:start_at] duration -= (step[:start_at] - step[:paused_at]) end end duration end |
#test_status ⇒ Object
pending (i.e. test hasn’t started), paused, running, passed, failed
192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/cyperful.rb', line 192 def test_status return @test_result[:status] if @test_result # passed or failed if @pause_at_step return "running" if @steps.any? { |step| step[:status] == "running" } return "pending" unless @current_step return "paused" end "running" end |