snapshot_ui 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed5906abe86dd34b16173cdbff8239e7f649ab4ace8adceabf250c90165b1879
4
- data.tar.gz: fce78cca8afc9baffd515257e1f5e7a564e27d803b6c2e3a84341441254d7ead
3
+ metadata.gz: 5d083a6167cbd33d814565a34c6da82c32f920bf1fef6faa8171e1cf4bae1f86
4
+ data.tar.gz: 15f7d8346e5f192e4b8901760b64a1d91362507bb7adf91cbaed37e30a257613
5
5
  SHA512:
6
- metadata.gz: 2c64f1972c0f1261fe57e09e8aad3e9e9ba174184dbc9ff82ed988012e27a8d2340818629189d90a41d3ea7f2d11725ff9ec546500b32a1c0adddf6d2ec083a7
7
- data.tar.gz: 791edbd08ad2eaffed422b850063dc8b3c5914a2214f8914f2079607c03a6c0b3cfa8b6608618e4d54750953886b633a9a7d26f3c975c8e06f5ced609f533b1f
6
+ metadata.gz: 07c26236a62e772945eb5165e2552d2a278493acb7b65d4429f445e98b2401b5ee39acfe842b2a5c9787220b5444ac3f0a1ecab1f7e8cb0a7f87e2f01726b2b2
7
+ data.tar.gz: 5fa30fbda712efff8c596acca55ec3811fee661a6684fa7d34aee7a30b2032255e14930c8288f90e5e5fe15d82424cef66848a4860d560241d9631e57837b94a
@@ -6,18 +6,19 @@ require "async/websocket/client"
6
6
  require "async/websocket/adapters/rack"
7
7
  require "listen"
8
8
  require "set"
9
+ require "uri"
9
10
 
10
- SNAPSHOT_DIRECTORY = ENV.fetch("SNAPSHOT_DIRECTORY")
11
- WEBSOCKET_ENDPOINT = "http://localhost:7070/cable"
11
+ load ENV.fetch("SNAPSHOT_UI_INITIALIZER_FILE")
12
12
 
13
13
  REFRESH_MESSAGE = {identifier: "{\"channel\":\"RefreshChannel\"}", message: "refresh"}.to_json
14
14
  CONFIRM_SUBSCRIPTION_MESSAGE = {identifier: "{\"channel\":\"RefreshChannel\"}", type: "confirm_subscription"}.to_json
15
- PING_MESSAGE = {type: "ping", message: Time.now.to_i.to_s}.to_json
16
15
 
17
16
  @connections = Set.new
18
17
 
