cyperful 0.1.4 → 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 +4 -4
- data/lib/cyperful/commands.rb +19 -0
- data/lib/cyperful/driver.rb +111 -46
- data/lib/cyperful/framework_injections.rb +38 -1
- data/lib/cyperful/test_parser.rb +1 -1
- data/lib/cyperful/ui_server.rb +12 -0
- data/lib/cyperful.rb +22 -7
- data/public/assets/index-CDElGKtz.css +1 -0
- data/public/assets/index-DMlaSnZ7.js +54 -0
- data/public/frame-agent.js +120 -0
- data/public/index.html +3 -5
- metadata +9 -9
- data/public/assets/index-25bd736c.css +0 -1
- data/public/assets/index-c11ede11.js +0 -40
- data/public/assets/logo-169f5e20.svg +0 -72
- data/watcher.js +0 -151
- /data/public/assets/{favicon-9f6bc28c.ico → favicon-DMdBZQlK.ico} +0 -0
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 83254b2335dc0d99b443c2aad2260adc7c4f56bebaf32847a9e4ce151c03c6bc
         | 
| 4 | 
            +
              data.tar.gz: 5a444d60e56c014569604e91162943aed5d6cb4f21655aaff8405be62d67b622
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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
         | 
    
        data/lib/cyperful/driver.rb
    CHANGED
    
    | @@ -18,7 +18,7 @@ class Cyperful::Driver | |
| 18 18 |  | 
| 19 19 | 
             
                @source_filepath =
         | 
| 20 20 | 
             
                  Object.const_source_location(test_class.name).first ||
         | 
| 21 | 
            -
                    ( | 
| 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  | 
| 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
         | 
| @@ -81,6 +81,11 @@ class Cyperful::Driver | |
| 81 81 |  | 
| 82 82 | 
             
                @pause_at_step = true
         | 
| 83 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 | 
            +
             | 
| 84 89 | 
             
                @test_result = nil
         | 
| 85 90 |  | 
| 86 91 | 
             
                # reset SCREENSHOTS_DIR
         | 
| @@ -88,10 +93,39 @@ class Cyperful::Driver | |
| 88 93 | 
             
                FileUtils.mkdir_p(SCREENSHOTS_DIR)
         | 
| 89 94 | 
             
              end
         | 
| 90 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 | 
            +
             | 
| 91 115 | 
             
              def queue_reset
         | 
| 92 | 
            -
                 | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 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
         | 
| 95 129 | 
             
              end
         | 
| 96 130 |  | 
| 97 131 | 
             
              # subscribe to the execution of each line of code in the test.
         | 
| @@ -111,28 +145,41 @@ class Cyperful::Driver | |
| 111 145 | 
             
                @tracepoint.enable
         | 
| 112 146 | 
             
              end
         | 
| 113 147 |  | 
| 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
         | 
| 152 | 
            +
             | 
| 114 153 | 
             
              # Every time a file changes the `test/` directory, reset this test
         | 
| 115 154 | 
             
              # TODO: add an option to auto-run on reload
         | 
| 116 155 | 
             
              def setup_file_listener
         | 
| 117 | 
            -
                # TODO | 
| 118 | 
            -
             | 
| 119 | 
            -
                #  | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
                 | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 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
         | 
| 130 175 | 
             
              end
         | 
| 131 176 |  | 
| 132 177 | 
             
              def print_steps
         | 
| 133 | 
            -
                puts "#{@steps.length} steps:"
         | 
| 134 | 
            -
                @steps. | 
| 135 | 
            -
                  puts " | 
| 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]}"
         | 
| 136 183 | 
             
                end
         | 
| 137 184 | 
             
                puts
         | 
| 138 185 | 
             
              end
         | 
| @@ -190,11 +237,39 @@ class Cyperful::Driver | |
| 190 237 | 
             
                @ui_server.notify(steps_updated_data)
         | 
| 191 238 | 
             
              end
         | 
| 192 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
         | 
| 193 262 | 
             
              private def finish_current_step(error = nil)
         | 
| 194 263 | 
             
                if @current_step
         | 
| 195 264 | 
             
                  @current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
         | 
| 196 265 | 
             
                  @current_step[:status] = !error ? "passed" : "failed"
         | 
| 197 266 |  | 
| 267 | 
            +
                  puts(
         | 
| 268 | 
            +
                    " (#{@current_step[:end_at] - @current_step[:start_at]}ms)#{
         | 
| 269 | 
            +
                      error ? " FAILED" : ""
         | 
| 270 | 
            +
                    }",
         | 
| 271 | 
            +
                  )
         | 
| 272 | 
            +
             | 
| 198 273 | 
             
                  # take screenshot after the step has finished
         | 
| 199 274 | 
             
                  # path = File.join(SCREENSHOTS_DIR, "#{@current_step[:index]}.png")
         | 
