Class: Capybara::Simulated::Trace

Inherits:
Object
  • Object
show all
Defined in:
lib/capybara/simulated/trace.rb

Overview

Per-test trace of Capybara actions with DOM snapshots, screenshots, console output, and network requests interleaved. JSON output, one file per test — downstream tooling builds whatever viewer it wants.

A screenshot is carried INLINE as a data URL, like everything else here: the viewer's whole point is that it opens from file:// with no server, and a side-file image would need one (or a second artefact to lose).

Off by default. CSIM_TRACE_DIR=/path/to/dir enables auto-mode via Browser#record_action; the RSpec hook in csim_rspec.rb persists with a slugged filename. Programmatic activation is via Driver#start_tracing / #stop_tracing.

Defined Under Namespace

Classes: Step

Constant Summary collapse

VIEWER_TEMPLATE_PATH =
File.expand_path('trace_viewer.html', __dir__)
VIEWER_DATA_TOKEN =
'__CSIM_TRACE_DATA__'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(metadata: {}) ⇒ Trace

Returns a new instance of Trace.



70
71
72
73
74
75
76
77
# File 'lib/capybara/simulated/trace.rb', line 70

def initialize(metadata: {})
  @steps        = []
  @metadata     = 
  @started_at   = monotonic_ms
  @console_buf  = []
  @network_buf  = []
  @open_step    = nil
end

Instance Attribute Details

#metadataObject (readonly)

Returns the value of attribute metadata.



68
69
70
# File 'lib/capybara/simulated/trace.rb', line 68

def 
  @metadata
end

#stepsObject (readonly)

Returns the value of attribute steps.



68
69
70
# File 'lib/capybara/simulated/trace.rb', line 68

def steps
  @steps
end

Class Method Details

.render_viewer(json_text) ⇒ Object

Render the self-contained HTML viewer for a trace JSON string, embedding it inline (the capybara-simulated trace CLI is the caller). The whole point of inline embedding over fetch / import … with { type: 'json' } is that the result opens straight from file:// with no server (module / fetch loads are CORS-blocked for file:// origins).

EVERY < is escaped, not just </. Escaping only the closing form looks sufficient — nothing can close the block early — and is worse than nothing: <!-- puts the HTML tokenizer into script-data-escaped state and a following <script into script-data-DOUBLE-escaped state, where a real </script> no longer closes the element and only </script can leave... which is exactly what the old escaping guaranteed could never appear. A DOM snapshot of a page with a commented-out script tag — <!-- <script src="/analytics.js"></script> -->, which is not an exotic thing for a page to contain — therefore swallowed the rest of the viewer as text and rendered a blank white page, with nothing in the console to say why. \u003c is a legal escape inside a JSON string, < cannot appear anywhere else in JSON, and JSON.parse restores it, so the round trip is exact.



61
62
63
64
65
66
# File 'lib/capybara/simulated/trace.rb', line 61

def self.render_viewer(json_text)
  template = (@viewer_template ||= File.read(VIEWER_TEMPLATE_PATH))
  # Block form: the replacement is taken literally, so backslashes
  # in the JSON aren't interpreted as regexp backreferences.
  template.sub(VIEWER_DATA_TOKEN) { json_text.to_s.gsub('<', '\u003c') }
end

Instance Method Details

#begin_step(kind, description:, url_before: nil) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/capybara/simulated/trace.rb', line 108

def begin_step(kind, description:, url_before: nil)
  finish_step if @open_step
  @open_step = {
    kind:        kind,
    description: description,
    url_before:  url_before,
    start_ms:    monotonic_ms
  }
  @console_buf = []
  @network_buf = []
end

#empty?Boolean

Returns:

  • (Boolean)


150
# File 'lib/capybara/simulated/trace.rb', line 150

def empty? = @steps.empty?

#finish_step(url_after: nil, dom_after: nil, shot_after: nil, error: nil) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/capybara/simulated/trace.rb', line 120

def finish_step(url_after: nil, dom_after: nil, shot_after: nil, error: nil)
  return unless @open_step
  s = @open_step
  @steps << Step.new(
    index:        @steps.size,
    kind:         s[:kind],
    description:  s[:description],
    url_before:   s[:url_before],
    url_after:    url_after,
    dom_after:    dom_after,
    shot_after:   shot_after,
    console:      @console_buf,
    network:      @network_buf,
    elapsed_ms:   (s[:start_ms] - @started_at).round,
    duration_ms:  (monotonic_ms - s[:start_ms]).round,
    error:        error
  )
  @open_step   = nil
  @console_buf = []
  @network_buf = []
end

#log_console(severity, message) ⇒ Object

Pushed from Browser#log_console (the JS bridge's console.* host-fn target) and Browser#rack_request (network). Entries land on the currently-open step; outside a step they're dropped (boot noise, post-test cleanup).



83
84
85
86
# File 'lib/capybara/simulated/trace.rb', line 83

def log_console(severity, message)
  return unless @open_step
  @console_buf << {severity: severity.to_s, message: message.to_s}
end

#log_network(method, url, status, content_type: nil, size: nil, duration_ms: nil, redirected: nil, request_headers: nil, request_body: nil, response_headers: nil, response_body: nil) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/capybara/simulated/trace.rb', line 88

def log_network(method, url, status,
                content_type: nil, size: nil, duration_ms: nil, redirected: nil,
                request_headers: nil, request_body: nil,
                response_headers: nil, response_body: nil)
  return unless @open_step
  @network_buf << {
    method:           method.to_s,
    url:              url.to_s,
    status:           status,
    content_type:     content_type,
    size:             size,
    duration_ms:      duration_ms,
    redirected:       redirected,
    request_headers:  request_headers,
    request_body:     request_body,
    response_headers: response_headers,
    response_body:    response_body
  }.compact  # drop fields the caller couldn't determine, keeping entries lean
end

#retrying_failure?(kind, description) ⇒ Boolean

Is the step just recorded another attempt at the SAME failing action? Capybara retries an action for its whole wait window, and every attempt records a step — so this is what keeps a screenshot from being painted 60 times for one failed click.

Returns:

  • (Boolean)


145
146
147
148
# File 'lib/capybara/simulated/trace.rb', line 145

def retrying_failure?(kind, description)
  last = @steps.last
  !last.nil? && !last.error.nil? && last.kind == kind && last.description == description
end

#to_hObject



152
153
154
# File 'lib/capybara/simulated/trace.rb', line 152

def to_h
  {version: 1, metadata: @metadata, steps: @steps.map(&:to_h)}
end

#to_json(*args) ⇒ Object



156
# File 'lib/capybara/simulated/trace.rb', line 156

def to_json(*args) = JSON.generate(to_h, *args)

#write_json(path) ⇒ Object



158
159
160
161
162
# File 'lib/capybara/simulated/trace.rb', line 158

def write_json(path)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, to_json)
  path
end