capydash 0.1.7 → 0.2.1

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.
@@ -1,78 +0,0 @@
1
- require 'eventmachine'
2
- require 'faye/websocket'
3
- require 'json'
4
-
5
- module CapyDash
6
- # Forwards events to an external WebSocket server when CAPYDASH_EXTERNAL_WS=1
7
- class Forwarder
8
- attr_reader :port
9
-
10
- def self.instance(port: nil)
11
- configured_port = port || (CapyDash.respond_to?(:configuration) ? CapyDash.configuration&.port : nil)
12
- @instance ||= new(port: configured_port || 4000)
13
- end
14
-
15
- def initialize(port: 4000)
16
- @port = port
17
- @connected = false
18
- @queue = []
19
- @ws = nil
20
- @mutex = Mutex.new
21
- start
22
- end
23
-
24
- def start
25
- return if @started
26
- @started = true
27
-
28
- Thread.new do
29
- EM.run do
30
- connect
31
- end
32
- end
33
- end
34
-
35
- def connect
36
- url = "ws://127.0.0.1:#{@port}"
37
- @ws = Faye::WebSocket::Client.new(url)
38
-
39
- @ws.on(:open) do |_event|
40
- @connected = true
41
- flush_queue
42
- end
43
-
44
- @ws.on(:close) do |_event|
45
- @connected = false
46
- # attempt reconnect after short delay
47
- EM.add_timer(0.5) { connect }
48
- end
49
-
50
- @ws.on(:error) do |event|
51
- # keep trying; errors are expected if server not yet up
52
- end
53
- end
54
-
55
- def send_message(raw_message)
56
- message = raw_message.is_a?(String) ? raw_message : JSON.dump(raw_message)
57
- @mutex.synchronize do
58
- if @connected && @ws
59
- @ws.send(message)
60
- else
61
- @queue << message
62
- end
63
- end
64
- end
65
-
66
- private
67
-
68
- def flush_queue
69
- return unless @connected && @ws
70
- pending = nil
71
- @mutex.synchronize do
72
- pending = @queue.dup
73
- @queue.clear
74
- end
75
- pending.each { |m| @ws.send(m) }
76
- end
77
- end
78
- end
@@ -1,153 +0,0 @@
1
- require 'capybara'
2
- require 'capydash/dashboard_server'
3
- require 'base64'
4
- require 'fileutils'
5
-
6
- module CapyDash
7
- module Instrumentation
8
- def visit(path)
9
- emit_step("visit", path) { super(path) }
10
- end
11
-
12
- def click_button(*args)
13
- emit_step("click_button", args.first) { super(*args) }
14
- end
15
-
16
- def fill_in(locator, with:)
17
- emit_step("fill_in", "#{locator} => #{with}") { super(locator, with: with) }
18
- end
19
-
20
- private
21
-
22
- def emit_step(step_name, detail)
23
- # emit "running" event first (without screenshot)
24
- CapyDash::EventEmitter.broadcast(
25
- step_name: step_name,
26
- detail: detail,
27
- test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
28
- status: "running"
29
- )
30
-
31
- # run the original step
32
- yield
33
-
34
- # take screenshot AFTER the action is completed
35
- base_dir = if CapyDash.respond_to?(:configuration)
36
- CapyDash.configuration&.screenshot_path || "tmp/capydash_screenshots"
37
- else
38
- "tmp/capydash_screenshots"
39
- end
40
- FileUtils.mkdir_p(base_dir) unless Dir.exist?(base_dir)
41
- timestamp = Time.now.strftime('%Y%m%d-%H%M%S-%L')
42
- safe_step = step_name.gsub(/\s+/, '_')
43
- screenshot_path = File.join(base_dir, "#{safe_step}-#{timestamp}.png")
44
-
45
- # Ensure we have an absolute path for Base64 encoding
46
- absolute_screenshot_path = File.absolute_path(screenshot_path)
47
-
48
- data_url = nil
49
- if defined?(Capybara)
50
- begin
51
- current_driver = Capybara.current_driver
52
- if current_driver != :rack_test
53
- if respond_to?(:page)
54
- page.save_screenshot(screenshot_path)
55
- else
56
- Capybara.current_session.save_screenshot(screenshot_path)
57
- end
58
-
59
- # Try to find the actual screenshot file location
60
- actual_screenshot_path = nil
61
- if File.exist?(absolute_screenshot_path)
62
- actual_screenshot_path = absolute_screenshot_path
63
- else
64
- # Check if it was saved in the capybara tmp directory
65
- capybara_path = File.join(Dir.pwd, "tmp", "capybara", screenshot_path)
66
- if File.exist?(capybara_path)
67
- actual_screenshot_path = capybara_path
68
- end
69
- end
70
-
71
- if actual_screenshot_path && File.exist?(actual_screenshot_path)
72
- encoded = Base64.strict_encode64(File.binread(actual_screenshot_path))
73
- data_url = "data:image/png;base64,#{encoded}"
74
- end
75
- end
76
- rescue => e
77
- # Silently handle screenshot capture failures
78
- end
79
- end
80
-
81
- # mark success with screenshot
82
- CapyDash::EventEmitter.broadcast(
83
- step_name: step_name,
84
- detail: detail,
85
- screenshot: actual_screenshot_path || screenshot_path,
86
- data_url: data_url,
87
- test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
88
- status: "passed"
89
- )
90
- rescue => e
91
- # take screenshot even on failure (after the action was attempted)
92
- base_dir = if CapyDash.respond_to?(:configuration)
93
- CapyDash.configuration&.screenshot_path || "tmp/capydash_screenshots"
94
- else
95
- "tmp/capydash_screenshots"
96
- end
97
- FileUtils.mkdir_p(base_dir) unless Dir.exist?(base_dir)
98
- timestamp = Time.now.strftime('%Y%m%d-%H%M%S-%L')
99
- safe_step = step_name.gsub(/\s+/, '_')
100
- screenshot_path = File.join(base_dir, "#{safe_step}-#{timestamp}.png")
101
-
102
- # Ensure we have an absolute path for Base64 encoding
103
- absolute_screenshot_path = File.absolute_path(screenshot_path)
104
-
105
- data_url = nil
106
- if defined?(Capybara)
107
- begin
108
- current_driver = Capybara.current_driver
109
- if current_driver != :rack_test
110
- if respond_to?(:page)
111
- page.save_screenshot(screenshot_path)
112
- else
113
- Capybara.current_session.save_screenshot(screenshot_path)
114
- end
115
-
116
- # Try to find the actual screenshot file location
117
- actual_screenshot_path = nil
118
- if File.exist?(absolute_screenshot_path)
119
- actual_screenshot_path = absolute_screenshot_path
120
- else
121
- # Check if it was saved in the capybara tmp directory
122
- capybara_path = File.join(Dir.pwd, "tmp", "capybara", screenshot_path)
123
- if File.exist?(capybara_path)
124
- actual_screenshot_path = capybara_path
125
- end
126
- end
127
-
128
- if actual_screenshot_path && File.exist?(actual_screenshot_path)
129
- encoded = Base64.strict_encode64(File.binread(actual_screenshot_path))
130
- data_url = "data:image/png;base64,#{encoded}"
131
- end
132
- end
133
- rescue => e
134
- # Silently handle screenshot capture failures
135
- end
136
- end
137
-
138
- CapyDash::EventEmitter.broadcast(
139
- step_name: step_name,
140
- detail: detail,
141
- screenshot: actual_screenshot_path || screenshot_path,
142
- data_url: data_url,
143
- test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
144
- status: "failed",
145
- error: e.message
146
- )
147
- raise e
148
- end
149
- end
150
- end
151
-
152
- # Prepend into Capybara DSL so it wraps all calls
153
- Capybara::Session.prepend(CapyDash::Instrumentation)
@@ -1,99 +0,0 @@
1
- require 'logger'
2
- require 'fileutils'
3
-
4
- module CapyDash
5
- class Logger
6
- class << self
7
- attr_accessor :instance
8
- end
9
-
10
- def initialize(config = nil)
11
- @config = config || Configuration.new
12
- @logger = create_logger
13
- end
14
-
15
- def self.setup(config = nil)
16
- self.instance = new(config)
17
- end
18
-
19
- def self.info(message, context = {})
20
- instance&.info(message, context)
21
- end
22
-
23
- def self.warn(message, context = {})
24
- instance&.warn(message, context)
25
- end
26
-
27
- def self.error(message, context = {})
28
- instance&.error(message, context)
29
- end
30
-
31
- def self.debug(message, context = {})
32
- instance&.debug(message, context)
33
- end
34
-
35
- def info(message, context = {})
36
- log(:info, message, context)
37
- end
38
-
39
- def warn(message, context = {})
40
- log(:warn, message, context)
41
- end
42
-
43
- def error(message, context = {})
44
- log(:error, message, context)
45
- end
46
-
47
- def debug(message, context = {})
48
- log(:debug, message, context)
49
- end
50
-
51
- private
52
-
53
- def create_logger
54
- # Ensure log directory exists
55
- log_dir = File.dirname(@config.log_file)
56
- FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
57
-
58
- # Create logger with file rotation
59
- logger = ::Logger.new(
60
- @config.log_file,
61
- @config.max_files,
62
- @config.max_file_size
63
- )
64
-
65
- # Set log level
66
- logger.level = case @config.log_level.downcase
67
- when 'debug' then ::Logger::DEBUG
68
- when 'info' then ::Logger::INFO
69
- when 'warn' then ::Logger::WARN
70
- when 'error' then ::Logger::ERROR
71
- else ::Logger::INFO
72
- end
73
-
74
- # Set format
75
- logger.formatter = proc do |severity, datetime, progname, msg|
76
- "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{progname}: #{msg}\n"
77
- end
78
-
79
- logger
80
- end
81
-
82
- def log(level, message, context = {})
83
- return unless @logger
84
-
85
- # Add context information
86
- full_message = message
87
- if context.any?
88
- context_str = context.map { |k, v| "#{k}=#{v}" }.join(' ')
89
- full_message = "#{message} | #{context_str}"
90
- end
91
-
92
- @logger.send(level, full_message)
93
- rescue => e
94
- # Fallback to stdout if logging fails
95
- puts "Logging error: #{e.message}"
96
- puts "#{level.upcase}: #{message}"
97
- end
98
- end
99
- end
@@ -1,134 +0,0 @@
1
- require 'json'
2
- require 'fileutils'
3
-
4
- module CapyDash
5
- class Persistence
6
- class << self
7
- def save_test_run(test_run_data)
8
- ensure_data_directory
9
- file_path = test_run_file_path(test_run_data[:id] || generate_run_id)
10
-
11
- begin
12
- File.write(file_path, JSON.pretty_generate(test_run_data))
13
- file_path
14
- rescue => e
15
- ErrorHandler.handle_error(e, {
16
- error_type: 'persistence',
17
- operation: 'save_test_run',
18
- run_id: test_run_data[:id]
19
- })
20
- nil
21
- end
22
- end
23
-
24
- def load_test_run(run_id)
25
- file_path = test_run_file_path(run_id)
26
- return nil unless File.exist?(file_path)
27
-
28
- begin
29
- JSON.parse(File.read(file_path), symbolize_names: true)
30
- rescue => e
31
- ErrorHandler.handle_error(e, {
32
- error_type: 'persistence',
33
- operation: 'load_test_run',
34
- run_id: run_id
35
- })
36
- nil
37
- end
38
- end
39
-
40
- def list_test_runs(limit = 50)
41
- ensure_data_directory
42
- data_dir = data_directory
43
-
44
- begin
45
- Dir.glob(File.join(data_dir, "test_run_*.json"))
46
- .sort_by { |f| File.mtime(f) }
47
- .reverse
48
- .first(limit)
49
- .map do |file_path|
50
- run_id = File.basename(file_path, '.json').gsub('test_run_', '')
51
- {
52
- id: run_id,
53
- file_path: file_path,
54
- created_at: File.mtime(file_path),
55
- size: File.size(file_path)
56
- }
57
- end
58
- rescue => e
59
- ErrorHandler.handle_error(e, {
60
- error_type: 'persistence',
61
- operation: 'list_test_runs'
62
- })
63
- []
64
- end
65
- end
66
-
67
- def delete_test_run(run_id)
68
- file_path = test_run_file_path(run_id)
69
- return false unless File.exist?(file_path)
70
-
71
- begin
72
- File.delete(file_path)
73
- Logger.info("Test run deleted", { run_id: run_id })
74
- true
75
- rescue => e
76
- ErrorHandler.handle_error(e, {
77
- error_type: 'persistence',
78
- operation: 'delete_test_run',
79
- run_id: run_id
80
- })
81
- false
82
- end
83
- end
84
-
85
- def cleanup_old_runs(days_to_keep = 30)
86
- ensure_data_directory
87
- cutoff_time = Time.now - (days_to_keep * 24 * 60 * 60)
88
- deleted_count = 0
89
-
90
- begin
91
- Dir.glob(File.join(data_directory, "test_run_*.json")).each do |file_path|
92
- if File.mtime(file_path) < cutoff_time
93
- run_id = File.basename(file_path, '.json').gsub('test_run_', '')
94
- if delete_test_run(run_id)
95
- deleted_count += 1
96
- end
97
- end
98
- end
99
-
100
- Logger.info("Cleanup completed", {
101
- deleted_runs: deleted_count,
102
- days_kept: days_to_keep
103
- })
104
-
105
- deleted_count
106
- rescue => e
107
- ErrorHandler.handle_error(e, {
108
- error_type: 'persistence',
109
- operation: 'cleanup_old_runs'
110
- })
111
- 0
112
- end
113
- end
114
-
115
- private
116
-
117
- def data_directory
118
- @data_directory ||= File.join(Dir.pwd, "tmp", "capydash_data")
119
- end
120
-
121
- def ensure_data_directory
122
- FileUtils.mkdir_p(data_directory) unless Dir.exist?(data_directory)
123
- end
124
-
125
- def test_run_file_path(run_id)
126
- File.join(data_directory, "test_run_#{run_id}.json")
127
- end
128
-
129
- def generate_run_id
130
- "#{Time.now.to_i}_#{SecureRandom.hex(4)}"
131
- end
132
- end
133
- end
134
- end