18
+ live_websocket_uri = URI.parse(SnapshotUI.configuration.live_websocket_url)
19
+
19
20
  run lambda { |env|
20
- if env["REQUEST_PATH"] == "/cable"
21
+ if env["REQUEST_PATH"] == live_websocket_uri.path
21
22
  Async::WebSocket::Adapters::Rack.open(env, protocols: ["actioncable-v1-json"]) do |connection|
22
23
  @connections << connection
23
24
 
@@ -27,7 +28,7 @@ run lambda { |env|
27
28
 
28
29
  Async do |ping_task|
29
30
  loop do
30
- connection.write(PING_MESSAGE)
31
+ connection.write({type: "ping", message: Time.now.to_i.to_s}.to_json)
31
32
  connection.flush
32
33
  sleep(2)
33
34
  end
@@ -50,7 +51,7 @@ run lambda { |env|
50
51
  }
51
52
 
52
53
  Async do |client_task|
53
- endpoint = Async::HTTP::Endpoint.parse(WEBSOCKET_ENDPOINT)
54
+ endpoint = Async::HTTP::Endpoint.parse(SnapshotUI.configuration.live_websocket_url)
54
55
 
55
56
  Async::WebSocket::Client.connect(endpoint) do |connection|
56
57
  Async do |listener_task|
@@ -68,15 +69,16 @@ Async do |client_task|
68
69
  end
69
70
 
70
71
  def detect_snapshots_update(task)
71
- Pathname.new(SNAPSHOT_DIRECTORY).mkpath
72
- listener = Listen.to(SNAPSHOT_DIRECTORY) { |_modified, _added, _removed| broadcast_update }
72
+ Pathname.new(SnapshotUI.configuration.storage_directory).mkpath
73
+ listener = Listen.to(SnapshotUI.configuration.storage_directory) { |_modified, _added, _removed| broadcast_update }
73
74
 
74
75
  task.async do
75
76
  listener.start
76
77
  task.sleep
77
78
  end
78
79
 
79
- Console.info("Watching for snapshots updates in #{SNAPSHOT_DIRECTORY}...")
80
+ Console.info("Watching for snapshots updates in #{SnapshotUI.configuration.storage_directory}")
81
+ Console.info("Review snapshots on #{SnapshotUI.configuration.web_url}")
80
82
  end
81
83
 
82
84
  def broadcast_update
@@ -1,22 +1,25 @@
1
1
  require "bundler/setup"
2
2
  require "thor"
3
3
  require "pathname"
4
+ require "uri"
4
5
 
5
6
  module SnapshotUI
6
7
  class CLI < Thor
7
- WEBSOCKET_HOST = "localhost:7070"
8
-
9
- desc "watch SNAPSHOT_DIRECTORY", "Watches for snapshot changes in SNAPSHOT_DIRECTORY and broadcasts them on ws://#{WEBSOCKET_HOST}/cable."
10
- def watch(snapshot_directory)
11
- unless File.exist?(snapshot_directory)
12
- puts "The provided directory `#{snapshot_directory}` doesn't exist. Please double check the path."
8
+ desc "live SNAPSHOT_UI_INITIALIZER_FILE", "Run the command to enable live refreshing of UI snapshots."
9
+ def live(initializer_file)
10
+ unless File.exist?(initializer_file)
11
+ puts "The provided initializer file `#{initializer_file}` doesn't exist. Please double check the path."
13
12
  exit 1
14
13
  end
15
14
 
16
- websocket_host = "http://#{WEBSOCKET_HOST}"
15
+ load initializer_file
16
+
17
+ websocket_host_uri = URI.parse(SnapshotUI.configuration.live_websocket_url)
18
+ websocket_host_uri.path = ""
19
+
17
20
  config_path = Pathname.new(__dir__).join("cli", "watcher.ru").cleanpath.to_s
18
21
 
19
- exec "SNAPSHOT_DIRECTORY=#{snapshot_directory} bundle exec falcon serve --bind #{websocket_host} --count 1 --config #{config_path}"
22
+ exec "SNAPSHOT_UI_INITIALIZER_FILE=#{initializer_file} bundle exec falcon serve --bind #{websocket_host_uri} --count 1 --config #{config_path}"
20
23
  end
21
24
 
22
25
  def self.exit_on_failure?
@@ -5,12 +5,13 @@ require_relative "colorize"
5
5
  module SnapshotUI
6
6
  class Configuration
7
7
  attr_writer :storage_directory, :project_root_directory
8
- attr_accessor :web_url
8
+ attr_accessor :web_url, :live_websocket_url
9
9
 
10
- def initialize(project_root_directory:, storage_directory:, web_url:)
10
+ def initialize(project_root_directory:, storage_directory:, web_url:, live_websocket_url:)
11
11
  @project_root_directory = project_root_directory
12
12
  @storage_directory = storage_directory
13
13
  @web_url = web_url
14
+ @live_websocket_url = live_websocket_url
14
15
  end
15
16
 
16
17
  def storage_directory
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SnapshotUI
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -119,6 +119,33 @@ module SnapshotUI
119
119
  def refresh_controller
120
120
  'data-controller="refresh" data-action="refresh-connected@window->refresh#connected refresh-disconnected@window->refresh#disconnected turbo:before-render@window->refresh#display_status"'
121
121
  end
122
+
123
+ def snapshot_title(snapshot)
124
+ title = snapshot.context.name.sub("test_", "").gsub(/^\d{4}\s*/, "").tr("_", " ")
125
+ suffix =
126
+ if snapshot.context.take_snapshot_index > 0
127
+ " (##{snapshot.context.take_snapshot_index + 1} in the same test)"
128
+ end
129
+
130
+ "#{title}#{suffix}"
131
+ end
132
+
133
+ def test_group_title(test_group)
134
+ parts = test_group.split("::")
135
+ last_part = "<span class='last'>#{parts.last}</span>"
136
+ all = parts[0..-2] << last_part
137
+
138
+ all.join(" <span class='divider'>/</span> ")
139
+ end
140
+
141
+ def copy_icon_svg
142
+ <<~HTML
143
+ <svg class="copy icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
144
+ <!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
145
+ <path fill="currentColor" d="M384 336l-192 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l140.1 0L400 115.9 400 320c0 8.8-7.2 16-16 16zM192 384l192 0c35.3 0 64-28.7 64-64l0-204.1c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1L192 0c-35.3 0-64 28.7-64 64l0 256c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64L0 448c0 35.3 28.7 64 64 64l192 0c35.3 0 64-28.7 64-64l0-32-48 0 0 32c0 8.8-7.2 16-16 16L64 464c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l32 0 0-48-32 0z"/>
146
+ </svg>
147
+ HTML
148
+ end
122
149
  end
123
150
  end
124
151
  end
@@ -3,6 +3,8 @@ import "@hotwired/turbo"
3
3
  import "channels"
4
4
 
5
5
  import RefreshController from "controllers/refresh_controller"
6
+ import SourceLocationController from "controllers/source_location_controller"
6
7
 
7
8
  window.Stimulus = Application.start()
8
9
  Stimulus.register("refresh", RefreshController)
10
+ Stimulus.register("source-location", SourceLocationController)
@@ -0,0 +1,17 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ copy(event) {
5
+ event.preventDefault()
6
+
7
+ let currentTarget = event.currentTarget
8
+
9
+ navigator.clipboard.writeText(currentTarget.dataset.sourceLocation)
10
+
11
+ currentTarget.classList.toggle('animate');
12
+ setTimeout(()=> {
13
+ currentTarget.classList.toggle('animate')
14
+ },300)
15
+
16
+ }
17
+ }
@@ -28,6 +28,15 @@ body#snapshots_index h1 {
28
28
  body#snapshots_index h2 {
29
29
  margin: 30px 0 0;
30
30
  font-size: 24px;
31
+ color: #aaa;
32
+ }
33
+
34
+ body#snapshots_index h2 .last {
35
+ color: #666666;
36
+ }
37
+
38
+ body#snapshots_index h2 .divider {
39
+ color: #ccc;
31
40
  }
