capybara-presenter 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 641d4e0c4f6f27c908eff20417912e7e98e8f610d95c0f448e0709fab2f93349
4
+ data.tar.gz: 8ed8c25c782cfdb7498a439a00ff88e3e0b402a395b54b38549b5bd600bf672f
5
+ SHA512:
6
+ metadata.gz: 6b08f1f0be9dc7ac77b0b7773fbe730ab77308314177dba62e38b6ac1edf88b4c38bb54d936d9c90e0135ece22bfc4beecc547566dd9c1d648936d0e84c654ad
7
+ data.tar.gz: ac9b3f4e0d5a026faad59fc7f2528f55343860ed26f140a8660838fd680600e3de462341c28ac01618435a2970ba2847c8f8a2b4c6a0e2459c0c387910183c47
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ SuggestExtensions: false
4
+ NewCops: enable
5
+
6
+ plugins:
7
+ - rubocop-minitest
8
+
9
+ # Exclude test directories from metrics
10
+ Metrics:
11
+ Exclude:
12
+ - 'test/**/*'
13
+ - 'examples/**/*'
14
+
15
+ # Disable problematic cops
16
+ Lint/MissingCopEnableDirective:
17
+ Enabled: false
18
+
19
+ Lint/DuplicateBranch:
20
+ Enabled: false
21
+
22
+ Naming/PredicateName:
23
+ Enabled: false
24
+
25
+ Gemspec/DevelopmentDependencies:
26
+ Enabled: false
27
+
28
+ # Allow multiple assertions in system tests
29
+ Minitest/MultipleAssertions:
30
+ Exclude:
31
+ - 'examples/**/*'
data/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Input sanitization for JavaScript injection prevention in notifications
12
+ - Environment variable validation with proper error handling and warnings
13
+ - Comprehensive development infrastructure (CI, RuboCop, pre-commit hooks)
14
+ - Support for presenter mode with visual notifications and delays
15
+ - Automatic delays between Capybara actions for better presentations
16
+ - Browser notifications for test milestones and progress
17
+ - Configurable notification positions (top, center, bottom)
18
+ - Sample application demonstrating gem functionality
19
+
20
+ ### Changed
21
+ - Renamed gem from `capybara-demo` to `capybara-presenter` for clarity
22
+ - Renamed API methods from `demo_*` to `presenter_*` for consistency
23
+ - Renamed environment variables from `DEMO_*` to `PRESENTER_*`
24
+ - Updated all documentation and examples to use presenter terminology
25
+ - **BREAKING**: Raised minimum Ruby version from 3.1.0 to 3.2.0 for selenium-webdriver compatibility
26
+
27
+ ### Security
28
+ - Added proper input sanitization to prevent XSS attacks in notifications
29
+ - Implemented safe HTML and JavaScript escaping for user content
30
+
31
+ ### Development
32
+ - Added GitHub Actions CI for Ruby 3.1, 3.2, and 3.3
33
+ - Configured RuboCop with minimal overrides for code quality
34
+ - Set up Lefthook for pre-commit hooks (RuboCop and tests)
35
+ - Removed unused RSpec dependency in favor of Minitest
36
+ - Added proper .gitignore for generated files
37
+
38
+ ## [0.1.0] - 2025-05-27
39
+
40
+ ### Added
41
+ - Initial release of Capybara::Presenter gem
42
+ - Basic presenter mode functionality
43
+ - Configuration system with environment variable support
44
+ - Integration with Capybara test framework
45
+ - Documentation and examples
data/CLAUDE.md ADDED
@@ -0,0 +1,60 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Common Development Commands
6
+
7
+ ### Testing
8
+ - `rake test` - Run gem unit tests
9
+ - `rake presenter:test` - Run sample app system tests
10
+ - `bundle exec ruby test/test_capybara_demo.rb` - Run specific test file
11
+
12
+ ### Development Workflow
13
+ - `bin/console` - Start IRB console with gem loaded
14
+ - `bundle exec rubocop` - Run linting (enforced via pre-commit hooks)
15
+ - `bundle exec rake install` - Install gem locally for testing
16
+ - `bundle exec rake build` - Build gem package
17
+
18
+ ### Demo/Presentation Commands
19
+ - `rake presenter:run` - Run live demo with browser automation
20
+ - `rake presenter:browser` - Run browser-only demo
21
+ - `rake presenter:help` - Show all available presenter options
22
+
23
+ ## Architecture Overview
24
+
25
+ **Capybara::Presenter** is a Ruby gem that transforms standard Capybara system tests into presentation-ready demos. The gem operates by:
26
+
27
+ 1. **Environment-based activation**: Only active when `PRESENTER_MODE=true`
28
+ 2. **Zero overhead when disabled**: No performance impact in regular test runs
29
+ 3. **Visual feedback**: Displays notifications in browser during test execution
30
+ 4. **Configurable timing**: Adds delays between actions for recording-friendly pacing
31
+
32
+ ### Core Components
33
+
34
+ - **Main module** (`lib/capybara/presenter.rb`): Entry point and configuration setup
35
+ - **Configuration** (`lib/capybara/presenter/configuration.rb`): Global settings management
36
+ - **Delays** (`lib/capybara/presenter/delays.rb`): Timing control between actions
37
+ - **Notifications** (`lib/capybara/presenter/notifications.rb`): Browser notification system
38
+ - **Capybara Extensions** (`lib/capybara/presenter/capybara_extensions.rb`): Monkey-patches Capybara methods
39
+
40
+ ### Key Patterns
41
+
42
+ - **Method wrapping**: Core functionality wraps existing Capybara methods without breaking existing tests
43
+ - **JavaScript injection**: Notifications are displayed via injected JavaScript in the browser
44
+ - **Configuration precedence**: Environment variables override global configuration
45
+ - **Sample app**: Complete working example in `examples/sample_app/` using Sinatra
46
+
47
+ ### Setup Requirements
48
+
49
+ For full functionality, tests need:
50
+ 1. **Parallel testing disabled**: `parallelize(workers: 1)` when PRESENTER_MODE=true
51
+ 2. **Non-headless browser**: `driven_by :selenium, using: :chrome` for notifications
52
+ 3. **Test start notifications**: Call `presenter_test_start_notification(self.class, @NAME)` in setup
53
+ 4. **Automatic delays**: Call `setup_presenter_delays` in setup for action delays
54
+
55
+ ### Development Notes
56
+
57
+ - **Ruby version**: Requires >= 3.2.0
58
+ - **Dependencies**: Capybara >= 3.0, Selenium >= 4.0
59
+ - **Testing**: Uses Minitest with comprehensive mocking for browser interactions
60
+ - **Code quality**: RuboCop enforced via Lefthook pre-commit hooks
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 José Coelho
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Capybara::Presenter
2
+
3
+ Transform your Capybara system tests into a presentations with automatic delays and browser notifications. Perfect for creating test recordings and demos.
4
+
5
+ I had this in a project and it helps me give quick updates about the progress of a feature. I like to [communicate often](https://josecoelho.com/Software-Engineer/Communicate-often), the easier, the better. Decided to extract it to a gem with some help from Claude. I hope it's useful for you as well. :)
6
+
7
+ ![Demo](./examples/presenter.gif)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'capybara-presenter', group: :test
15
+ ```
16
+
17
+ Then run:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Complete Setup
26
+
27
+ **1. Include the module in your test class:**
28
+
29
+ ```ruby
30
+ require 'capybara/presenter'
31
+
32
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
33
+ include Capybara::Presenter
34
+
35
+ # Disable parallel testing in presenter mode for sequential execution
36
+ parallelize(workers: 1) if ENV["PRESENTER_MODE"] == "true"
37
+
38
+ # Configure browser driver for presenter mode
39
+ if ENV["PRESENTER_MODE"] == "true"
40
+ driven_by :selenium, using: :chrome, screen_size: [1920, 1080]
41
+ else
42
+ driven_by :selenium, using: :headless_chrome
43
+ end
44
+
45
+ # Add test start notifications and automatic delays
46
+ setup do
47
+ if presenter_mode?
48
+ presenter_test_start_notification(self.class, @NAME)
49
+ setup_presenter_delays
50
+ end
51
+ end
52
+ end
53
+ ```
54
+
55
+ **2. For Minitest users, also add to your individual test classes:**
56
+
57
+ ```ruby
58
+ class UsersSystemTest < ApplicationSystemTestCase
59
+ # Test methods automatically get start notifications
60
+ end
61
+ ```
62
+
63
+ **3. For RSpec users:**
64
+
65
+ ```ruby
66
+ RSpec.configure do |config|
67
+ config.include Capybara::Presenter, type: :system
68
+
69
+ config.before(:each, type: :system) do
70
+ if presenter_mode?
71
+ presenter_test_start_notification(self.class, example.description)
72
+ setup_presenter_delays
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### Running Tests in Presenter Mode
79
+
80
+ ```bash
81
+ # Enable presenter mode
82
+ PRESENTER_MODE=true bundle exec rails test:system
83
+
84
+ # Customize timing
85
+ PRESENTER_MODE=true PRESENTER_DELAY=1.5 bundle exec rails test:system
86
+
87
+ # Disable notifications
88
+ PRESENTER_MODE=true PRESENTER_NOTIFICATIONS=false bundle exec rails test:system
89
+ ```
90
+
91
+ ### In Your Tests
92
+
93
+ ```ruby
94
+ test "user registration" do
95
+ visit new_user_registration_path
96
+
97
+ fill_in "Email", with: "user@example.com"
98
+ fill_in "Password", with: "password123"
99
+ presenter_milestone("Form Complete", "All fields filled")
100
+
101
+ click_button "Sign up"
102
+ presenter_milestone("Success", "User registered successfully")
103
+
104
+ assert_text "Welcome!"
105
+ end
106
+ ```
107
+
108
+ ## API
109
+
110
+ - `presenter_mode?` - Check if presenter mode is enabled
111
+ - `presenter_delay(seconds)` - Add custom delay
112
+ - `presenter_notification(title, message)` - Show browser notification
113
+ - `presenter_milestone(title, message)` - Show milestone notification
114
+
115
+ ## Configuration
116
+
117
+ ```ruby
118
+ Capybara::Presenter.configure do |config|
119
+ config.enabled = ENV['PRESENTER_MODE'] == 'true'
120
+ config.delay = 2.0
121
+ config.notifications = true
122
+ config.notification_position = :center # :top, :center, :bottom
123
+ end
124
+ ```
125
+
126
+ ## Environment Variables
127
+
128
+ - `PRESENTER_MODE=true` - Enable presenter mode
129
+ - `PRESENTER_DELAY=2.0` - Action delay in seconds
130
+ - `PRESENTER_TEST_START_DELAY=2.0` - Delay before each test
131
+ - `PRESENTER_NOTIFICATIONS=false` - Disable notifications
132
+
133
+ ## Browser Setup
134
+
135
+ For notifications to appear, use a non-headless driver:
136
+
137
+ ```ruby
138
+ if ENV["PRESENTER_MODE"] == "true"
139
+ driven_by :selenium, using: :chrome
140
+ else
141
+ driven_by :selenium, using: :headless_chrome
142
+ end
143
+ ```
144
+
145
+ ## Examples
146
+
147
+ ```bash
148
+ # See the gem in action
149
+ rake presenter:run
150
+
151
+ # View all options
152
+ rake presenter:help
153
+ ```
154
+
155
+ ## License
156
+
157
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ # Main gem tests
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/test_*.rb']
11
+ end
12
+
13
+ # Presenter and example tasks
14
+ # rubocop:disable Metrics/BlockLength
15
+ namespace :presenter do
16
+ desc 'Run the sample app system tests in normal mode'
17
+ task :test do
18
+ Dir.chdir('examples/sample_app') do
19
+ sh 'ruby system_test.rb'
20
+ end
21
+ end
22
+
23
+ desc 'Run the sample app system tests in presenter mode'
24
+ task :run do
25
+ Dir.chdir('examples/sample_app') do
26
+ sh 'PRESENTER_MODE=true ruby system_test.rb'
27
+ end
28
+ end
29
+
30
+ desc 'Run presenter with custom delays (DELAY=3.0 START_DELAY=2.0)'
31
+ task :slow do
32
+ delay = ENV['DELAY'] || '3.0'
33
+ start_delay = ENV['START_DELAY'] || '2.0'
34
+ Dir.chdir('examples/sample_app') do
35
+ sh "PRESENTER_MODE=true PRESENTER_DELAY=#{delay} PRESENTER_TEST_START_DELAY=#{start_delay} ruby system_test.rb"
36
+ end
37
+ end
38
+
39
+ desc 'Show all presenter options'
40
+ task :help do
41
+ puts <<~HELP
42
+ 🎬 Capybara::Presenter - Available Rake Tasks
43
+
44
+ Main Tasks:
45
+ rake test - Run gem unit tests
46
+ rake presenter:test - Run sample app tests (normal mode)
47
+ rake presenter:run - Run sample app tests (presenter mode)
48
+ rake presenter:slow - Run presenter with slower timing
49
+
50
+ Custom Presenter Options:
51
+ rake presenter:slow DELAY=3.0 START_DELAY=2.0
52
+ #{' '}
53
+ Environment Variables:
54
+ PRESENTER_MODE=true - Enable presenter mode
55
+ PRESENTER_DELAY=2.0 - Action delay in seconds
56
+ PRESENTER_TEST_START_DELAY=2.0 - Test start delay in seconds
57
+ PRESENTER_NOTIFICATIONS=false - Disable notifications
58
+ #{' '}
59
+ Examples:
60
+ rake presenter:run # Basic presenter
61
+ PRESENTER_DELAY=1.0 rake presenter:run # Faster presenter
62
+ PRESENTER_NOTIFICATIONS=false rake presenter:run # No notifications
63
+ HELP
64
+ end
65
+ end
66
+ # rubocop:enable Metrics/BlockLength
67
+
68
+ task default: :test
69
+
70
+ # Add help task at top level
71
+ desc 'Show all available tasks'
72
+ task :help do
73
+ puts <<~HELP
74
+ 🎬 Capybara::Presenter Gem
75
+
76
+ Main Commands:
77
+ rake test - Run gem tests
78
+ rake presenter:help - Presenter options
79
+ #{' '}
80
+ Quick Start:
81
+ rake presenter:run - See the gem in action!
82
+ HELP
83
+ end
data/lefthook.yml ADDED
@@ -0,0 +1,8 @@
1
+ pre-commit:
2
+ commands:
3
+ rubocop:
4
+ run: bundle exec rubocop --autocorrect {staged_files}
5
+ glob: "*.rb"
6
+ stage_fixed: true
7
+ tests:
8
+ run: bundle exec rake test
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Presenter
5
+ # Extends Capybara methods with presentation features.
6
+ # Wraps common Capybara actions to add delays and logging for better presentations.
7
+ module CapybaraExtensions
8
+ WRAPPED_METHODS = %i[visit click_button click_link click_on fill_in select choose check uncheck
9
+ attach_file].freeze
10
+
11
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
12
+ def setup_presenter_delays
13
+ return unless presenter_mode?
14
+ return if self.class.instance_variable_get(:@presenter_delays_setup)
15
+
16
+ self.class.instance_variable_set(:@presenter_delays_setup, true)
17
+
18
+ WRAPPED_METHODS.each do |action|
19
+ next if respond_to?(:"#{action}_without_presenter")
20
+
21
+ # Create alias for original method
22
+ self.class.alias_method :"#{action}_without_presenter", action
23
+
24
+ # Override the method with presenter delays
25
+ self.class.define_method(action) do |*args, **kwargs, &block|
26
+ if presenter_mode?
27
+ log_presenter_action(action, args.first)
28
+
29
+ # Pre-action delay
30
+ case action
31
+ when :click_button, :click_link, :click_on
32
+ presenter_delay(0.3)
33
+ when :fill_in, :select, :choose, :check, :uncheck
34
+ presenter_delay(0.2)
35
+ when :visit
36
+ presenter_delay(0.5)
37
+ when :attach_file
38
+ presenter_delay(0.3)
39
+ end
40
+ end
41
+
42
+ # Execute the original action
43
+ result = send(:"#{action}_without_presenter", *args, **kwargs, &block)
44
+
45
+ # Post-action delay for actions that trigger page changes
46
+ presenter_delay(0.8) if presenter_mode? && %i[click_button click_link click_on visit].include?(action)
47
+
48
+ result
49
+ end
50
+ end
51
+ end
52
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
53
+
54
+ private
55
+
56
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
57
+ def log_presenter_action(action, target)
58
+ return unless presenter_mode?
59
+
60
+ case action
61
+ when :click_button, :click_link, :click_on
62
+ puts "🖱️ Clicking: #{target}" if target
63
+ when :fill_in
64
+ puts "⌨️ Filling: #{target}" if target
65
+ when :select, :choose, :check, :uncheck
66
+ puts "☑️ Selecting: #{target}" if target
67
+ when :visit
68
+ puts "🌐 Navigating to: #{target}" if target
69
+ when :attach_file
70
+ puts "📎 Attaching file: #{target}" if target
71
+ end
72
+ end
73
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Presenter
5
+ # Configuration class for Capybara::Presenter settings.
6
+ # Manages global settings like delays, notifications, and environment variable parsing.
7
+ class Configuration
8
+ attr_reader :enabled, :delay, :notifications, :notification_position, :test_start_delay
9
+
10
+ def initialize
11
+ @enabled = ENV['PRESENTER_MODE'] == 'true'
12
+ @delay = parse_float_env('PRESENTER_DELAY', 2.0)
13
+ @notifications = ENV['PRESENTER_NOTIFICATIONS'] != 'false'
14
+ @notification_position = :center
15
+ @test_start_delay = parse_float_env('PRESENTER_TEST_START_DELAY', 2.0)
16
+ end
17
+
18
+ def enabled=(value)
19
+ @enabled = !!value
20
+ end
21
+
22
+ def delay=(value)
23
+ raise ArgumentError, 'delay must be a positive number' unless value.is_a?(Numeric) && value.positive?
24
+
25
+ @delay = value.to_f
26
+ end
27
+
28
+ def notifications=(value)
29
+ @notifications = !!value
30
+ end
31
+
32
+ def notification_position=(value)
33
+ valid_positions = %i[top center bottom]
34
+ unless valid_positions.include?(value)
35
+ raise ArgumentError, "notification_position must be one of: #{valid_positions.join(', ')}"
36
+ end
37
+
38
+ @notification_position = value
39
+ end
40
+
41
+ def test_start_delay=(value)
42
+ raise ArgumentError, 'test_start_delay must be a positive number' unless value.is_a?(Numeric) && value >= 0
43
+
44
+ @test_start_delay = value.to_f
45
+ end
46
+
47
+ private
48
+
49
+ # rubocop:disable Metrics/MethodLength
50
+ def parse_float_env(env_var, default)
51
+ value = ENV.fetch(env_var, nil)
52
+ return default unless value
53
+
54
+ begin
55
+ parsed = Float(value)
56
+ if parsed.negative?
57
+ warn "Warning: #{env_var}=#{value} is negative, using default #{default}"
58
+ default
59
+ else
60
+ parsed
61
+ end
62
+ rescue ArgumentError
63
+ warn "Warning: #{env_var}=#{value} is not a valid number, using default #{default}"
64
+ default
65
+ end
66
+ end
67
+ # rubocop:enable Metrics/MethodLength
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Presenter
5
+ # Provides browser notification functionality for presenting test milestones.
6
+ # Injects JavaScript to display visual notifications during test execution.
7
+ # rubocop:disable Metrics/ModuleLength
8
+ module Notifications
9
+ def presenter_notification(title, message = nil, type: :info, duration: 4000)
10
+ return unless presenter_mode? && presenter_notifications_enabled?
11
+
12
+ show_browser_notification(title, message, type, duration)
13
+ end
14
+
15
+ def presenter_milestone(title, message = nil)
16
+ return unless presenter_mode?
17
+
18
+ presenter_notification("📋 #{title}", message, type: :success, duration: 3000)
19
+ presenter_delay(1.0)
20
+ end
21
+
22
+ def presenter_test_start_notification(test_class, test_method)
23
+ return unless presenter_mode? && presenter_notifications_enabled?
24
+
25
+ test_name = test_class.name.gsub(/Test$/, '')
26
+ method_name = test_method.gsub(/^test_/, '').gsub('_', ' ').split.map(&:capitalize).join(' ')
27
+
28
+ presenter_notification("🧪 #{test_name}", "Running: #{method_name}", type: :info, duration: 8000)
29
+ puts "📋 Starting test: #{test_name} - #{method_name}"
30
+ presenter_delay(Capybara::Presenter.configuration.test_start_delay)
31
+ end
32
+
33
+ private
34
+
35
+ def show_browser_notification(title, message, type, duration)
36
+ return unless has_browser_access?
37
+
38
+ begin
39
+ page.driver.browser.execute_script(notification_javascript(title, message, type, duration))
40
+ rescue StandardError => e
41
+ # Gracefully handle JavaScript errors
42
+ puts "Presenter notification failed: #{e.message}" if ENV['DEBUG']
43
+ end
44
+ end
45
+
46
+ def has_browser_access?
47
+ respond_to?(:page) &&
48
+ page.respond_to?(:driver) &&
49
+ page.driver.respond_to?(:browser)
50
+ end
51
+
52
+ # rubocop:disable Metrics/MethodLength
53
+ def notification_javascript(title, message, _type, duration)
54
+ # Sanitize inputs to prevent XSS
55
+ safe_title = sanitize_for_js(title)
56
+ safe_message = sanitize_for_js(message) if message
57
+ safe_title_html = sanitize_for_html(title)
58
+ safe_message_html = sanitize_for_html(message) if message
59
+
60
+ position = notification_position_css
61
+ <<~JS
62
+ console.log('Presenter notification: #{safe_title}');
63
+
64
+ // Remove existing presenter notifications
65
+ document.querySelectorAll('.presenter-notification').forEach(el => el.remove());
66
+
67
+ // Create notification container if it doesn't exist
68
+ let container = document.getElementById('presenter-notification-container');
69
+ if (!container) {
70
+ container = document.createElement('div');
71
+ container.id = 'presenter-notification-container';
72
+ container.style.cssText = `
73
+ position: fixed;
74
+ #{position}
75
+ z-index: 10000;
76
+ pointer-events: none;
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ gap: 12px;
81
+ `;
82
+ document.body.appendChild(container);
83
+ }
84
+
85
+ // Create notification element
86
+ const notification = document.createElement('div');
87
+ notification.className = 'presenter-notification';
88
+
89
+ notification.style.cssText = `
90
+ background: rgba(0, 0, 0, 0.5);
91
+ color: white;
92
+ padding: 32px 48px;
93
+ border-radius: 8px;
94
+ min-width: 700px;
95
+ max-width: 900px;
96
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
97
+ font-size: 22px;
98
+ font-weight: 600;
99
+ line-height: 1.3;
100
+ text-align: center;
101
+ transition: all 0.3s ease-in-out;
102
+ pointer-events: auto;
103
+ `;
104
+
105
+ notification.innerHTML = `
106
+ <div style="font-size: 26px; font-weight: 800; margin-bottom: #{message ? '12px' : '0'};">#{safe_title_html}</div>
107
+ #{safe_message ? "<div style=\"font-size: 20px; font-weight: 500; opacity: 0.9;\">#{safe_message_html}</div>" : ''}
108
+ `;
109
+
110
+ container.appendChild(notification);
111
+
112
+ // Auto remove after duration
113
+ setTimeout(() => {
114
+ notification.style.opacity = '0';
115
+ notification.style.transform = 'translateY(-10px)';
116
+ setTimeout(() => {
117
+ if (notification.parentNode) {
118
+ notification.parentNode.removeChild(notification);
119
+ }
120
+ }, 300);
121
+ }, #{duration});
122
+
123
+ // Click to dismiss
124
+ notification.addEventListener('click', () => {
125
+ notification.style.opacity = '0';
126
+ notification.style.transform = 'translateY(-10px)';
127
+ setTimeout(() => {
128
+ if (notification.parentNode) {
129
+ notification.parentNode.removeChild(notification);
130
+ }
131
+ }, 300);
132
+ });
133
+ JS
134
+ end
135
+
136
+ def notification_position_css
137
+ case Capybara::Presenter.configuration.notification_position
138
+ when :top
139
+ 'top: 40px; left: 50%; transform: translateX(-50%);'
140
+ when :center
141
+ 'top: 50%; left: 50%; transform: translate(-50%, -50%);'
142
+ when :bottom
143
+ 'bottom: 40px; left: 50%; transform: translateX(-50%);'
144
+ else
145
+ 'top: 40px; left: 50%; transform: translateX(-50%);'
146
+ end
147
+ end
148
+ # rubocop:enable Metrics/MethodLength
149
+
150
+ def sanitize_for_js(string)
151
+ return '' unless string
152
+
153
+ string.to_s
154
+ .gsub('\\', '\\\\') # Escape backslashes first
155
+ .gsub("'", "\\'") # Escape single quotes
156
+ .gsub('"', '\\"') # Escape double quotes
157
+ .gsub("\n", '\\n') # Escape newlines
158
+ .gsub("\r", '\\r') # Escape carriage returns
159
+ .gsub("\t", '\\t') # Escape tabs
160
+ .gsub('</script>', '<\\/script>') # Prevent script injection
161
+ end
162
+
163
+ def sanitize_for_html(string)
164
+ return '' unless string
165
+
166
+ string.to_s
167
+ .gsub('&', '&amp;') # Escape ampersands first
168
+ .gsub('<', '&lt;') # Escape less than
169
+ .gsub('>', '&gt;') # Escape greater than
170
+ .gsub('"', '&quot;') # Escape double quotes
171
+ .gsub("'", '&#39;') # Escape single quotes
172
+ end
173
+ end
174
+ # rubocop:enable Metrics/ModuleLength
175
+ end
176
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Presenter
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'presenter/version'
4
+ require_relative 'presenter/configuration'
5
+ require_relative 'presenter/notifications'
6
+ require_relative 'presenter/capybara_extensions'
7
+
8
+ module Capybara
9
+ # Main module for the Capybara::Presenter gem.
10
+ # Transforms standard Capybara system tests into presentation-ready demos
11
+ # with visual notifications and configurable delays.
12
+ module Presenter
13
+ class Error < StandardError; end
14
+
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration) if block_given?
22
+ end
23
+
24
+ def reset_configuration!
25
+ @configuration = Configuration.new
26
+ end
27
+ end
28
+
29
+ def self.included(base)
30
+ base.extend(ClassMethods)
31
+ base.include(InstanceMethods)
32
+
33
+ # Show setup guidance when presenter mode is enabled
34
+ return unless configuration.enabled
35
+
36
+ show_setup_guidance
37
+ end
38
+
39
+ # rubocop:disable Metrics/MethodLength
40
+ def self.show_setup_guidance
41
+ puts <<~GUIDANCE
42
+
43
+ 🎬 Capybara::Presenter Setup Guidance:
44
+
45
+ For browser notifications to appear:
46
+ 1. Use a non-headless driver (Chrome, Firefox, Safari)
47
+ 2. Ensure browser opens visibly#{' '}
48
+ 3. Disable parallel testing: parallelize(workers: 1)
49
+
50
+ Environment variables:
51
+ - PRESENTER_DELAY=2.0 (action delays)
52
+ - PRESENTER_TEST_START_DELAY=2.0 (pause before each test)
53
+ - PRESENTER_NOTIFICATIONS=false (disable notifications)
54
+
55
+ 💡 This gem works with your existing Capybara driver configuration
56
+
57
+ GUIDANCE
58
+ end
59
+ # rubocop:enable Metrics/MethodLength
60
+
61
+ # Class-level helper methods for checking presenter configuration.
62
+ module ClassMethods
63
+ def presenter_mode?
64
+ Capybara::Presenter.configuration.enabled
65
+ end
66
+
67
+ def presenter_delay_duration
68
+ Capybara::Presenter.configuration.delay
69
+ end
70
+
71
+ def presenter_notifications_enabled?
72
+ Capybara::Presenter.configuration.notifications
73
+ end
74
+ end
75
+
76
+ # Instance-level helper methods for checking presenter configuration.
77
+ module InstanceMethods
78
+ include Notifications
79
+ include CapybaraExtensions
80
+
81
+ def presenter_mode?
82
+ self.class.presenter_mode?
83
+ end
84
+
85
+ def presenter_delay(seconds = nil)
86
+ return unless presenter_mode?
87
+
88
+ sleep(seconds || presenter_delay_duration)
89
+ end
90
+
91
+ def presenter_delay_duration
92
+ self.class.presenter_delay_duration
93
+ end
94
+
95
+ def presenter_notifications_enabled?
96
+ self.class.presenter_notifications_enabled?
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,70 @@
1
+ module Capybara
2
+ module Presenter
3
+ VERSION: String
4
+
5
+ class Configuration
6
+ attr_reader enabled: bool
7
+ attr_reader delay: Float
8
+ attr_reader notifications: bool
9
+ attr_reader notification_position: Symbol
10
+ attr_reader notification_style: Symbol
11
+ attr_reader test_start_delay: Float
12
+
13
+ def initialize: () -> void
14
+ def enabled=: (untyped value) -> bool
15
+ def delay=: (Numeric value) -> Float
16
+ def notifications=: (untyped value) -> bool
17
+ def notification_position=: (Symbol value) -> Symbol
18
+ def notification_style=: (Symbol value) -> Symbol
19
+ def test_start_delay=: (Numeric value) -> Float
20
+
21
+ private
22
+
23
+ def parse_float_env: (String env_var, Float default) -> Float
24
+ end
25
+
26
+ module Notifications
27
+ def presenter_notification: (String title, ?String? message, ?type: Symbol, ?duration: Integer) -> void
28
+ def presenter_milestone: (String title, ?String? message) -> void
29
+ def presenter_test_start_notification: (Class test_class, String test_method) -> void
30
+
31
+ private
32
+
33
+ def show_browser_notification: (String title, String? message, Symbol type, Integer duration) -> void
34
+ def has_browser_access?: () -> bool
35
+ def notification_javascript: (String title, String? message, Symbol type, Integer duration) -> String
36
+ def notification_position_css: () -> String
37
+ def sanitize_for_js: (String? string) -> String
38
+ def sanitize_for_html: (String? string) -> String
39
+ end
40
+
41
+ module Delays
42
+ def presenter_delay: (?Float? seconds) -> void
43
+ end
44
+
45
+ module CapybaraExtensions
46
+ def setup_presenter_delays: () -> void
47
+
48
+ private
49
+
50
+ def log_presenter_action: (Symbol action, untyped target) -> void
51
+ end
52
+
53
+ def self.configuration: () -> Configuration
54
+ def self.configure: () { (Configuration) -> void } -> void
55
+ def self.reset_configuration!: () -> Configuration
56
+ def self.show_setup_guidance: () -> void
57
+
58
+ module ClassMethods
59
+ def presenter_mode?: () -> bool
60
+ def presenter_delay_duration: () -> Float
61
+ def presenter_notifications_enabled?: () -> bool
62
+ end
63
+
64
+ module InstanceMethods
65
+ def presenter_mode?: () -> bool
66
+ def presenter_delay_duration: () -> Float
67
+ def presenter_notifications_enabled?: () -> bool
68
+ end
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capybara-presenter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - José Coelho
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-05-27 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: capybara
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: selenium-webdriver
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: lefthook
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.11'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.11'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.75'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.75'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop-minitest
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.38'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.38'
110
+ description: Transform Capybara system tests into polished presentations with automatic
111
+ delays, browser notifications, and visual cues perfect for creating professional
112
+ test recordings and demonstrations.
113
+ email:
114
+ - jose.coelho@palomagroup.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".rubocop.yml"
120
+ - CHANGELOG.md
121
+ - CLAUDE.md
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - lefthook.yml
126
+ - lib/capybara/presenter.rb
127
+ - lib/capybara/presenter/capybara_extensions.rb
128
+ - lib/capybara/presenter/configuration.rb
129
+ - lib/capybara/presenter/notifications.rb
130
+ - lib/capybara/presenter/version.rb
131
+ - sig/capybara/demo.rbs
132
+ homepage: https://github.com/josecoelho/capybara-presenter
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ allowed_push_host: https://rubygems.org
137
+ homepage_uri: https://github.com/josecoelho/capybara-presenter
138
+ source_code_uri: https://github.com/josecoelho/capybara-presenter
139
+ changelog_uri: https://github.com/josecoelho/capybara-presenter/blob/main/CHANGELOG.md
140
+ rubygems_mfa_required: 'true'
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 3.2.0
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 3.6.6
156
+ specification_version: 4
157
+ summary: Make Capybara tests presentation-ready with visual notifications and recording-friendly
158
+ delays
159
+ test_files: []