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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/lib/vizzly.rb +135 -10
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0011bf796b1114bd00011a649d41cb860d4658cc4d98164f53d4e2308404d6c2
4
- data.tar.gz: 14551857950efde73e174cb04d46be7192724262978f4a009f99bde06da9daea
3
+ metadata.gz: 00b928505560c41d2c1402ccb75ce47a3814cfaedf3d15094b5685d21a5ae2fb
4
+ data.tar.gz: dafb6128f52bc6e76b9cba415ef1e8823615758048d58613fb1688e337ab8c15
5
5
  SHA512:
6
- metadata.gz: c66401d88b74442243b1f01c44a479d58844c0448c3b954bedcd823ea777a96408d87ce98e6b02b8a3812751a4381ff0073a448d469c2156a45169a17154d104
7
- data.tar.gz: 3c30f1d6e5cd13ce62a2a779fce10410f8b0f525e29fa92ecfe10cf520c7ac4db33ff9defb241967ee21c0fff94a8293cae6aee009b274ba5dadfc1437baaa90
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 [Integer] :threshold Pixel difference threshold (0-100)
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: ENV.fetch('VIZZLY_BUILD_ID', nil),
60
- threshold: options[:threshold] || 0,
61
- fullPage: options[:full_page] || false,
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(uri.host, uri.port, read_timeout: 30) do |http|
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
- disabled: disabled?
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.2.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-07 00:00:00.000000000 Z
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.3.27
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