32
41
 
33
42
  body#snapshots_index p {
@@ -44,8 +53,14 @@ body#snapshots_index h1, h2, h3, h4, h5, h6 {
44
53
  color: #666666;
45
54
  }
46
55
 
47
- body#snapshots_index a { color: #000; text-decoration: none; }
48
- body#snapshots_index a:hover { text-decoration: underline; }
56
+ body#snapshots_index a {
57
+ color: #000;
58
+ text-decoration: none;
59
+ }
60
+
61
+ body#snapshots_index a:hover {
62
+ text-decoration: underline;
63
+ }
49
64
 
50
65
  body#snapshots_index code {
51
66
  background: rgba(175, 184, 193, 0.2);
@@ -101,6 +116,67 @@ body#not_found p {
101
116
  color: #666666;
102
117
  }
103
118
 
119
+ .copy_source_location {
120
+ all: unset;
121
+ cursor: pointer;
122
+ position: relative;
123
+ }
124
+
125
+ .copy_source_location .icon {
126
+ width: 0.9em;
127
+ height: 0.9em;
128
+ color: #aaa;
129
+ }
130
+
131
+ .copy_source_location:hover .icon {
132
+ color: #000;
133
+ }
134
+
135
+ .copy_source_location.animate .icon {
136
+ animation: enlarge-and-back-to-normal 0.3s;
137
+ }
138
+
139
+ @keyframes enlarge-and-back-to-normal {
140
+ 0%,
141
+ 100% {
142
+ transform: scale(1, 1);
143
+ }
144
+ 50% {
145
+ transform: scale(1.3, 1.3);
146
+ }
147
+ }
148
+
149
+ .copy_source_location:before {
150
+ content: attr(data-source-location);
151
+ position:absolute;
152
+ font-size: 90%;
153
+ top:-95%;
154
+ transform:translateX(-50%);
155
+ left:100%;
156
+ margin-top:-15px;
157
+ padding:3px 15px 1px;
158
+ border-radius:10px;
159
+ background:#eee;
160
+ color: #666;
161
+ text-align:center;
162
+ display:none;
163
+ }
164
+
165
+ .copy_source_location:after {
166
+ content: "";
167
+ position:absolute;
168
+ top:-11px;
169
+ transform:translateX(-50%);
170
+ margin-left:10px;
171
+ border:10px solid #eee;
172
+ border-color: #eee transparent transparent transparent;
173
+ display:none;
174
+ }
175
+
176
+ .copy_source_location:hover:before, .copy_source_location:hover:after {
177
+ display:block;
178
+ }
179
+
104
180
  /* Hotwired Turbo related */
