Module: Panda::Core::Testing::CupriteHelpers

Included in:
SystemTestHelpers
Defined in:
lib/panda/core/testing/support/system/cuprite_helpers.rb

Instance Method Summary collapse

Instance Method Details

#browser_debugObject

Drop #browser_debug anywhere in a test to open a Chrome inspector and pause the execution Usage: browser_debug(binding)



219
220
221
222
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 219

def browser_debug(*)
  # Cuprite-specific debug method
  page.driver.debug
end

#capybara_artifacts_dirObject

Resolve the configured Capybara artifacts directory



22
23
24
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 22

def capybara_artifacts_dir
  Pathname.new(Capybara.save_path || Rails.root.join("tmp/capybara"))
end

#click_on_selectors(*css_selectors) ⇒ Object

Allows sending a list of CSS selectors to be clicked on in the correct order (no delay) Useful where you need to trigger e.g. a blur event on an input field



226
227
228
229
230
231
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 226

def click_on_selectors(*css_selectors)
  css_selectors.each do |selector|
    find(selector).click
    # sleep 0.1 # Add a small delay to allow JavaScript to run
  end
end

#ensure_page_loadedObject

Ensure page is loaded and stable before interacting



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 99

def ensure_page_loaded
  # Check if we're on about:blank and need to reload
  current_url = begin
    page.current_url
  rescue
    "unknown"
  end
  if current_url.include?("about:blank")
    puts "[CI] Page is on about:blank, skipping recovery to avoid loops" if ENV["GITHUB_ACTIONS"]
    # Don't try to recover - let the test handle it
    return false
  end

  # Wait for page to be ready
  wait_for_ready_state
  # Wait for JavaScript to load
  wait_for_javascript
  true
end

#pauseObject

Drop #pause anywhere in a test to stop the execution. Useful when you want to checkout the contents of a web page in the middle of a test running in a headful mode.



212
213
214
215
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 212

def pause
  # Cuprite-specific pause method
  page.driver.pause
end

#record_video!(name = nil, seconds: 3) ⇒ Object

Record a small MP4 video of the test — uses Cuprite’s Chrome DevTools API



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
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 42

def record_video!(name = nil, seconds: 3)
  name ||= example.[:full_description].parameterize
  path = capybara_artifacts_dir.join("#{name}.mp4")

  FileUtils.mkdir_p(File.dirname(path))

  session = page.driver.browser
  client = session.client

  # Enable screencast
  client.command("Page.startScreencast", format: "png", quality: 80, maxWidth: 1280, maxHeight: 800)

  frames = []

  start = Time.now
  while Time.now - start < seconds
    message = client.listen
    if message["method"] == "Page.screencastFrame"
      frames << message["params"]["data"]
      client.command("Page.screencastFrameAck", sessionId: message["params"]["sessionId"])
    end
  end

  # Stop
  client.command("Page.stopScreencast")

  # Convert frames to MP4 using ffmpeg
  Dir.mktmpdir do |dir|
    png_dir = File.join(dir, "frames")
    FileUtils.mkdir_p(png_dir)

    frames.each_with_index do |data, i|
      File.binwrite(File.join(png_dir, "frame-%05d.png" % i), Base64.decode64(data))
    end

    system <<~CMD
      ffmpeg -y -framerate 8 -pattern_type glob -i '#{png_dir}/*.png' -c:v libx264 -pix_fmt yuv420p '#{path}'
    CMD
  end

  puts "🎥 Saved video: #{path}"
  path
rescue => e
  puts "⚠️ Failed to record video: #{e.message}"
  nil
end

#safe_click_button(locator) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 292

def safe_click_button(locator)
  retries = 0
  start_time = Time.now
  max_duration = 5 # Maximum 5 seconds total

  begin
    click_button locator
  rescue Ferrum::NodeNotFoundError, Capybara::ElementNotFound => e
    retries += 1
    elapsed = Time.now - start_time

    if retries <= 2 && elapsed < max_duration && ENV["GITHUB_ACTIONS"]
      puts "[CI] Element not found on click_button '#{locator}', retry #{retries}/2 (#{elapsed.round(1)}s elapsed)"
      # sleep 0.5
      retry
    else
      puts "[CI] Giving up on click_button '#{locator}' after #{retries} retries and #{elapsed.round(1)}s" if ENV["GITHUB_ACTIONS"]
      raise e
    end
  end
end

#safe_fill_in(locator, with:) ⇒ Object

Safe methods that handle Ferrum NodeNotFoundError in CI



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 248

def safe_fill_in(locator, with:)
  retries = 0
  start_time = Time.now
  max_duration = 5 # Maximum 5 seconds total

  begin
    fill_in locator, with: with
  rescue Ferrum::NodeNotFoundError, Capybara::ElementNotFound => e
    retries += 1
    elapsed = Time.now - start_time

    if retries <= 2 && elapsed < max_duration && ENV["GITHUB_ACTIONS"]
      puts "[CI] Element not found on fill_in '#{locator}', retry #{retries}/2 (#{elapsed.round(1)}s elapsed)"
      # sleep 0.5
      retry
    else
      puts "[CI] Giving up on fill_in '#{locator}' after #{retries} retries and #{elapsed.round(1)}s" if ENV["GITHUB_ACTIONS"]
      raise e
    end
  end
end

#safe_select(value, from:) ⇒ Object



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 270