| 200 275 |  | 
| @@ -210,25 +285,6 @@ class Cyperful::Driver | |
| 210 285 | 
             
                notify_updated_steps
         | 
| 211 286 | 
             
              end
         | 
| 212 287 |  | 
| 213 | 
            -
              def pause_on_step(step)
         | 
| 214 | 
            -
                @current_step = step
         | 
| 215 | 
            -
             | 
| 216 | 
            -
                puts "STEP: #{step[:as_string]}"
         | 
| 217 | 
            -
             | 
| 218 | 
            -
                if @pause_at_step == true || @pause_at_step == step[:index]
         | 
| 219 | 
            -
                  @current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
         | 
| 220 | 
            -
                  @current_step[:status] = "paused"
         | 
| 221 | 
            -
                  notify_updated_steps
         | 
| 222 | 
            -
             | 
| 223 | 
            -
                  # async wait for `continue_next_step`
         | 
| 224 | 
            -
                  step_pausing_dequeue
         | 
| 225 | 
            -
                end
         | 
| 226 | 
            -
             | 
| 227 | 
            -
                @current_step[:status] = "running"
         | 
| 228 | 
            -
                @current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
         | 
| 229 | 
            -
                notify_updated_steps
         | 
| 230 | 
            -
              end
         | 
| 231 | 
            -
             | 
| 232 288 | 
             
              private def continue_next_step
         | 
| 233 289 | 
             
                @step_pausing_queue.enq(:next)
         | 
| 234 290 | 
             
              end
         | 
| @@ -283,7 +339,15 @@ class Cyperful::Driver | |
| 283 339 | 
             
                [abs_url, display_url]
         | 
| 284 340 | 
             
              end
         | 
| 285 341 |  | 
| 286 | 
            -
               | 
| 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
         | 
| 287 351 |  | 
| 288 352 | 
             
              private def skip_multi_sessions
         | 
| 289 353 | 
             
                unless Capybara.current_session == @session
         | 
| @@ -307,10 +371,10 @@ class Cyperful::Driver | |
| 307 371 |  | 
| 308 372 | 
             
                @session.execute_script("window.location.href = #{abs_url.to_json}")
         | 
| 309 373 |  | 
| 310 | 
            -
                # inject the  | 
| 374 | 
            +
                # inject the frame-agent script into the page being tested.
         | 
| 311 375 | 
             
                # this script will notify the Cyperful UI for events like:
         | 
| 312 376 | 
             
                # console logs, network requests, client navigations, errors, etc.
         | 
