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.



21
22
23
24
25
# File 'lib/browserctl/workflow.rb', line 21

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



41
42
43
44
45
# File 'lib/browserctl/workflow.rb', line 41

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.



19
20
21
# File 'lib/browserctl/workflow.rb', line 19

def client
  @client
end

#paramsObject (readonly)

Returns the value of attribute params.



19
20
21
# File 'lib/browserctl/workflow.rb', line 19

def params
  @params
end

#replay_contextObject (readonly)

Returns the value of attribute replay_context.



19
20
21
# File 'lib/browserctl/workflow.rb', line 19

def replay_context
  @replay_context
end

Instance Method Details

#ask(prompt) ⇒ Object



129
130
131
132
# File 'lib/browserctl/workflow.rb', line 129

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

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

Raises:



146
147
148
# File 'lib/browserctl/workflow.rb', line 146

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:



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

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:



62
63
64
65
66
67
# File 'lib/browserctl/workflow.rb', line 62

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

  res
end

#composeObject

Raises:



169
170
171
172
173
# File 'lib/browserctl/workflow.rb', line 169

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:



34
35
36
37
38
39
# File 'lib/browserctl/workflow.rb', line 34

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



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

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



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

def list_sessions
  @client.session_list[:sessions]
end

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



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/browserctl/workflow.rb', line 107

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.



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

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:



55
56
57
58
59
60
# File 'lib/browserctl/workflow.rb', line 55

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



51
52
53
# File 'lib/browserctl/workflow.rb', line 51

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

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

Returns:

  • (Boolean)


47
48
49
# File 'lib/browserctl/workflow.rb', line 47

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

#save_session(session_name, encrypt: false) ⇒ Object

Raises:



69
70
71
72
73
74
# File 'lib/browserctl/workflow.rb', line 69

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:



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

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:



27
28
29
30
31
32
# File 'lib/browserctl/workflow.rb', line 27

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

  value
end