Class: Appom::Visual::TestHelpers

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/appom/visual.rb

Overview

Visual testing and comparison utilities

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Logging

level, level=, #log_debug, #log_element_action, #log_error, #log_info, #log_wait_end, #log_wait_start, #log_warn, #logger

Constructor Details

#initialize(baseline_dir: 'visual_baselines', results_dir: 'visual_results', threshold: 0.01) ⇒ TestHelpers

Returns a new instance of TestHelpers.



17
18
19
20
21
22
23
24
# File 'lib/appom/visual.rb', line 17

def initialize(baseline_dir: 'visual_baselines', results_dir: 'visual_results', threshold: 0.01)
  @baseline_dir = File.expand_path(baseline_dir)
  @results_dir = File.expand_path(results_dir)
  @threshold = threshold
  @comparison_results = []

  ensure_directories_exist
end

Instance Attribute Details

#baseline_dirObject (readonly)

Returns the value of attribute baseline_dir.



15
16
17
# File 'lib/appom/visual.rb', line 15

def baseline_dir
  @baseline_dir
end

#results_dirObject (readonly)

Returns the value of attribute results_dir.



15
16
17
# File 'lib/appom/visual.rb', line 15

def results_dir
  @results_dir
end

#thresholdObject (readonly)

Returns the value of attribute threshold.



15
16
17
# File 'lib/appom/visual.rb', line 15

def threshold
  @threshold
end

Instance Method Details

#capture_element_sequence(element, duration: 3, interval: 0.5, name_prefix: 'sequence') ⇒ Object

Capture element sequence (for animations)



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/appom/visual.rb', line 232

def capture_element_sequence(element, duration: 3, interval: 0.5, name_prefix: 'sequence')
  frames = []
  start_time = Time.now
  frame_count = 0

  while Time.now - start_time < duration
    frame_path = take_element_screenshot(element, "#{name_prefix}_frame_#{frame_count}")
    frames << {
      path: frame_path,
      timestamp: Time.now - start_time,
      frame_number: frame_count,
    }

    frame_count += 1
    sleep interval
  end

  log_info("Captured #{frames.size} frames for element sequence")
  frames
end

#clear_results!Object

Clear all results



293
294
295
296
297
# File 'lib/appom/visual.rb', line 293

def clear_results!
  @comparison_results.clear
  FileUtils.rm_rf(Dir.glob(File.join(@results_dir, '*')))
  log_info('Visual test results cleared')
end

#compare_element_visuals(element, baseline_name, options = {}) ⇒ Object

Compare element visual state



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/appom/visual.rb', line 118

def compare_element_visuals(element, baseline_name, options = {})
  element_screenshot = take_element_screenshot(element)
  baseline_path = File.join(@baseline_dir, "#{baseline_name}_element.png")

  # Verify element screenshot was created
  unless File.exist?(element_screenshot)
    return {
      error: 'Failed to create element screenshot',
      passed: false,
    }
  end

  if File.exist?(baseline_path)
    comparison = compare_images(baseline_path, element_screenshot)

    {
      element: element,
      baseline: baseline_path,
      current: element_screenshot,
      similarity: comparison[:similarity],
      differences: comparison[:differences],
      passed: comparison[:similarity] >= (1.0 - (options[:threshold] || @threshold)),
    }
  else
    begin
      FileUtils.cp(element_screenshot, baseline_path)
      { baseline_created: true, baseline_path: baseline_path }
    rescue StandardError => e
      {
        error: "Failed to create baseline: #{e.message}",
        passed: false,
      }
    end
  end
end

#generate_report(output_file: nil) ⇒ Object

Create visual test report



170
171
172
173
174
175
176
177
178
# File 'lib/appom/visual.rb', line 170

def generate_report(output_file: nil)
  output_file ||= File.join(@results_dir, "visual_test_report_#{Time.now.strftime('%Y%m%d_%H%M%S')}.html")

  html_content = generate_html_report
  File.write(output_file, html_content)

  log_info("Visual test report generated: #{output_file}")
  output_file
end

#highlight_element(element, color: 'red', thickness: 3) ⇒ Object

Highlight element in screenshot



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/appom/visual.rb', line 198

def highlight_element(element, color: 'red', thickness: 3)
  screenshot_path = take_screenshot("temp_highlight_#{Time.now.to_i}.png")

  # Get element location and size
  location = element.location
  size = element.size

  # Handle location and size as hash or object
  x = location.is_a?(Hash) ? location[:x] || location['x'] : location.x
  y = location.is_a?(Hash) ? location[:y] || location['y'] : location.y
  width = size.is_a?(Hash) ? size[:width] || size['width'] : size.width
  height = size.is_a?(Hash) ? size[:height] || size['height'] : size.height

  # Add highlight annotation
  annotations = [{
    type: :rectangle,
    x: x,
    y: y,
    width: width,
    height: height,
    color: color,
    thickness: thickness,
  }]

  highlighted_path = File.join(@results_dir, "highlighted_element_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png")
  annotate_screenshot(screenshot_path, highlighted_path, annotations)

  # Clean up temp file
  FileUtils.rm_f(screenshot_path)

  highlighted_path
end

#log_warning(message) ⇒ Object

Log a warning message



11
12
13
# File 'lib/appom/visual.rb', line 11

def log_warning(message)
  warn "[Appom::Visual][WARNING] #{message}"
end

#results_summaryObject

Get visual test results summary



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/appom/visual.rb', line 181

