Class: Browserctl::WorkflowContext

Inherits:
Object
  • Object
show all
Defined in:
lib/browserctl/workflow.rb

Constant Summary collapse

DEPRECATED_LOAD_SESSION_FALLBACK =
<<~MSG
  [browserctl] DEPRECATION: `load_session(name, fallback:, expired_if:)` is superseded by
  `load_state(name)` with a flow-bound bundle (`save_state(name, flow: :name)`).
  `load_session` will be removed in v0.12. See docs/concepts/state.md.
MSG

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params, client, replay_context: nil) ⇒ WorkflowContext

Returns a new instance of WorkflowContext.



76
77
78
79
80
# File 'lib/browserctl/workflow.rb', line 76

def initialize(params, client, replay_context: nil)
  @params = params
  @client = client
  @replay_context = replay_context
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



96
97
98
99
100
# File 'lib/browserctl/workflow.rb', line 96

def method_missing(name, *args)
  return @params[name] if @params.key?(name)

  super
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



74
75
76
# File 'lib/browserctl/workflow.rb', line 74

def client
  @client
end

#paramsObject (readonly)

Returns the value of attribute params.



74
75
76
# File 'lib/browserctl/workflow.rb', line 74

def params
  @params
end

#replay_contextObject (readonly)

Returns the value of attribute replay_context.



74
75
76
# File 'lib/browserctl/workflow.rb', line 74

def replay_context
  @replay_context
end

Instance Method Details

#ask(prompt) ⇒ Object



184
185
186
187
# File 'lib/browserctl/workflow.rb', line 184

def ask(prompt)
  $stderr.print("[browserctl] #{prompt} ")
  $stdin.gets.chomp
end

#assert(condition, msg = "assertion failed") ⇒ Object

Raises:



201
202
203
# File 'lib/browserctl/workflow.rb', line 201

def assert(condition, msg = "assertion failed")
  raise WorkflowError, msg unless condition
end

#assert_snapshot_stable(page_name, expected_digest:) ⇒ Object

