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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61dce63e0bff592ded11835cf202b241d717acaaf99331db8f447d3d7dc2aba9
4
- data.tar.gz: a2ec8532880c523e33ce15d377a1fc28b6832354306c784fa3dea7a38200302b
3
+ metadata.gz: 83254b2335dc0d99b443c2aad2260adc7c4f56bebaf32847a9e4ce151c03c6bc
4
+ data.tar.gz: 5a444d60e56c014569604e91162943aed5d6cb4f21655aaff8405be62d67b622
5
5
  SHA512:
6
- metadata.gz: 0cd35d0e7f3ee879367682ef2780fd6904d879a2d0a6c95a936191c3054b9cbfe60f682d4b911804565050b4da8a75744f5f8f58ef41d8fe82c02e1de84278de
7
- data.tar.gz: 5755db30366159e25279fde63c4e2ccc2f94c934dee4538b237b8e3aee05774ff0182ee8e006d21aaf525ed0b8bdb75675a2fdbeb2879d0ab8fe66d92ecbd4ff
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
@@ -18,7 +18,7 @@ class Cyperful::Driver
18
18
 
19
19
  @source_filepath =
20
20
  Object.const_source_location(test_class.name).first ||
21
- (raise "Could not find source file for #{test_class.name}")
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 is not empty during setup"
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
- # FIXME: there may be other tests that are "queued" to run `at_exit`,
93
- # so they'll run before this test restarts.
94
- at_exit { Minitest.run_one_method(@test_class, @test_name) }
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: we need to somehow reload the source files
118
-
119
- # test_dir = @source_filepath.match(%r{^/.+/(?:test|spec)\b})[0]
120
-
121
- # @file_listener&.stop
122
- # @file_listener =
123
- # Listen.to(test_dir) do |_modified, _added, _removed|
124
- # puts "Test files changed, resetting test..."
125
-
126
- # @pause_at_step = true
127
- # @step_pausing_queue.enq(:reset)
128
- # end
129
- # @file_listener.start
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.each do |step|
135
- puts " #{step[:method]}: #{step[:line]}:#{step[:column]}"
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
- WATCHER_JS = File.read(File.join(Cyperful::ROOT_DIR, "watcher.js"))
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 watcher script into the page being tested.
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(WATCHER_JS) # ~9ms empirically
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: 3004)
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 "\nPlease ignore the error, we're just resetting the test ;)"
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:3004")
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."
@@ -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+/, "_")}".to_sym
77
+ test_method = :"test_#{test_string.gsub(/\s+/, "_")}"
78
78
 
79
79
  block_node = node.children[2]
80
80
  [test_method, block_node]
@@ -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
- class Cyperful::AbstractCommand < StandardError
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}