def safe_select(value, from:)
  retries = 0
  start_time = Time.now
  max_duration = 5 # Maximum 5 seconds total

  begin
    select value, from: from
  rescue Ferrum::NodeNotFoundError, Capybara::ElementNotFound => e
    retries += 1
    elapsed = Time.now - start_time

    if retries <= 2 && elapsed < max_duration && ENV["GITHUB_ACTIONS"]
      puts "[CI] Element not found on select '#{value}' from '#{from}', retry #{retries}/2 (#{elapsed.round(1)}s elapsed)"
      # sleep 0.5
      retry
    else
      puts "[CI] Giving up on select '#{value}' from '#{from}' after #{retries} retries and #{elapsed.round(1)}s" if ENV["GITHUB_ACTIONS"]
      raise e
    end
  end
end

#save_html!(name = nil) ⇒ Object



89
90
91
92
93
94
95
96
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 89

def save_html!(name = nil)
  name ||= example.[:full_description].parameterize
  path = capybara_artifacts_dir.join("#{name}.html")
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, page.html)
  puts "📝 Saved HTML snapshot: #{path}"
  path
end

#save_screenshot!(name = nil) ⇒ Object

Save a PNG screenshot for the current page.



28
29
30
31
32
33
34
35
36
37
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 28

def save_screenshot!(name = nil)
  name ||= example.[:full_description].parameterize
  path = capybara_artifacts_dir.join("#{name}.png")

  FileUtils.mkdir_p(File.dirname(path))
  page.save_screenshot(path, full: true) # rubocop:disable Lint/Debugger
  puts "📸 Saved screenshot: #{path}"

  path
end

#wait_for_dom_mutation(timeout: 5) ⇒ Boolean

Waits for JavaScript to modify the DOM

Parameters:

  • timeout (Integer) (defaults to: 5)

    Maximum time to wait in seconds (default: 5)

Returns:

  • (Boolean)

    true if mutation occurred, false if timeout occurs



198
199
200
201
202
203
204
205
206
207
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 198

def wait_for_dom_mutation(timeout: 5)
  start_time = Time.now
  initial_dom = page.html
  while Time.now - start_time < timeout
    return true if page.html != initial_dom

    # sleep 0.1
  end
  false
end

#wait_for_field_value(field_name, value, timeout: 5) ⇒ Object

Wait for a field to have a specific value

Parameters:

  • field_name (String)

    The field name or label

  • value (String)

    The expected value

  • timeout (Integer) (defaults to: 5)

    Maximum time to wait in seconds (default: 5)



237
238
239
240
241
242
243
244
245
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 237

def wait_for_field_value(field_name, value, timeout: 5)
  start_time = Time.now
  while Time.now - start_time < timeout
    return true if page.has_field?(field_name, with: value)

    # sleep 0.1
  end
  false
end

#wait_for_javascript(timeout: 5) ⇒ Object

Wait for JavaScript to load (application-specific flag) Override in your application if you have a custom loaded flag



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 135

def wait_for_javascript(timeout: 5)
  Timeout.timeout(timeout) do
    loop do
      loaded = begin
        # Check for common JavaScript loaded indicators
        page.evaluate_script("document.readyState === 'complete'")
      rescue
        false
      end
      break if loaded

      # sleep 0.1
    end
  end
  true
rescue Timeout::Error
  puts "[CI] Timeout waiting for JavaScript to load" if ENV["GITHUB_ACTIONS"]
  false
end

#wait_for_network_idle(timeout: 5) ⇒ Boolean

Waits for network requests to complete

Parameters:

  • timeout (Integer) (defaults to: 5)

    Maximum time to wait in seconds (default: 5)

Returns:

  • (Boolean)

    true if network is idle, false if timeout occurs



186
187
188
189
190
191
192
193
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 186

def wait_for_network_idle(timeout: 5)
  # Cuprite has direct network idle support
  page.driver.wait_for_network_idle(timeout: timeout)
  true
rescue => e
  puts "[CI] Network idle timeout: #{e.message}" if ENV["GITHUB_ACTIONS"]
  false
end

#wait_for_ready_stateObject

Wait for document ready state



120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 120

def wait_for_ready_state
  Timeout.timeout(5) do
    loop do
      ready = page.evaluate_script("document.readyState")
      break if ready == "complete"

      # sleep 0.1
    end
  end
rescue Timeout::Error
  puts "[CI] Timeout waiting for document ready state" if ENV["GITHUB_ACTIONS"]
end

#wait_for_selector(selector, timeout: 5) ⇒ Boolean

Waits for a specific selector to be present and visible on the page

Parameters:

  • selector (String)

    CSS selector to wait for

  • timeout (Integer) (defaults to: 5)

    Maximum time to wait in seconds (default: 5)

Returns:

  • (Boolean)

    true if element is found, false if timeout occurs



159
160
161
162
163
164
165
166
167
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 159

def wait_for_selector(selector, timeout: 5)
  start_time = Time.now
  while Time.now - start_time < timeout
    return true if page.has_css?(selector, visible: true)

    # sleep 0.1
  end
  false
end

#wait_for_text(text, timeout: 5) ⇒ Boolean

Waits for a specific text to be present on the page

Parameters:

  • text (String)

    Text to wait for

  • timeout (Integer) (defaults to: 5)

    Maximum time to wait in seconds (default: 5)

Returns:

  • (Boolean)

    true if text is found, false if timeout occurs



173
174
175
176
177
178
179
180
181
# File 'lib/panda/core/testing/support/system/cuprite_helpers.rb', line 173

def wait_for_text(text, timeout: 5)
  start_time = Time.now
  while Time.now - start_time < timeout
    return true if page.has_text?(text)

    # sleep 0.1
  end
  false
end