Snapshots the named page and compares its digest against ‘expected_digest`. Under `workflow run –check` (a replay context is attached), a mismatch is recorded as a drift event with reason “post-snapshot mismatch” and the step still passes. Outside –check, mismatch raises WorkflowError so the workflow fails fast.

Raises:



210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/browserctl/workflow.rb', line 210

def assert_snapshot_stable(page_name, expected_digest:)
  res = @client.snapshot(page_name.to_s, format: "elements")
  snapshot = res[:snapshot]
  actual = Replay::SnapshotDiff.digest(snapshot)
  return if actual == expected_digest

  msg = "post-snapshot mismatch on :#{page_name} — expected #{expected_digest}, got #{actual}"
  raise WorkflowError, msg unless @replay_context

  @replay_context.record(command: :assert_snapshot_stable, selector: page_name.to_s,
                         matched_ref: nil, score: nil, reason: "post-snapshot mismatch")
  warn "[browserctl replay] #{msg}"
end

#close_page(page_name) ⇒ Object

Raises:



117
118
119
120
121
122
# File 'lib/browserctl/workflow.rb', line 117

def close_page(page_name)
  res = @client.page_close(page_name.to_s)
  raise WorkflowError, res[:error] if res[:error]

  res
end

#composeObject

Raises:



224
225
226
227
228
# File 'lib/browserctl/workflow.rb', line 224

def compose(*)
  raise WorkflowError,
        "`compose` must be called at the workflow definition level, not inside a step block. " \
        "Did you mean `invoke`?"
end

#fetch(key) ⇒ Object

Raises:



89
90
91
92
93
94
# File 'lib/browserctl/workflow.rb', line 89

def fetch(key)
  res = @client.fetch(key.to_s)
  raise WorkflowError, res[:error] if res[:error]

  res[:value]
end

#invoke(target_name, page: nil, **override_params) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/browserctl/workflow.rb', line 189

def invoke(target_name, page: nil, **override_params)
  name = target_name.to_s
  guard_circular!(name)

  flow = lookup_flow_target(name)
  if flow
    track_invoke(name) { run_invoked_flow(flow, page_name: page, **override_params) }
  else
    track_invoke(name) { run_nested(target_name, **override_params) }
  end
end

#list_sessionsObject



180
181
182
# File 'lib/browserctl/workflow.rb', line 180

def list_sessions
  @client.session_list[:sessions]
end

#load_session(session_name, fallback: nil, expired_if: nil) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/browserctl/workflow.rb', line 162

def load_session(session_name, fallback: nil, expired_if: nil)
  warn DEPRECATED_LOAD_SESSION_FALLBACK if fallback || expired_if
  validate_expired_if!(expired_if)
  fallback_name = fallback&.to_s
  res = @client.session_load(session_name)

  if res[:error]
    raise WorkflowError, res[:error] unless fallback_name

    invoke(fallback_name)
    return load_after_fallback(session_name, fallback_name)
  end

  return res if expired_if.nil? || !call_expired_if(expired_if, session_name)

  recover_expired_session(session_name, fallback_name, expired_if)
end

#load_state(name, on_auth_required: nil) ⇒ Object

Restores a .bctl bundle. When the daemon detects AUTH_REQUIRED before applying (e.g. expired cookies in the payload), this rotates the bound flow and retries — no caller code change required.

Parameters:

  • on_auth_required (Proc, nil) (defaults to: nil)

    override the auto-rotate path. The block runs in the workflow context, in lieu of invoking the manifest’s bound flow. Use this when the recovery procedure is bespoke.



150
151
152
153
154
155
# File 'lib/browserctl/workflow.rb', line 150

def load_state(name, on_auth_required: nil)
  res = @client.state_load(name.to_s)
  return res unless auth_required_response?(res)

  recover_auth_required_state(name.to_s, res, on_auth_required)
end

#open_page(page_name, url: nil) ⇒ Object

Raises:



110
111
112
113
114
115
# File 'lib/browserctl/workflow.rb', line 110

def open_page(page_name, url: nil)
  res = @client.page_open(page_name.to_s, url: url)
  raise WorkflowError, res[:error] if res[:error]

  res
end

#page(name) ⇒ Object



106
107
108
# File 'lib/browserctl/workflow.rb', line 106

def page(name)
  PageProxy.new(name.to_s, @client, replay_context: @replay_context)
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
# File 'lib/browserctl/workflow.rb', line 102

def respond_to_missing?(name, include_private = false)
  @params.key?(name) || super
end

#save_session(session_name, encrypt: false) ⇒ Object

Raises:



124
125
126
127
128
129
# File 'lib/browserctl/workflow.rb', line 124

def save_session(session_name, encrypt: false)
  res = @client.session_save(session_name, encrypt: encrypt)
  raise WorkflowError, res[:error] if res[:error]

  res
end

#save_state(name, flow: nil, origins: nil, encrypt: false) ⇒ Object

Persists the daemon’s current cookies + storage as a .bctl bundle. Optional flow binding lets ‘load_state` auto-rotate when the bundle is detected as needing authentication.

Raises:



134
135
136
137
138
139
140
141
# File 'lib/browserctl/workflow.rb', line 134

def save_state(name, flow: nil, origins: nil, encrypt: false)
  passphrase = encrypt ? ENV.fetch("BROWSERCTL_STATE_PASSPHRASE", nil) : nil
  res = @client.state_save(name.to_s,
                           flow: flow&.to_s, origins: origins, passphrase: passphrase)
  raise WorkflowError, res[:error] if res[:error]

  res
end

#store(key, value) ⇒ Object

Raises:



82
83
84
85
86
87
# File 'lib/browserctl/workflow.rb', line 82

def store(key, value)
  res = @client.store(key.to_s, value)
  raise WorkflowError, res[:error] if res[:error]

  value
end