vizzly 0.1.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 (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +118 -0
  4. data/lib/vizzly.rb +257 -0
  5. metadata +51 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 83c5bb29a091449d684562effec3161f0ddab82fe2be22b3d357cb66723d44b9
4
+ data.tar.gz: 1a97c02f6b0d670a6841d9536ee7fe434340b5ca210bf315f764a03e350e360c
5
+ SHA512:
6
+ metadata.gz: 64a47f4a33270a9a82c4c1edbe267e4ef2eedd7f0f096859f28a306aa53d90401ffb3440cc5f7191a2138e2a6ac732dc1214b4d029c911e3d7d705f30a692056
7
+ data.tar.gz: f16bd4924bcfc9a2ddadbe0d639f01267853a1d2107dab97eb65e2d7ff1f01d63a21bd8db82a76cb4d1c281bacde9bc3fae38d194733dfb26f1f465a98ff68b3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stubborn Mule Software
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Vizzly Ruby Client
2
+
3
+ A lightweight Ruby client SDK for capturing screenshots and sending them to Vizzly for visual
4
+ regression testing.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'vizzly'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```bash
17
+ bundle install
18
+ ```
19
+
20
+ Or install it yourself as:
21
+
22
+ ```bash
23
+ gem install vizzly
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Basic Usage
29
+
30
+ ```ruby
31
+ require 'vizzly'
32
+
33
+ # Take a screenshot
34
+ image_data = File.binread('screenshot.png')
35
+ Vizzly.screenshot('homepage', image_data)
36
+ ```
37
+
38
+ ### With Options
39
+
40
+ ```ruby
41
+ Vizzly.screenshot('checkout-page', image_data,
42
+ properties: {
43
+ browser: 'chrome',
44
+ viewport: { width: 1920, height: 1080 }
45
+ },
46
+ threshold: 5
47
+ )
48
+ ```
49
+
50
+ ### Using a Client Instance
51
+
52
+ ```ruby
53
+ client = Vizzly::Client.new
54
+ client.screenshot('login-form', image_data)
55
+
56
+ # Check if client is ready
57
+ puts "Ready: #{client.ready?}"
58
+
59
+ # Get client info
60
+ puts client.info
61
+ ```
62
+
63
+ ### Integration with Test Frameworks
64
+
65
+ #### RSpec + Capybara
66
+
67
+ ```ruby
68
+ RSpec.describe 'Homepage', type: :feature do
69
+ it 'displays the homepage correctly' do
70
+ visit '/'
71
+ expect(page).to have_content('Welcome')
72
+
73
+ # Take a screenshot for visual regression testing
74
+ image_data = page.driver.browser.screenshot_as(:png)
75
+ Vizzly.screenshot('homepage', image_data)
76
+ end
77
+
78
+ it 'displays the checkout page' do
79
+ visit '/checkout'
80
+ fill_in 'Email', with: 'test@example.com'
81
+
82
+ # Take a screenshot with properties
83
+ image_data = page.driver.browser.screenshot_as(:png)
84
+ Vizzly.screenshot('checkout-form', image_data,
85
+ properties: {
86
+ browser: 'chrome',
87
+ viewport: { width: 1920, height: 1080 }
88
+ }
89
+ )
90
+ end
91
+ end
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ The client automatically discovers a running Vizzly TDD server by looking for `.vizzly/server.json`
97
+ in the current and parent directories.
98
+
99
+ You can also configure via environment variables:
100
+
101
+ - `VIZZLY_SERVER_URL` - Server URL (e.g., `http://localhost:47392`)
102
+ - `VIZZLY_BUILD_ID` - Build identifier for grouping screenshots
103
+
104
+ ## Development
105
+
106
+ After checking out the repo, run tests:
107
+
108
+ ```bash
109
+ ruby test/vizzly_test.rb
110
+ ```
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/vizzly-testing/cli.
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the MIT License.
data/lib/vizzly.rb ADDED
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'base64'
6
+
7
+ # Vizzly visual regression testing client
8
+ module Vizzly
9
+ class Error < StandardError; end
10
+
11
+ # Default port for local TDD server
12
+ DEFAULT_TDD_PORT = 47392
13
+
14
+ class Client
15
+ attr_reader :server_url, :disabled
16
+
17
+ def initialize(server_url: nil)
18
+ @server_url = server_url || discover_server_url
19
+ @disabled = false
20
+ @warned = false
21
+ end
22
+
23
+ # Take a screenshot for visual regression testing
24
+ #
25
+ # @param name [String] Unique name for the screenshot
26
+ # @param image_data [String] PNG image data as binary string
27
+ # @param options [Hash] Optional configuration
28
+ # @option options [Hash] :properties Additional properties to attach
29
+ # @option options [Integer] :threshold Pixel difference threshold (0-100)
30
+ # @option options [Boolean] :full_page Whether this is a full page screenshot
31
+ #
32
+ # @return [Hash, nil] Response data or nil if disabled/failed
33
+ #
34
+ # @example
35
+ # client = Vizzly::Client.new
36
+ # image_data = File.binread('screenshot.png')
37
+ # client.screenshot('homepage', image_data)
38
+ #
39
+ # @example With options
40
+ # client.screenshot('checkout', image_data,
41
+ # properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } },
42
+ # threshold: 5
43
+ # )
44
+ def screenshot(name, image_data, options = {})
45
+ return nil if disabled?
46
+
47
+ unless @server_url
48
+ warn_once('Vizzly client not initialized. Screenshots will be skipped.')
49
+ disable!
50
+ return nil
51
+ end
52
+
53
+ image_base64 = Base64.strict_encode64(image_data)
54
+
55
+ payload = {
56
+ name: name,
57
+ image: image_base64,
58
+ buildId: ENV.fetch('VIZZLY_BUILD_ID', nil),
59
+ threshold: options[:threshold] || 0,
60
+ fullPage: options[:full_page] || false,
61
+ properties: options[:properties] || {}
62
+ }.compact
63
+
64
+ uri = URI("#{@server_url}/screenshot")
65
+
66
+ begin
67
+ response = Net::HTTP.start(uri.host, uri.port, read_timeout: 30) do |http|
68
+ request = Net::HTTP::Post.new(uri)
69
+ request['Content-Type'] = 'application/json'
70
+ request.body = JSON.generate(payload)
71
+ http.request(request)
72
+ end
73
+
74
+ unless response.is_a?(Net::HTTPSuccess)
75
+ error_data = begin
76
+ JSON.parse(response.body)
77
+ rescue JSON::ParserError, StandardError
78
+ {}
79
+ end
80
+
81
+ # In TDD mode with visual differences, log but don't raise
82
+ if response.code == '422' && error_data['tddMode'] && error_data['comparison']
83
+ comp = error_data['comparison']
84
+ diff_percent = comp['diffPercentage']&.round(2) || 0.0
85
+
86
+ # Extract port from server_url
87
+ port = begin
88
+ @server_url.match(/:(\d+)/)[1]
89
+ rescue StandardError
90
+ DEFAULT_TDD_PORT.to_s
91
+ end
92
+ dashboard_url = "http://localhost:#{port}/dashboard"
93
+
94
+ warn "⚠️ Visual diff: #{comp['name']} (#{diff_percent}%) → #{dashboard_url}"
95
+
96
+ return {
97
+ success: true,
98
+ status: 'failed',
99
+ name: comp['name'],
100
+ diffPercentage: comp['diffPercentage']
101
+ }
102
+ end
103
+
104
+ raise Error,
105
+ "Screenshot failed: #{response.code} #{response.message} - #{error_data['error'] || 'Unknown error'}"
106
+ end
107
+
108
+ JSON.parse(response.body)
109
+ rescue Error => e
110
+ # Re-raise Vizzly errors (like visual diffs)
111
+ raise if e.message.include?('Visual diff')
112
+
113
+ warn "Vizzly screenshot failed for #{name}: #{e.message}"
114
+
115
+ if e.message.include?('Connection refused') || e.is_a?(Errno::ECONNREFUSED)
116
+ warn "Server URL: #{@server_url}/screenshot"
117
+ warn 'This usually means the Vizzly server is not running or not accessible'
118
+ warn 'Check that the server is started and the port is correct'
119
+ elsif e.message.include?('404') || e.message.include?('Not Found')
120
+ warn "Server URL: #{@server_url}/screenshot"
121
+ warn 'The screenshot endpoint was not found - check server configuration'
122
+ end
123
+
124
+ # Disable the SDK after first failure to prevent spam
125
+ disable!('failure')
126
+
127
+ nil
128
+ rescue StandardError => e
129
+ warn "Vizzly screenshot failed for #{name}: #{e.message}"
130
+ disable!('failure')
131
+ nil
132
+ end
133
+ end
134
+
135
+ # Wait for all queued screenshots to be processed
136
+ # (Simple client doesn't need explicit flushing)
137
+ #
138
+ # @return [true]
139
+ def flush
140
+ true
141
+ end
142
+
143
+ # Check if the client is ready to capture screenshots
144
+ #
145
+ # @return [Boolean]
146
+ def ready?
147
+ !disabled? && !@server_url.nil?
148
+ end
149
+
150
+ # Disable screenshot capture
151
+ #
152
+ # @param reason [String] Optional reason for disabling
153
+ def disable!(reason = 'disabled')
154
+ @disabled = true
155
+ return if reason == 'disabled'
156
+
157
+ warn "Vizzly SDK disabled due to #{reason}. Screenshots will be skipped for the remainder of this session."
158
+ end
159
+
160
+ # Check if screenshot capture is disabled
161
+ #
162
+ # @return [Boolean]
163
+ def disabled?
164
+ @disabled
165
+ end
166
+
167
+ # Get client information
168
+ #
169
+ # @return [Hash] Client state information
170
+ def info
171
+ {
172
+ enabled: !disabled?,
173
+ server_url: @server_url,
174
+ ready: ready?,
175
+ build_id: ENV.fetch('VIZZLY_BUILD_ID', nil),
176
+ disabled: disabled?
177
+ }
178
+ end
179
+
180
+ private
181
+
182
+ def warn_once(message)
183
+ return if @warned
184
+
185
+ warn message
186
+ @warned = true
187
+ end
188
+
189
+ # Discover Vizzly server URL from environment or auto-discovery
190
+ def discover_server_url
191
+ # First check environment variable
192
+ return ENV['VIZZLY_SERVER_URL'] if ENV['VIZZLY_SERVER_URL']
193
+
194
+ # Then try auto-discovery
195
+ auto_discover_tdd_server
196
+ end
197
+
198
+ # Auto-discover local TDD server by checking for server.json
199
+ def auto_discover_tdd_server
200
+ dir = Dir.pwd
201
+ root = File.expand_path('/')
202
+
203
+ until dir == root
204
+ server_json_path = File.join(dir, '.vizzly', 'server.json')
205
+
206
+ if File.exist?(server_json_path)
207
+ begin
208
+ server_info = JSON.parse(File.read(server_json_path))
209
+ port = server_info['port'] || DEFAULT_TDD_PORT
210
+ return "http://localhost:#{port}"
211
+ rescue JSON::ParserError, Errno::ENOENT
212
+ # Invalid JSON or file disappeared, continue searching
213
+ end
214
+ end
215
+
216
+ dir = File.dirname(dir)
217
+ end
218
+
219
+ nil
220
+ end
221
+ end
222
+
223
+ class << self
224
+ # Get or create the shared client instance
225
+ #
226
+ # @return [Client]
227
+ def client
228
+ @client ||= Client.new
229
+ end
230
+
231
+ # Take a screenshot using the shared client
232
+ #
233
+ # @see Client#screenshot
234
+ def screenshot(name, image_data, options = {})
235
+ client.screenshot(name, image_data, options)
236
+ end
237
+
238
+ # Flush the shared client
239
+ #
240
+ # @see Client#flush
241
+ def flush
242
+ client.flush
243
+ end
244
+
245
+ # Check if the shared client is ready
246
+ #
247
+ # @see Client#ready?
248
+ def ready?
249
+ client.ready?
250
+ end
251
+
252
+ # Reset the shared client (useful for testing)
253
+ def reset!
254
+ @client = nil
255
+ end
256
+ end
257
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vizzly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vizzly
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A lightweight client SDK for capturing screenshots and sending them to
14
+ Vizzly for visual regression testing
15
+ email:
16
+ - support@vizzly.dev
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - lib/vizzly.rb
24
+ homepage: https://github.com/vizzly-testing/cli
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://github.com/vizzly-testing/cli
29
+ source_code_uri: https://github.com/vizzly-testing/cli
30
+ changelog_uri: https://github.com/vizzly-testing/cli/blob/main/clients/ruby/CHANGELOG.md
31
+ rubygems_mfa_required: 'true'
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.3.27
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Vizzly visual regression testing client for Ruby
51
+ test_files: []