Class: Cyperful::Driver
- Inherits:
-
Object
- Object
- Cyperful::Driver
- Defined in:
- lib/cyperful/driver.rb
Constant Summary collapse
- SCREENSHOTS_DIR =
File.join(Cyperful::ROOT_DIR, "public/screenshots")
- WATCHER_JS =
File.read(File.join(Cyperful::ROOT_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 ⇒ Driver
constructor
A new instance of Driver.
- #internal_current_url ⇒ Object
- #internal_visit(url) ⇒ Object
- #pause_on_step(step) ⇒ Object
- #print_steps ⇒ Object
- #queue_reset ⇒ 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 ⇒ Driver
Returns a new instance of Driver.
6 7 8 9 10 11 12 13 |
# File 'lib/cyperful/driver.rb', line 6 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.
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 |
Instance Method Details
#drive_iframe ⇒ Object
234 235 236 237 238 239 240 241 |
# File 'lib/cyperful/driver.rb', line 234 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
292 293 294 295 296 |
# File 'lib/cyperful/driver.rb', line 292 def internal_current_url return nil unless @driving_iframe @session.evaluate_script("window.location.href") end |
#internal_visit(url) ⇒ Object
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/cyperful/driver.rb', line 271 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
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/cyperful/driver.rb', line 211 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
130 131 132 133 134 135 136 |
# File 'lib/cyperful/driver.rb', line 130 def print_steps puts "#{@steps.length} steps:" @steps.each do |step| puts " #{step[:method]}: #{step[:line]}:#{step[:column]}" end puts end |
#queue_reset ⇒ Object
90 91 92 93 94 |
# File 'lib/cyperful/driver.rb', line 90 def queue_reset # FIXME: there may be other tests that are "queued" to run `at_exit`, # so they'll run before this test restarts. at_exit { Minitest.run_one_method(@test_class, @test_name) } end |
#reset_steps ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/cyperful/driver.rb', line 59 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
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/cyperful/driver.rb', line 15 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
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/cyperful/driver.rb', line 298 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
116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/cyperful/driver.rb', line 116 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.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/cyperful/driver.rb', line 98 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
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/cyperful/driver.rb', line 46 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
172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/cyperful/driver.rb', line 172 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
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 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 |
# File 'lib/cyperful/driver.rb', line 334 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`) queue_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.select do |s| i ||= 0 if s.include?(@source_filepath) i += 1 if i break if i && i > 4 true 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`) puts "Cyperful teardown complete. Waiting for command..." command = @step_pausing_queue.deq queue_reset if command == :reset end |
#test_duration_ms ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/cyperful/driver.rb', line 152 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
139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/cyperful/driver.rb', line 139 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 |