| 313 | 
            -
                @session.execute_script( | 
| 377 | 
            +
                @session.execute_script(Cyperful::Driver.load_frame_agent_js) # ~9ms empirically
         | 
| 314 378 |  | 
| 315 379 | 
             
                true
         | 
| 316 380 | 
             
              end
         | 
| @@ -323,7 +387,7 @@ class Cyperful::Driver | |
| 323 387 | 
             
              end
         | 
| 324 388 |  | 
| 325 389 | 
             
              def setup_api_server
         | 
| 326 | 
            -
                @ui_server = Cyperful::UiServer.new(port:  | 
| 390 | 
            +
                @ui_server = Cyperful::UiServer.new(port: Cyperful.config.port)
         | 
| 327 391 |  | 
| 328 392 | 
             
                @cyperful_origin = @ui_server.url_origin
         | 
| 329 393 |  | 
| @@ -363,7 +427,7 @@ class Cyperful::Driver | |
| 363 427 | 
             
                @tracepoint = nil
         | 
| 364 428 |  | 
| 365 429 | 
             
                if error&.is_a?(Cyperful::ResetCommand)
         | 
| 366 | 
            -
                  puts "\ | 
| 430 | 
            +
                  puts "\nResetting test (ignore any error logs)..."
         | 
| 367 431 |  | 
| 368 432 | 
             
                  @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
         | 
| 369 433 |  | 
| @@ -395,6 +459,7 @@ class Cyperful::Driver | |
| 395 459 | 
             
                @ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
         | 
| 396 460 |  | 
| 397 461 | 
             
                puts "Cyperful teardown complete. Waiting for command..."
         | 
| 462 | 
            +
                # NOTE: this will raise an `Interrupt` if the user Ctrl+C's here
         | 
| 398 463 | 
             
                command = @step_pausing_queue.deq
         | 
| 399 464 | 
             
                queue_reset if command == :reset
         | 
| 400 465 | 
             
              ensure
         | 
| @@ -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
         | 
| @@ -51,13 +53,48 @@ module Cyperful::SystemTestHelper | |
| 51 53 | 
             
                Cyperful.teardown(error)
         | 
| 52 54 | 
             
                super
         | 
| 53 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
         | 
| 62 | 
            +
            end
         | 
| 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
         | 
| 54 86 | 
             
            end
         | 
| 87 | 
            +
            ActionDispatch::SystemTesting::Driver.prepend(PrependSystemTestingDriver)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
            # if defined?(Minitest::Test)
         | 
| 90 | 
            +
            #   Minitest::Test::PASSTHROUGH_EXCEPTIONS << Cyperful::AbstractCommand
         | 
| 91 | 
            +
            # end
         | 
| 55 92 |  | 
| 56 93 | 
             
            # we need to allow the iframe to be embedded in the cyperful server
         | 
| 57 94 | 
             
            # TODO: use Rack middleware instead to support non-Rails apps
         | 
| 58 95 | 
             
            if defined?(Rails)
         | 
| 59 96 | 
             
              Rails.application.config.content_security_policy do |policy|
         | 
| 60 | 
            -
                policy.frame_ancestors(:self, "localhost | 
| 97 | 
            +
                policy.frame_ancestors(:self, "localhost:#{Cyperful.config.port}")
         | 
| 61 98 | 
             
              end
         | 
| 62 99 | 
             
            else
         | 
| 63 100 | 
             
              warn "Cyperful: Rails not detected, skipping content_security_policy fix.\nThe Cyperful UI may not work correctly."
         | 
    
        data/lib/cyperful/test_parser.rb
    CHANGED
    
    | @@ -74,7 +74,7 @@ class Cyperful::TestParser | |
| 74 74 | 
             
                      test_string = node.children[0].children[2].children[0]
         | 
| 75 75 |  | 
| 76 76 | 
             
                      # https://github.com/rails/rails/blob/66676ce499a32e4c62220bd05f8ee2cdf0e15f0c/activesupport/lib/active_support/testing/declarative.rb#L14C23-L14C61
         | 
| 77 | 
            -
                      test_method = "test_#{test_string.gsub(/\s+/, "_")}" | 
| 77 | 
            +
                      test_method = :"test_#{test_string.gsub(/\s+/, "_")}"
         | 
| 78 78 |  | 
| 79 79 | 
             
                      block_node = node.children[2]
         | 
| 80 80 | 
             
                      [test_method, block_node]
         | 
    
        data/lib/cyperful/ui_server.rb
    CHANGED
    
    | @@ -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
         | 
| @@ -34,13 +55,7 @@ module Cyperful | |
| 34 55 | 
             
              end
         | 
| 35 56 | 
             
            end
         | 
| 36 57 |  | 
| 37 | 
            -
             | 
| 38 | 
            -
            end
         | 
| 39 | 
            -
            class Cyperful::ResetCommand < Cyperful::AbstractCommand
         | 
| 40 | 
            -
            end
         | 
| 41 | 
            -
            class Cyperful::ExitCommand < Cyperful::AbstractCommand
         | 
| 42 | 
            -
            end
         | 
| 43 | 
            -
             | 
| 58 | 
            +
            require "cyperful/commands"
         | 
| 44 59 | 
             
            require "cyperful/test_parser"
         | 
| 45 60 | 
             
            require "cyperful/ui_server"
         | 
| 46 61 | 
             
            require "cyperful/driver"
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.static{position:static}.absolute{position:absolute}.relative{position:relative}.bottom-1{bottom:.25rem}.left-0{left:0}.left-1{left:.25rem}.right-0{right:0}.right-1{right:.25rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.m-4{margin:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.ml-1{margin-left:.25rem}.mr-2{margin-right:.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-16{max-height:4rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.basis-96{flex-basis:24rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity))}.border-orange-500{--tw-border-opacity: 1;border-color:rgb(249 115 22 / var(--tw-border-opacity))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity))}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}.border-yellow-600{--tw-border-opacity: 1;border-color:rgb(202 138 4 / var(--tw-border-opacity))}.bg-\[\#121b2e\]{--tw-bg-opacity: 1;background-color:rgb(18 27 46 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-orange-400{--tw-bg-opacity: 1;background-color:rgb(251 146 60 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.pt-0{padding-top:0}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[1\.25em\]{font-size:1.25em}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-green-100{--tw-text-opacity: 1;color:rgb(220 252 231 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}.opacity-50{opacity:.5}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.Logo g{transform-origin:50% 50%;animation:rotate 5s linear infinite;animation-play-state:paused}.Logo.Logo--animating g{animation-play-state:running}.Logo g:nth-child(1){animation-duration:1s}.Logo g:nth-child(2){animation-duration:1.5s}.Logo g:nth-child(3){animation-duration:3s}.Logo g:nth-child(4){animation-duration:4s}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))}.hover\:bg-orange-500:hover{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-yellow-600:hover{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity))}.hover\:underline:hover{text-decoration-line:underline}.group:hover .group-hover\:block{display:block}
         |