vizzly 0.2.0 → 0.3.0
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/README.md +24 -2
- data/lib/vizzly.rb +135 -10
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00b928505560c41d2c1402ccb75ce47a3814cfaedf3d15094b5685d21a5ae2fb
|
|
4
|
+
data.tar.gz: dafb6128f52bc6e76b9cba415ef1e8823615758048d58613fb1688e337ab8c15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f9afac44fa26d16dd705e82dbe6f38cc161004ebe97580f6c47d1d68f2f808a57f644008e718339f1695b9cc421206ca30ec58229ef7d8fb68d1b1bb1658d9f
|
|
7
|
+
data.tar.gz: d9af13050f7617fd2e0c94abc364a8b90ca890d92d3df7542fe18797dc4aea014a21dcb1d1e62f96e7ab8ac699441f5a8c8a84cfb5015f72c182749dd17800b0
|
data/README.md
CHANGED
|
@@ -43,20 +43,40 @@ Vizzly.screenshot('checkout-page', image_data,
|
|
|
43
43
|
browser: 'chrome',
|
|
44
44
|
viewport: { width: 1920, height: 1080 }
|
|
45
45
|
},
|
|
46
|
-
threshold: 5
|
|
46
|
+
threshold: 5,
|
|
47
|
+
min_cluster_size: 3,
|
|
48
|
+
full_page: true,
|
|
49
|
+
build_id: 'build_123',
|
|
50
|
+
request_timeout: 60_000
|
|
47
51
|
)
|
|
48
52
|
```
|
|
49
53
|
|
|
54
|
+
Ruby option names use snake_case. The client also accepts camelCase aliases for
|
|
55
|
+
parity with the JavaScript API: `minClusterSize`, `fullPage`, `buildId`, and
|
|
56
|
+
`requestTimeout`. `request_timeout` is measured in milliseconds, so
|
|
57
|
+
`60_000` means one minute.
|
|
58
|
+
|
|
50
59
|
### Using a Client Instance
|
|
51
60
|
|
|
52
61
|
```ruby
|
|
53
62
|
client = Vizzly::Client.new
|
|
54
63
|
client.screenshot('login-form', image_data)
|
|
55
64
|
|
|
65
|
+
# Override local TDD visual diff behavior for this client
|
|
66
|
+
strict_client = Vizzly::Client.new(fail_on_diff: true)
|
|
67
|
+
|
|
68
|
+
# Attach screenshots to a known build and tune request timeout in milliseconds
|
|
69
|
+
client.screenshot(
|
|
70
|
+
'checkout',
|
|
71
|
+
image_data,
|
|
72
|
+
build_id: 'build_123',
|
|
73
|
+
request_timeout: 60_000
|
|
74
|
+
)
|
|
75
|
+
|
|
56
76
|
# Check if client is ready
|
|
57
77
|
puts "Ready: #{client.ready?}"
|
|
58
78
|
|
|
59
|
-
# Get client info
|
|
79
|
+
# Get client info, including the effective fail_on_diff setting
|
|
60
80
|
puts client.info
|
|
61
81
|
```
|
|
62
82
|
|
|
@@ -100,6 +120,8 @@ You can also configure via environment variables:
|
|
|
100
120
|
|
|
101
121
|
- `VIZZLY_SERVER_URL` - Server URL (e.g., `http://localhost:47392`)
|
|
102
122
|
- `VIZZLY_BUILD_ID` - Build identifier for grouping screenshots
|
|
123
|
+
- `VIZZLY_FAIL_ON_DIFF` - Set to `true` or `1` to raise when a local TDD
|
|
124
|
+
comparison returns a visual diff
|
|
103
125
|
|
|
104
126
|
## Development
|
|
105
127
|
|
data/lib/vizzly.rb
CHANGED
|
@@ -11,12 +11,15 @@ module Vizzly
|
|
|
11
11
|
# Default port for local TDD server
|
|
12
12
|
DEFAULT_TDD_PORT = 47392
|
|
13
13
|
|
|
14
|
+
# rubocop:disable Metrics/ClassLength
|
|
14
15
|
class Client
|
|
15
16
|
attr_reader :server_url, :disabled
|
|
16
17
|
|
|
17
|
-
def initialize(server_url: nil)
|
|
18
|
+
def initialize(server_url: nil, fail_on_diff: nil)
|
|
19
|
+
@server_info = nil
|
|
20
|
+
@configured_fail_on_diff = fail_on_diff
|
|
18
21
|
@server_url = server_url || discover_server_url
|
|
19
|
-
@disabled = false
|
|
22
|
+
@disabled = ENV['VIZZLY_ENABLED'] == 'false'
|
|
20
23
|
@warned = false
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -26,8 +29,13 @@ module Vizzly
|
|
|
26
29
|
# @param image_data [String] PNG image data as binary string
|
|
27
30
|
# @param options [Hash] Optional configuration
|
|
28
31
|
# @option options [Hash] :properties Additional properties to attach
|
|
29
|
-
# @option options [
|
|
32
|
+
# @option options [Numeric] :threshold Delta E comparison threshold. When
|
|
33
|
+
# omitted, the server's configured threshold is used.
|
|
34
|
+
# @option options [Integer] :min_cluster_size Ignore connected diff
|
|
35
|
+
# clusters smaller than this size. When omitted, the server config is used.
|
|
30
36
|
# @option options [Boolean] :full_page Whether this is a full page screenshot
|
|
37
|
+
# @option options [String] :build_id Build ID for grouping screenshots
|
|
38
|
+
# @option options [Numeric] :request_timeout Request timeout in milliseconds
|
|
31
39
|
#
|
|
32
40
|
# @return [Hash, nil] Response data or nil if disabled/failed
|
|
33
41
|
#
|
|
@@ -41,6 +49,7 @@ module Vizzly
|
|
|
41
49
|
# properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } },
|
|
42
50
|
# threshold: 5
|
|
43
51
|
# )
|
|
52
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
44
53
|
def screenshot(name, image_data, options = {})
|
|
45
54
|
return nil if disabled?
|
|
46
55
|
|
|
@@ -51,21 +60,34 @@ module Vizzly
|
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
image_base64 = Base64.strict_encode64(image_data)
|
|
63
|
+
options = normalize_options(options)
|
|
64
|
+
normalized = normalize_screenshot_options(options)
|
|
65
|
+
|
|
66
|
+
normalized[:warnings].each { |warning| warn warning[:message] }
|
|
67
|
+
|
|
68
|
+
request_timeout = normalized[:request_timeout]
|
|
69
|
+
request_timeout_seconds = request_timeout ? request_timeout.to_f / 1000.0 : 30
|
|
70
|
+
build_id = normalized[:build_id] || ENV.fetch('VIZZLY_BUILD_ID', nil)
|
|
54
71
|
|
|
55
72
|
payload = {
|
|
56
73
|
name: name,
|
|
57
74
|
image: image_base64,
|
|
58
75
|
type: 'base64',
|
|
59
|
-
buildId:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
properties: options[:properties] || {}
|
|
76
|
+
buildId: build_id,
|
|
77
|
+
properties: normalized[:properties],
|
|
78
|
+
warnings: normalized[:warnings]
|
|
63
79
|
}.compact
|
|
64
80
|
|
|
65
81
|
uri = URI("#{@server_url}/screenshot")
|
|
66
82
|
|
|
67
83
|
begin
|
|
68
|
-
response = Net::HTTP.start(
|
|
84
|
+
response = Net::HTTP.start(
|
|
85
|
+
uri.host,
|
|
86
|
+
uri.port,
|
|
87
|
+
use_ssl: uri.scheme == 'https',
|
|
88
|
+
open_timeout: 10,
|
|
89
|
+
read_timeout: request_timeout_seconds
|
|
90
|
+
) do |http|
|
|
69
91
|
request = Net::HTTP::Post.new(uri)
|
|
70
92
|
request['Content-Type'] = 'application/json'
|
|
71
93
|
request.body = JSON.generate(payload)
|
|
@@ -84,6 +106,11 @@ module Vizzly
|
|
|
84
106
|
comp = error_data['comparison']
|
|
85
107
|
diff_percent = comp['diffPercentage']&.round(2) || 0.0
|
|
86
108
|
|
|
109
|
+
if fail_on_diff?
|
|
110
|
+
raise Error,
|
|
111
|
+
"Visual diff detected for \"#{comp['name'] || name}\" (#{comp['diffPercentage'] || 0}% difference)"
|
|
112
|
+
end
|
|
113
|
+
|
|
87
114
|
# Extract port from server_url
|
|
88
115
|
port = begin
|
|
89
116
|
@server_url.match(/:(\d+)/)[1]
|
|
@@ -106,7 +133,13 @@ module Vizzly
|
|
|
106
133
|
"Screenshot failed: #{response.code} #{response.message} - #{error_data['error'] || 'Unknown error'}"
|
|
107
134
|
end
|
|
108
135
|
|
|
109
|
-
JSON.parse(response.body)
|
|
136
|
+
body = JSON.parse(response.body)
|
|
137
|
+
if body['tddMode'] && %w[diff failed].include?(body['status']) && fail_on_diff?
|
|
138
|
+
raise Error,
|
|
139
|
+
"Visual diff detected for \"#{body['name'] || name}\" (#{body['diffPercentage'] || 0}% difference)"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
body
|
|
110
143
|
rescue Error => e
|
|
111
144
|
# Re-raise Vizzly errors (like visual diffs)
|
|
112
145
|
raise if e.message.include?('Visual diff')
|
|
@@ -125,6 +158,18 @@ module Vizzly
|
|
|
125
158
|
# Disable the SDK after first failure to prevent spam
|
|
126
159
|
disable!('failure')
|
|
127
160
|
|
|
161
|
+
nil
|
|
162
|
+
rescue Net::OpenTimeout
|
|
163
|
+
warn "Vizzly connection timed out for #{name}: couldn't connect within 10s"
|
|
164
|
+
warn "Server URL: #{@server_url}/screenshot"
|
|
165
|
+
warn 'This usually means the server is unreachable (firewall, network issue, or wrong host)'
|
|
166
|
+
disable!('failure')
|
|
167
|
+
nil
|
|
168
|
+
rescue Net::ReadTimeout
|
|
169
|
+
warn "Vizzly request timed out for #{name}: no response within 30s"
|
|
170
|
+
warn "Server URL: #{@server_url}/screenshot"
|
|
171
|
+
warn 'The server may be overloaded or processing is taking too long'
|
|
172
|
+
disable!('failure')
|
|
128
173
|
nil
|
|
129
174
|
rescue StandardError => e
|
|
130
175
|
warn "Vizzly screenshot failed for #{name}: #{e.message}"
|
|
@@ -132,6 +177,7 @@ module Vizzly
|
|
|
132
177
|
nil
|
|
133
178
|
end
|
|
134
179
|
end
|
|
180
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
135
181
|
|
|
136
182
|
# Wait for all queued screenshots to be processed
|
|
137
183
|
# (Simple client doesn't need explicit flushing)
|
|
@@ -172,14 +218,82 @@ module Vizzly
|
|
|
172
218
|
{
|
|
173
219
|
enabled: !disabled?,
|
|
174
220
|
server_url: @server_url,
|
|
221
|
+
serverUrl: @server_url,
|
|
175
222
|
ready: ready?,
|
|
176
223
|
build_id: ENV.fetch('VIZZLY_BUILD_ID', nil),
|
|
177
|
-
|
|
224
|
+
buildId: ENV.fetch('VIZZLY_BUILD_ID', nil),
|
|
225
|
+
disabled: disabled?,
|
|
226
|
+
fail_on_diff: fail_on_diff?,
|
|
227
|
+
failOnDiff: fail_on_diff?
|
|
178
228
|
}
|
|
179
229
|
end
|
|
180
230
|
|
|
181
231
|
private
|
|
182
232
|
|
|
233
|
+
def normalize_options(options)
|
|
234
|
+
options.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def option_value(options, *keys)
|
|
238
|
+
keys.each do |key|
|
|
239
|
+
return options[key] if options.key?(key)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def normalize_screenshot_options(options)
|
|
246
|
+
threshold = options[:threshold]
|
|
247
|
+
min_cluster_size = option_value(options, :min_cluster_size, :minClusterSize)
|
|
248
|
+
full_page = options.key?(:full_page) ? options[:full_page] : options[:fullPage]
|
|
249
|
+
build_id = option_value(options, :build_id, :buildId)
|
|
250
|
+
request_timeout = option_value(options, :request_timeout, :requestTimeout)
|
|
251
|
+
properties = {}
|
|
252
|
+
warnings = []
|
|
253
|
+
|
|
254
|
+
(options[:properties] || {}).each do |key, value|
|
|
255
|
+
option = key.to_s
|
|
256
|
+
case option
|
|
257
|
+
when 'threshold'
|
|
258
|
+
threshold = value if threshold.nil?
|
|
259
|
+
when 'min_cluster_size', 'minClusterSize'
|
|
260
|
+
min_cluster_size = value if min_cluster_size.nil?
|
|
261
|
+
when 'full_page', 'fullPage'
|
|
262
|
+
full_page = value if full_page.nil?
|
|
263
|
+
when 'build_id', 'buildId'
|
|
264
|
+
build_id = value if build_id.nil?
|
|
265
|
+
when 'request_timeout', 'requestTimeout'
|
|
266
|
+
request_timeout = value if request_timeout.nil?
|
|
267
|
+
else
|
|
268
|
+
properties[key] = value
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
warnings << reserved_property_warning(option)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
properties = properties.merge(
|
|
276
|
+
threshold: threshold,
|
|
277
|
+
minClusterSize: min_cluster_size,
|
|
278
|
+
fullPage: full_page
|
|
279
|
+
).compact
|
|
280
|
+
|
|
281
|
+
{
|
|
282
|
+
build_id: build_id,
|
|
283
|
+
request_timeout: request_timeout,
|
|
284
|
+
properties: properties,
|
|
285
|
+
warnings: warnings
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def reserved_property_warning(option)
|
|
290
|
+
{
|
|
291
|
+
code: 'reserved-property-option',
|
|
292
|
+
option: option,
|
|
293
|
+
message: "Move \"#{option}\" out of properties; properties is only for user metadata."
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
|
|
183
297
|
def warn_once(message)
|
|
184
298
|
return if @warned
|
|
185
299
|
|
|
@@ -196,6 +310,15 @@ module Vizzly
|
|
|
196
310
|
auto_discover_tdd_server
|
|
197
311
|
end
|
|
198
312
|
|
|
313
|
+
def fail_on_diff?
|
|
314
|
+
return @configured_fail_on_diff unless @configured_fail_on_diff.nil?
|
|
315
|
+
|
|
316
|
+
env_value = ENV.fetch('VIZZLY_FAIL_ON_DIFF', '').downcase
|
|
317
|
+
return true if %w[true 1].include?(env_value)
|
|
318
|
+
|
|
319
|
+
@server_info && @server_info['failOnDiff'] == true
|
|
320
|
+
end
|
|
321
|
+
|
|
199
322
|
# Auto-discover local TDD server by checking for server.json
|
|
200
323
|
def auto_discover_tdd_server
|
|
201
324
|
dir = Dir.pwd
|
|
@@ -207,6 +330,7 @@ module Vizzly
|
|
|
207
330
|
if File.exist?(server_json_path)
|
|
208
331
|
begin
|
|
209
332
|
server_info = JSON.parse(File.read(server_json_path))
|
|
333
|
+
@server_info = server_info
|
|
210
334
|
port = server_info['port'] || DEFAULT_TDD_PORT
|
|
211
335
|
return "http://localhost:#{port}"
|
|
212
336
|
rescue JSON::ParserError, Errno::ENOENT
|
|
@@ -220,6 +344,7 @@ module Vizzly
|
|
|
220
344
|
nil
|
|
221
345
|
end
|
|
222
346
|
end
|
|
347
|
+
# rubocop:enable Metrics/ClassLength
|
|
223
348
|
|
|
224
349
|
class << self
|
|
225
350
|
# Get or create the shared client instance
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vizzly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vizzly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01
|
|
11
|
+
date: 2026-06-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A lightweight client SDK for capturing screenshots and sending them to
|
|
14
14
|
Vizzly for visual regression testing
|
|
@@ -44,7 +44,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '0'
|
|
46
46
|
requirements: []
|
|
47
|
-
rubygems_version: 3.
|
|
47
|
+
rubygems_version: 3.5.22
|
|
48
48
|
signing_key:
|
|
49
49
|
specification_version: 4
|
|
50
50
|
summary: Vizzly visual regression testing client for Ruby
|