Class: Cyperful::Driver
- Inherits:
-
Object
- Object
- Cyperful::Driver
- Defined in:
- lib/cyperful/driver.rb
Constant Summary collapse
Instance Attribute Summary collapse
-
#pausing ⇒ Object
readonly
Returns the value of attribute pausing.
-
#steps ⇒ Object
readonly
Returns the value of attribute steps.
Class Method Summary collapse
Instance Method Summary collapse
- #drive_iframe ⇒ Object
- #enqueue_reset ⇒ Object
-
#initialize ⇒ Driver
constructor
A new instance of Driver.
- #internal_current_url ⇒ Object
- #internal_visit(url) ⇒ Object
-
#pause_on_step(step) ⇒ Object
called at the start of each step.
- #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 on reload.
-
#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_status ⇒ Object
pending (i.e. test hasn’t started), paused, running, passed, failed.
Constructor Details
#initialize ⇒ Driver
Returns a new instance of Driver.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/cyperful/driver.rb', line 14 def initialize @step_pausing_queue = Queue.new @session = Capybara.current_session raise "Could not find Capybara session" unless @session unless @session.driver.is_a?(Capybara::Selenium::Driver) raise "Cyperful only supports Selenium, got: #{@session.driver}" end unless @session.driver.browser.browser == :chrome raise "Cyperful only supports Chrome, got: #{@session.driver.browser.browser}" end setup_api_server end |
Instance Attribute Details
#pausing ⇒ Object (readonly)
Returns the value of attribute pausing.
2 3 4 |
# File 'lib/cyperful/driver.rb', line 2 def pausing @pausing end |
#steps ⇒ Object (readonly)
Returns the value of attribute steps.
2 3 4 |
# File 'lib/cyperful/driver.rb', line 2 def steps @steps end |
Class Method Details
.load_frame_agent_js ⇒ Object
376 377 378 379 380 381 382 383 384 |
# File 'lib/cyperful/driver.rb', line 376 def self.load_frame_agent_js return @frame_agent_js if defined?(@frame_agent_js) @frame_agent_js = File.read(File.join(Cyperful::ROOT_DIR, "public/frame-agent.js")).sub( "__CYPERFUL_CONFIG__", { CYPERFUL_ORIGIN: "http://localhost:#{Cyperful.config.port}" }.to_json, ) end |
.next_run_options=(options) ⇒ Object
119 120 121 |
# File 'lib/cyperful/driver.rb', line 119 def self.() @next_run_options = end |
.pop_run_options! ⇒ Object
122 123 124 125 126 |
# File 'lib/cyperful/driver.rb', line 122 def self. opts = @next_run_options @next_run_options = {} opts end |
Instance Method Details
#drive_iframe ⇒ Object
326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/cyperful/driver.rb', line 326 def drive_iframe logger.puts "Driving iframe..." # make sure a `within` block doesn't affect these commands without_finder_scopes do @session.switch_to_frame( # `find` waits for the iframe to load @session.find(:css, "iframe#scenario-frame"), ) end @driving_iframe = true end |
#enqueue_reset ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/cyperful/driver.rb', line 137 def enqueue_reset at_exit do case Cyperful.test_framework when :rspec RSpec.configuration.reset # private API. this resets the test reporter RSpec.configuration.start_time = RSpec::Core::Time.now # this needs to be reset RSpec.world.reset # private API. this unloads constants and clears examples RSpec::Core::Runner.invoke # this reloads and starts the test suite when :minitest # reload test-suite code on reset (for `setup_file_listener`) # TODO: also reload dependent files # NOTE: run_on_method will fail if test_name also changed @test_class = reload_const(@test_class.name, @source_filepath) # TODO # if Cyperful.config.reload_source_files && defined?(Rails) # Rails.application.reloader.reload! # end Minitest.run_one_method(@test_class, @test_name) else raise "Unsupported test framework: #{Cyperful.test_framework}" end end end |
#internal_current_url ⇒ Object
421 422 423 424 425 426 |
# File 'lib/cyperful/driver.rb', line 421 def internal_current_url return nil unless @driving_iframe return nil if skip_multi_sessions @session.evaluate_script("window.location.href") end |
#internal_visit(url) ⇒ Object
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 |
# File 'lib/cyperful/driver.rb', line 399 def internal_visit(url) return false unless @driving_iframe return false if skip_multi_sessions 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 frame-agent 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(Cyperful::Driver.load_frame_agent_js) # ~9ms empirically true end |
#pause_on_step(step) ⇒ Object
called at the start of each step
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/cyperful/driver.rb', line 275 def pause_on_step(step) @current_step = step # using `print` so we can append the step's status (see `finish_current_step`) print("STEP #{(step[:index] + 1).to_s.rjust(2)}: #{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
209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/cyperful/driver.rb', line 209 def print_steps logger.plain("Found #{@steps.length} steps:") @steps.each_with_index do |step, i| logger.plain( " #{ (i + 1).to_s.rjust(2) }. #{step[:method]}: #{step[:line]}:#{step[:column]}", ) end logger.plain end |
#reset_steps ⇒ Object
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 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/cyperful/driver.rb', line 76 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) raise "No steps found in #{@test_class}:#{@test_name}" if @steps.blank? editor_scheme = config.editor_scheme @steps.each_with_index do |step, i| step.merge!( index: i, status: "pending", start_at: nil, end_at: nil, paused_at: nil, permalink: if editor_scheme && !editor_scheme.empty? "#{editor_scheme}://file/#{@source_filepath}:#{step.fetch(:line)}" end, ) end # TODO: support multiple multiple steps per line, this takes only the last instance @step_per_line = @steps.index_by { |step| step[:line] } @current_step = nil @pause_at_step = true = self.class. if .key?(:pause_at_step) @pause_at_step = [:pause_at_step] end @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
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/cyperful/driver.rb', line 29 def set_current_test(test_class, test_name) @test_class = test_class @test_name = test_name.to_sym @source_filepath = if Cyperful.rspec? test_class..fetch(:absolute_file_path) else Object.const_source_location(test_class.name).first end || 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 "Unexpected: step_pausing_queue must be empty during setup" end # Wait for the user to click "Start" step_pausing_dequeue if @pause_at_step == true end |
#setup_api_server ⇒ Object
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/cyperful/driver.rb', line 428 def setup_api_server @ui_server = Cyperful::UiServer.new(port: config.port) @cyperful_origin = @ui_server.url_origin @ui_server.on_command do |command, params| case command.to_sym 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` logger.puts "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 on reload
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/cyperful/driver.rb', line 187 def setup_file_listener # TODO # if Cyperful.config.reload_source_files # @source_file_listener = Listen.to(rails_directory) ... # end if config.reload_test_files @file_listener&.stop @file_listener = Listen.to(test_directory) do |_modified, _added, _removed| logger.puts "Test files changed, resetting test..." # keep the same pause state after the reload self.class. = { pause_at_step: @pause_at_step } @pause_at_step = true # pause current test immediately @step_pausing_queue.enq(:reset) end @file_listener.start end 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.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/cyperful/driver.rb', line 165 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
63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/cyperful/driver.rb', line 63 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
255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/cyperful/driver.rb', line 255 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
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 |
# File 'lib/cyperful/driver.rb', line 464 def teardown(error = nil) @tracepoint&.disable @tracepoint = nil if error&.is_a?(Cyperful::ResetCommand) logger.puts "Resetting test (ignore any error logs)..." @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`) enqueue_reset return end return if error&.is_a?(Cyperful::ExitCommand) if error # get the 4 lines following the first line that includes the source file i = nil backtrace = [] error.backtrace.each do |s| i ||= 0 if s.include?(@source_filepath) next unless i i += 1 backtrace << s break if i >= 6 end 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`) logger.puts "teardown complete. Waiting for command..." # NOTE: this will raise an `Interrupt` if the user Ctrl+C's here command = @step_pausing_queue.deq enqueue_reset if command == :reset ensure @file_listener&.stop @file_listener = nil end |
#test_status ⇒ Object
pending (i.e. test hasn’t started), paused, running, passed, failed
222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/cyperful/driver.rb', line 222 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 |