105
181
  .turbo-progress-bar {
106
182
  height: 2px;
@@ -7,7 +7,7 @@
7
7
  <meta name="turbo-refresh-method" content="morph">
8
8
  <meta name="turbo-refresh-scroll" content="preserve">
9
9
  <link rel="stylesheet" href="<%= stylesheet_path("application.css") %>">
10
- <meta name="action-cable-url" content="ws://127.0.0.1:7070/cable" />
10
+ <meta name="action-cable-url" content="<%= SnapshotUI.configuration.live_websocket_url %>" />
11
11
  <script type="importmap" data-turbo-track="reload">
12
12
  {
13
13
  "imports": {
@@ -16,6 +16,7 @@
16
16
  "@hotwired/turbo": "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.4/+esm",
17
17
  "@rails/actioncable": "https://cdn.jsdelivr.net/npm/@rails/actioncable@7.1.3-4/+esm",
18
18
  "controllers/refresh_controller": "<%= javascript_path("controllers/refresh_controller.js") %>",
19
+ "controllers/source_location_controller": "<%= javascript_path("controllers/source_location_controller.js") %>",
19
20
  "channels/consumer": "<%= javascript_path("channels/consumer.js") %>",
20
21
  "channels": "<%= javascript_path("channels/index.js") %>",
21
22
  "channels/refresh_channel": "<%= javascript_path("channels/refresh_channel.js") %>"
@@ -44,15 +44,21 @@ bundle exec snapshot_ui watch SNAPSHOT_DIRECTORY</pre>
44
44
  <% end %>
45
45
 
46
46
  <% @grouped_by_test_class.each do |test_group, snapshots| %>
47
- <h2><%= test_group %></h2>
47
+
48
+ <h2><%= test_group_title(test_group) %></h2>
48
49
 
49
50
  <ul>
50
51
  <% snapshots.each do |snapshot| %>
51
52
  <li>
52
- <a href="<%= snapshot_path(snapshot.slug) %>">
53
- <%= snapshot.context.name %>
54
- <%= if snapshot.context.take_snapshot_index > 0 then "(##{(snapshot.context.take_snapshot_index + 1)} in the same test)" end %>
55
- </a>
53
+ <a href="<%= snapshot_path(snapshot.slug) %>"><%= snapshot_title(snapshot) %></a>
54
+ <button
55
+ class="copy_source_location"
56
+ data-controller="source-location"
57
+ data-action="click->source-location#copy"
58
+ data-source-location="<%= snapshot.context.source_location.join(":") %>"
59
+ >
60
+ <%= copy_icon_svg %>
61
+ </button>
56
62
  </li>
57
63
  <% end %>
58
64
  </ul>
data/lib/snapshot_ui.rb CHANGED
@@ -7,7 +7,8 @@ module SnapshotUI
7
7
  DEFAULT_CONFIGURATION = {
8
8
  project_root_directory: nil,
9
9
  storage_directory: nil,
10
- web_url: "http://localhost:3000/ui/snapshots"
10
+ web_url: "http://localhost:3000/ui/snapshots",
11
+ live_websocket_url: "http://localhost:49152/live"
11
12
  }.freeze
12
13
 
13
14
  def self.configure
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snapshot_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomaz Zlender
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-26 00:00:00.000000000 Z
11
+ date: 2024-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -110,6 +110,7 @@ files:
110
110
  - lib/snapshot_ui/web/assets/javascripts/channels/index.js
111
111
  - lib/snapshot_ui/web/assets/javascripts/channels/refresh_channel.js
112
112
  - lib/snapshot_ui/web/assets/javascripts/controllers/refresh_controller.js
113
+ - lib/snapshot_ui/web/assets/javascripts/controllers/source_location_controller.js
113
114
  - lib/snapshot_ui/web/assets/stylesheets/application.css
114
115
  - lib/snapshot_ui/web/views/layout.html.erb
115
116
  - lib/snapshot_ui/web/views/snapshots/index.html.erb