def results_summary
  return { tests_run: 0, passed: 0, failed: 0 } if @comparison_results.empty?

  passed = @comparison_results.count { |r| r[:passed] }
  failed = @comparison_results.count { |r| !r[:passed] }

  {
    tests_run: @comparison_results.size,
    passed: passed,
    failed: failed,
    pass_rate: (passed.to_f / @comparison_results.size * 100).round(2),
    threshold: @threshold,
    results: @comparison_results,
  }
end

#take_visual_screenshot(name, element: nil, full_page: false, annotations: []) ⇒ Object

Take screenshot with visual context



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/appom/visual.rb', line 101

def take_visual_screenshot(name, element: nil, full_page: false, annotations: [])
  file_path = File.join(@results_dir, "#{name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png")

  # Take base screenshot
  take_screenshot(file_path, element: element, full_page: full_page)

  # Add annotations if provided
  if annotations.any?
    annotated_path = File.join(@results_dir, "#{name}_annotated_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png")
    annotate_screenshot(file_path, annotated_path, annotations)
    file_path = annotated_path
  end

  file_path
end

#update_baselines(test_names = nil) ⇒ Object

Update baselines from current results



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/appom/visual.rb', line 300

def update_baselines(test_names = nil)
  results_to_update = if test_names
                        @comparison_results.select { |r| test_names.include?(r[:test_name]) }
                      else
                        @comparison_results
                      end

  updated_count = 0

  results_to_update.each do |result|
    if File.exist?(result[:current_path])
      FileUtils.cp(result[:current_path], result[:baseline_path])
      updated_count += 1
    end
  end

  log_info("Updated #{updated_count} visual baselines")
  updated_count
end

#visual_diff(image1_path, image2_path, output_path = nil) ⇒ Object

Visual diff between two screenshots



155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/appom/visual.rb', line 155

def visual_diff(image1_path, image2_path, output_path = nil)
  output_path ||= File.join(@results_dir, "diff_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png")

  comparison = compare_images(image1_path, image2_path, output_path)

  {
    image1: image1_path,
    image2: image2_path,
    diff: output_path,
    similarity: comparison[:similarity],
    differences_found: comparison[:similarity] < 1.0,
  }
end

#visual_regression_test(test_name, element: nil, full_page: false, baseline: nil) ⇒ Object

Visual regression test



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/appom/visual.rb', line 27

def visual_regression_test(test_name, element: nil, full_page: false, baseline: nil)
  baseline_path = baseline || File.join(@baseline_dir, "#{test_name}.png")
  current_path = File.join(@results_dir, "#{test_name}_current.png")
  diff_path = File.join(@results_dir, "#{test_name}_diff.png")

  # Take current screenshot
  take_screenshot(current_path, element: element, full_page: full_page)

  # Verify current screenshot was created successfully
  unless File.exist?(current_path)
    log_error("Failed to create current screenshot: #{current_path}")
    return {
      test_name: test_name,
      error: 'Failed to create current screenshot',
      passed: false,
      timestamp: Time.now,
    }
  end

  # Compare with baseline
  if File.exist?(baseline_path)
    comparison = compare_images(baseline_path, current_path, diff_path)

    result = {
      test_name: test_name,
      baseline_path: baseline_path,
      current_path: current_path,
      diff_path: diff_path,
      comparison: comparison,
      passed: comparison[:similarity] >= (1.0 - @threshold),
      timestamp: Time.now,
    }

    @comparison_results << result

    if result[:passed]
      log_info("Visual regression test PASSED: #{test_name} (#{(comparison[:similarity] * 100).round(2)}% similarity)")
    else
      similarity_percent = (comparison[:similarity] * 100).round(2)
      threshold_percent = 100 - (@threshold * 100)
      log_error("Visual regression test FAILED: #{test_name} (#{similarity_percent}% similarity, threshold: #{threshold_percent}%)")
    end

    result
  else
    # Create baseline
    begin
      FileUtils.cp(current_path, baseline_path)
      log_info("Created baseline for visual test: #{test_name}")

      result = {
        test_name: test_name,
        baseline_path: baseline_path,
        current_path: current_path,
        baseline_created: true,
        passed: true,
        timestamp: Time.now,
      }

      @comparison_results << result
      result
    rescue StandardError => e
      log_error("Failed to create baseline: #{e.message}")
      {
        test_name: test_name,
        error: "Failed to create baseline: #{e.message}",
        passed: false,
        timestamp: Time.now,
      }
    end
  end
end

#wait_for_visual_stability(element: nil, duration: 2, check_interval: 0.5, similarity_threshold: 0.99) ⇒ Object

Wait for visual stability



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/appom/visual.rb', line 254

def wait_for_visual_stability(element: nil, duration: 2, check_interval: 0.5, similarity_threshold: 0.99)
  stable_start = nil
  previous_screenshot = nil

  loop do
    current_screenshot = if element
                           take_element_screenshot(element)
                         else
                           take_screenshot("stability_check_#{Time.now.to_i}.png")
                         end

    if previous_screenshot
      comparison = compare_images(previous_screenshot, current_screenshot)

      if comparison[:similarity] >= similarity_threshold
        stable_start ||= Time.now

        if Time.now - stable_start >= duration
          # Clean up temp files
          [previous_screenshot, current_screenshot].each do |file|
            File.delete(file) if File.exist?(file) && file.include?('stability_check')
          end

          return true
        end
      else
        stable_start = nil
      end

      # Clean up old screenshot
      File.delete(previous_screenshot) if File.exist?(previous_screenshot) && previous_screenshot.include?('stability_check')
    end

    previous_screenshot = current_screenshot
    sleep check_interval
  end
end