From c3821e3b3c5622db041ecc5d245613fe4bc44dbd Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 25 Jan 2026 15:20:51 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20Add=20open=5Ftimeout=20to=20?= =?UTF-8?q?Ruby=20SDK=20to=20prevent=20hanging=20connections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without open_timeout, connection attempts to unreachable servers (e.g., firewall dropping packets) could hang for 60+ seconds. Now fails fast with 10s open timeout + 30s read timeout. --- clients/ruby/lib/vizzly.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/ruby/lib/vizzly.rb b/clients/ruby/lib/vizzly.rb index 363f067..2234be8 100644 --- a/clients/ruby/lib/vizzly.rb +++ b/clients/ruby/lib/vizzly.rb @@ -65,7 +65,7 @@ def screenshot(name, image_data, options = {}) uri = URI("#{@server_url}/screenshot") begin - response = Net::HTTP.start(uri.host, uri.port, read_timeout: 30) do |http| + response = Net::HTTP.start(uri.host, uri.port, open_timeout: 10, read_timeout: 30) do |http| request = Net::HTTP::Post.new(uri) request['Content-Type'] = 'application/json' request.body = JSON.generate(payload) From 42c5c8c9e6d535e676f9b26956d3968efbe90422 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 25 Jan 2026 15:55:09 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20Add=20specific=20timeout=20erro?= =?UTF-8?q?r=20messages=20to=20Ruby=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate handling for Net::OpenTimeout and Net::ReadTimeout with actionable guidance for each scenario. --- clients/ruby/lib/vizzly.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clients/ruby/lib/vizzly.rb b/clients/ruby/lib/vizzly.rb index 2234be8..625cc98 100644 --- a/clients/ruby/lib/vizzly.rb +++ b/clients/ruby/lib/vizzly.rb @@ -125,6 +125,18 @@ def screenshot(name, image_data, options = {}) # Disable the SDK after first failure to prevent spam disable!('failure') + nil + rescue Net::OpenTimeout => e + warn "Vizzly connection timed out for #{name}: couldn't connect within 10s" + warn "Server URL: #{@server_url}/screenshot" + warn 'This usually means the server is unreachable (firewall, network issue, or wrong host)' + disable!('failure') + nil + rescue Net::ReadTimeout => e + warn "Vizzly request timed out for #{name}: no response within 30s" + warn "Server URL: #{@server_url}/screenshot" + warn 'The server may be overloaded or processing is taking too long' + disable!('failure') nil rescue StandardError => e warn "Vizzly screenshot failed for #{name}: #{e.message}" From 050982cd0e6d464db2134b49b95b1c55960a7997 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 25 Jan 2026 16:03:33 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A5=20Replace=20fake=20Ruby=20SDK?= =?UTF-8?q?=20tests=20with=20real=20E2E=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete integration_test.rb (used useless 1x1 red pixels) - Update sdk-e2e.yml to run e2e_test.rb with Selenium + Chrome - Add selenium-webdriver to Gemfile - Remove fake integration tests from sdk-unit.yml The e2e tests capture real screenshots from the test-site using Selenium WebDriver, which actually validates the visual testing flow. --- .github/workflows/sdk-e2e.yml | 15 +- .github/workflows/sdk-unit.yml | 8 +- clients/ruby/Gemfile | 1 + clients/ruby/test/integration_test.rb | 330 -------------------------- 4 files changed, 12 insertions(+), 342 deletions(-) delete mode 100644 clients/ruby/test/integration_test.rb diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index 3e30d96..6960189 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -301,7 +301,7 @@ jobs: ruby: name: Ruby SDK runs-on: ubuntu-latest - timeout-minutes: 8 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -323,21 +323,26 @@ jobs: with: ruby-version: '3.3' + - name: Set up Chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + - name: Install Ruby dependencies working-directory: ./clients/ruby run: | gem install bundler bundle install - - name: Run integration tests (TDD mode) + - name: Run E2E tests (TDD mode) working-directory: ./clients/ruby - run: ../../bin/vizzly.js tdd run "VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb" + run: ../../bin/vizzly.js tdd run "VIZZLY_E2E=1 ruby -Ilib:test test/e2e_test.rb" env: CI: true - - name: Run integration tests (Cloud mode) + - name: Run E2E tests (Cloud mode) working-directory: ./clients/ruby - run: ../../bin/vizzly.js run "VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb" + run: ../../bin/vizzly.js run "VIZZLY_E2E=1 ruby -Ilib:test test/e2e_test.rb" env: CI: true VIZZLY_TOKEN: ${{ secrets.VIZZLY_RUBY_CLIENT_TOKEN }} diff --git a/.github/workflows/sdk-unit.yml b/.github/workflows/sdk-unit.yml index 79e5b95..712682c 100644 --- a/.github/workflows/sdk-unit.yml +++ b/.github/workflows/sdk-unit.yml @@ -86,13 +86,7 @@ jobs: - name: Run Ruby unit tests working-directory: ./clients/ruby - run: ruby -I lib test/vizzly_test.rb - - - name: Run Ruby integration tests - working-directory: ./clients/ruby - run: VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb - env: - CI: true + run: ruby -Ilib:test test/vizzly_test.rb # Storybook SDK - runs on multiple Node versions storybook: diff --git a/clients/ruby/Gemfile b/clients/ruby/Gemfile index 4c7b569..d6c47ff 100644 --- a/clients/ruby/Gemfile +++ b/clients/ruby/Gemfile @@ -9,4 +9,5 @@ group :development, :test do gem 'minitest', '~> 5.0' gem 'rake', '~> 13.0' gem 'rubocop', '~> 1.60' + gem 'selenium-webdriver', '~> 4.0' end diff --git a/clients/ruby/test/integration_test.rb b/clients/ruby/test/integration_test.rb deleted file mode 100644 index 3ef6ac5..0000000 --- a/clients/ruby/test/integration_test.rb +++ /dev/null @@ -1,330 +0,0 @@ -# frozen_string_literal: true - -require 'minitest/autorun' -require 'json' -require 'fileutils' -require 'tmpdir' -require 'open3' -require_relative '../lib/vizzly' - -# Helper methods for integration test setup and server management -module IntegrationTestHelpers - def find_vizzly_cli - path = File.expand_path('../../../dist/cli.js', __dir__) - return nil unless File.exist?(path) - - path - end - - def cli_path - @cli_path ||= find_vizzly_cli - end - - def start_server - return if @external_server - - pid = spawn('node', cli_path, 'tdd', 'start', %i[out err] => File::NULL) - _pid, status = Process.wait2(pid) - raise 'Failed to execute vizzly tdd start' unless status.success? - - 30.times do - break if File.exist?('.vizzly/server.json') - - sleep 0.1 - end - - unless File.exist?('.vizzly/server.json') - error_log = File.join('.vizzly', 'daemon-error.log') - puts "Error log: #{File.read(error_log)}" if File.exist?(error_log) - raise 'Server failed to start' - end - - @server_pid = true - end - - def stop_server - return unless @server_pid - return if @external_server - - pid = spawn('node', cli_path, 'tdd', 'stop', %i[out err] => File::NULL) - Process.wait(pid) - @server_pid = nil - end - - # Check if running in cloud mode (vizzly run) vs TDD mode (vizzly tdd run) - # Cloud mode returns { success: true } without a status field - # TDD mode returns comparison results with status field - def cloud_mode? - # In cloud mode, VIZZLY_TOKEN is typically set - token = ENV.fetch('VIZZLY_TOKEN', nil) - token && !token.empty? - end - - # Assert that a screenshot result indicates success - # In TDD mode: expects 'new' or 'match' status - # In cloud mode: expects 'success' to be true - def assert_screenshot_success(result) - if cloud_mode? - assert result['success'], "Expected success to be true in cloud mode, got: #{result.inspect}" - else - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" - end - end - - # Create a minimal valid PNG (1x1 red pixel) - def create_test_png - [ - 137, 80, 78, 71, 13, 10, 26, 10, - 0, 0, 0, 13, 73, 72, 68, 82, - 0, 0, 0, 1, 0, 0, 0, 1, - 8, 2, 0, 0, 0, 144, 119, 83, 222, - 0, 0, 0, 12, 73, 68, 65, 84, - 8, 215, 99, 248, 207, 192, 0, 0, 3, 1, 1, 0, - 24, 221, 141, 176, - 0, 0, 0, 0, 73, 69, 78, 68, - 174, 66, 96, 130 - ].pack('C*') - end -end - -# Integration test that requires a running Vizzly server -# Run with: VIZZLY_INTEGRATION=1 ruby test/integration_test.rb -# -# When run via `vizzly tdd run`, VIZZLY_SERVER_URL is set and we use that server. -# When run standalone, we start our own server. -# -# These tests use a minimal PNG for fast execution. For browser-based tests -# with the shared test-site, see example/test_screenshot.rb -class IntegrationTest < Minitest::Test - include IntegrationTestHelpers - - def setup - skip 'Set VIZZLY_INTEGRATION=1 to run integration tests' unless ENV['VIZZLY_INTEGRATION'] - - @original_dir = Dir.pwd - Vizzly.reset! - - # Check if we're running under `vizzly tdd run` or `vizzly run` - @external_server = !ENV['VIZZLY_SERVER_URL'].nil? - - if @external_server - # Running under vizzly wrapper - server is already running - # Stay in current directory (where server.json exists) - @temp_dir = nil - else - # Running standalone - create temp dir and start our own server - @temp_dir = Dir.mktmpdir - Dir.chdir(@temp_dir) - skip 'Vizzly CLI not found' unless cli_path - end - end - - def teardown - stop_server if @server_pid - return unless @temp_dir - - Dir.chdir(@original_dir) - FileUtils.rm_rf(@temp_dir) - end - - # =========================================================================== - # Basic Screenshot Capture - # =========================================================================== - - def test_basic_screenshot - start_server - image_data = create_test_png - - result = Vizzly.screenshot('basic-screenshot', image_data) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - def test_screenshot_with_properties - start_server - image_data = create_test_png - - result = Vizzly.screenshot('screenshot-with-props', image_data, - properties: { - browser: 'chrome', - viewport: { width: 1920, height: 1080 }, - theme: 'light' - }) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - def test_screenshot_with_threshold - start_server - image_data = create_test_png - - result = Vizzly.screenshot('screenshot-threshold', image_data, threshold: 5) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - def test_screenshot_with_full_page - start_server - image_data = create_test_png - - result = Vizzly.screenshot('screenshot-fullpage', image_data, full_page: true) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - def test_screenshot_with_all_options - start_server - image_data = create_test_png - - result = Vizzly.screenshot('screenshot-all-options', image_data, - properties: { - browser: 'firefox', - viewport: { width: 1280, height: 720 }, - component: 'hero' - }, - threshold: 3, - full_page: false) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - # =========================================================================== - # Auto-Discovery - # =========================================================================== - - def test_auto_discovery_via_server_json - # Skip when running under vizzly wrapper (server.json is in different directory) - skip 'Skipped under vizzly tdd run (uses external server)' if @external_server - - start_server - - # Verify server.json was created - assert File.exist?('.vizzly/server.json'), 'server.json should be created' - - # Create new client (should auto-discover) - client = Vizzly::Client.new - assert client.ready?, 'Client should be ready after auto-discovery' - assert_match(/localhost:\d+/, client.server_url) - - image_data = create_test_png - result = client.screenshot('auto-discovered', image_data) - - assert result, 'Expected result to be non-nil' - assert_screenshot_success(result) - end - - # =========================================================================== - # Client Configuration - # =========================================================================== - - def test_explicit_server_url - # Skip when running under vizzly wrapper (server.json is in different directory) - skip 'Skipped under vizzly tdd run (uses external server)' if @external_server - - start_server - - # Read port from server.json - server_info = JSON.parse(File.read('.vizzly/server.json')) - port = server_info['port'] - - client = Vizzly::Client.new(server_url: "http://localhost:#{port}") - assert client.ready?, 'Client with explicit URL should be ready' - assert_equal "http://localhost:#{port}", client.server_url - - image_data = create_test_png - result = client.screenshot('explicit-url', image_data) - - assert result, 'Expected result to be non-nil' - end - - def test_client_info - start_server - - client = Vizzly::Client.new - info = client.info - - assert_equal true, info[:enabled] - assert_equal true, info[:ready] - assert_equal false, info[:disabled] - assert_match(/localhost:\d+/, info[:server_url]) - end - - def test_client_ready_state - start_server - - client = Vizzly::Client.new - assert client.ready?, 'Client should be ready with running server' - refute client.disabled?, 'Client should not be disabled' - end - - # =========================================================================== - # Multiple Screenshots - # =========================================================================== - - def test_multiple_screenshots_sequence - start_server - image_data = create_test_png - - # Capture multiple screenshots in sequence - result1 = Vizzly.screenshot('sequence-1', image_data, properties: { index: 1 }) - result2 = Vizzly.screenshot('sequence-2', image_data, properties: { index: 2 }) - result3 = Vizzly.screenshot('sequence-3', image_data, properties: { index: 3 }) - - assert result1, 'First screenshot should succeed' - assert result2, 'Second screenshot should succeed' - assert result3, 'Third screenshot should succeed' - end - - # =========================================================================== - # Singleton Client - # =========================================================================== - - def test_singleton_client - start_server - image_data = create_test_png - - # Use module-level methods (singleton) - assert Vizzly.ready?, 'Singleton client should be ready' - - result = Vizzly.screenshot('singleton-test', image_data) - assert result, 'Screenshot via singleton should succeed' - - Vizzly.flush # Should complete without error - end - - # =========================================================================== - # Edge Cases - # =========================================================================== - - def test_empty_properties - start_server - image_data = create_test_png - - result = Vizzly.screenshot('empty-props', image_data, properties: {}) - - assert result, 'Screenshot with empty properties should succeed' - end - - def test_zero_threshold - start_server - image_data = create_test_png - - result = Vizzly.screenshot('zero-threshold', image_data, threshold: 0) - - assert result, 'Screenshot with zero threshold should succeed' - end - - def test_special_characters_in_name - start_server - image_data = create_test_png - - result = Vizzly.screenshot('screenshot_with-special.chars', image_data) - - assert result, 'Screenshot with special characters in name should succeed' - end -end From 3104be98e92140ce8b2ac95864f0dcdab6ba297b Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 25 Jan 2026 16:06:56 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20Fix=20rubocop=20warnings=20a?= =?UTF-8?q?nd=20add=20webrick=20for=20Ruby=203.0+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused exception variables in timeout rescues - Add rubocop:disable for metrics on screenshot method - Add webrick gem (removed from stdlib in Ruby 3.0) --- clients/ruby/Gemfile | 1 + clients/ruby/lib/vizzly.rb | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/clients/ruby/Gemfile b/clients/ruby/Gemfile index d6c47ff..91b3d4e 100644 --- a/clients/ruby/Gemfile +++ b/clients/ruby/Gemfile @@ -10,4 +10,5 @@ group :development, :test do gem 'rake', '~> 13.0' gem 'rubocop', '~> 1.60' gem 'selenium-webdriver', '~> 4.0' + gem 'webrick', '~> 1.8' # Required for Ruby 3.0+ (removed from stdlib) end diff --git a/clients/ruby/lib/vizzly.rb b/clients/ruby/lib/vizzly.rb index 625cc98..dcf6501 100644 --- a/clients/ruby/lib/vizzly.rb +++ b/clients/ruby/lib/vizzly.rb @@ -41,6 +41,7 @@ def initialize(server_url: nil) # properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } }, # threshold: 5 # ) + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def screenshot(name, image_data, options = {}) return nil if disabled? @@ -126,13 +127,13 @@ def screenshot(name, image_data, options = {}) disable!('failure') nil - rescue Net::OpenTimeout => e + rescue Net::OpenTimeout warn "Vizzly connection timed out for #{name}: couldn't connect within 10s" warn "Server URL: #{@server_url}/screenshot" warn 'This usually means the server is unreachable (firewall, network issue, or wrong host)' disable!('failure') nil - rescue Net::ReadTimeout => e + rescue Net::ReadTimeout warn "Vizzly request timed out for #{name}: no response within 30s" warn "Server URL: #{@server_url}/screenshot" warn 'The server may be overloaded or processing is taking too long' @@ -144,6 +145,7 @@ def screenshot(name, image_data, options = {}) nil end end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # Wait for all queued screenshots to be processed # (Simple client doesn't need explicit flushing)