Module: Testivai::Settle

Defined in:
lib/testivai/settle.rb

Overview

Page-settled probe: "has this page stopped changing?"

settle.js is GENERATED from packages/witness/src/capture/settle.ts, so every language polls the identical predicate.

Deliberately NOT network idle. Playwright's own docs mark that DISCOURAGED for testing, and it is the wrong signal for a visual snapshot anyway — a page with analytics beacons never goes quiet, while a network-idle page can still be animating. What matters is whether the rendered page settled.

Constant Summary collapse

DEFAULT_QUIET_MS =
150
DEFAULT_TIMEOUT =
5.0
ASSET =
Pathname.new(__dir__).join("settle.js")
STOP_JS =
<<~JS
  try { var s = window.__testivaiSettleState;
    if (s && s.observer && s.observer.disconnect) s.observer.disconnect(); } catch (e) {}
  try { delete window.__testivaiSettleState; } catch (e) {}
JS

Class Method Summary collapse

Class Method Details

.expression(quiet_ms = DEFAULT_QUIET_MS) ⇒ Object



39
40
41
# File 'lib/testivai/settle.rb', line 39

def expression(quiet_ms = DEFAULT_QUIET_MS)
  "return (#{source})(document, window, #{quiet_ms.to_i});"
end

.sourceObject



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/testivai/settle.rb', line 27

def source
  @source ||= begin
    # Explicit UTF-8: the generated asset is non-ASCII and Pathname#read
    # would otherwise use the default external encoding.
    raw = ASSET.read(encoding: "UTF-8")
    start = raw.index("function settleProbe")
    raise "settle.js is malformed: settleProbe not found" if start.nil?

    raw[start..].strip
  end
end

.stop(browser) ⇒ Object

Detach the observer so it does not linger on the page under test.



67
68
69
70
# File 'lib/testivai/settle.rb', line 67

def stop(browser)
  browser.execute_script(STOP_JS)
rescue StandardError # rubocop:disable Lint/SuppressedException
end

.wait_for(browser, quiet_ms: DEFAULT_QUIET_MS, timeout: DEFAULT_TIMEOUT) ⇒ Object

Poll until settled or the deadline passes. Always bounded: a page that never settles yields a capture, not a hang. A browser that cannot evaluate the probe returns something unusable, so we proceed at once rather than paying the whole timeout on every capture.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/testivai/settle.rb', line 47

def wait_for(browser, quiet_ms: DEFAULT_QUIET_MS, timeout: DEFAULT_TIMEOUT)
  expr = expression(quiet_ms)
  deadline = Time.now + timeout
  loop do
    state = begin
      browser.execute_script(expr)
    rescue StandardError
      return
    end
    return unless state.is_a?(Hash)

    settled = state["settled"]
    return unless [true, false].include?(settled)
    return if settled || Time.now >= deadline

    sleep 0.05
  end
end