Class: Capybara::Lightpanda::Downloads
- Inherits:
-
Object
- Object
- Capybara::Lightpanda::Downloads
- Defined in:
- lib/capybara/lightpanda/downloads.rb
Overview
File-download tracker built on Browser.setDownloadBehavior (upstream PR
#2722, build >= 7545, guaranteed by MINIMUM_NIGHTLY_BUILD). When a
navigation response carries Content-Disposition: attachment, Lightpanda
streams the body to disk under downloadPath and — with
eventsEnabled: true — emits Browser.downloadWillBegin /
Browser.downloadProgress. We mirror those into a completed-files list +
a blocking wait, the way ferrum's Downloads does.
IMPORTANT: the trigger is Content-Disposition: attachment, NOT MIME
type. A text/csv (or any) response without that header is rendered as a
normal navigation, not downloaded — which is why Capybara's MIME-triggered
:download shared spec stays in capybara_skip.
Structure deliberately mirrors Network (same browser ref, same mutex + subscribe/unsubscribe lifecycle, same reset-after-disposeBrowserContext contract) so the two trackers behave identically under create_page/reset.
Constant Summary collapse
- DOWNLOAD_BEGIN_GRACE =
How long #wait gives a download to BEGIN (the downloadWillBegin frame can arrive a beat after the click that triggers it, once the click's own wait_for_idle has already returned). Once a download has begun we wait the full
timeoutfor it to finish. 1.0
Instance Attribute Summary collapse
-
#browser ⇒ Object
readonly
Returns the value of attribute browser.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Instance Method Summary collapse
- #clear ⇒ Object
-
#enable(save_path) ⇒ Object
Opt into downloads, writing completed files under
save_path. -
#files ⇒ Object
Absolute paths of downloads completed this session (newest last).
- #in_progress? ⇒ Boolean
-
#initialize(browser) ⇒ Downloads
constructor
A new instance of Downloads.
-
#reset ⇒ Object
Wipe local state after Target.disposeBrowserContext, which drops both the subscriptions and the per-connection download config — leaving.
-
#started_count ⇒ Object
Monotonic count of downloads that have begun this session.
-
#wait(timeout: 5) ⇒ Object
Block until in-flight downloads finish, then return the completed-file list.
Constructor Details
#initialize(browser) ⇒ Downloads
Returns a new instance of Downloads.
32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/capybara/lightpanda/downloads.rb', line 32 def initialize(browser) @browser = browser @path = nil @enabled = false @mutex = Mutex.new @pending = {} # guid => on-disk basename (from suggestedFilename) @files = [] # absolute paths of completed downloads, in order @started = 0 # monotonic count of downloads begun this session @will_handler = nil @progress_handler = nil end |
Instance Attribute Details
#browser ⇒ Object (readonly)
Returns the value of attribute browser.
24 25 26 |
# File 'lib/capybara/lightpanda/downloads.rb', line 24 def browser @browser end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
24 25 26 |
# File 'lib/capybara/lightpanda/downloads.rb', line 24 def path @path end |
Instance Method Details
#clear ⇒ Object
108 109 110 111 112 113 114 |
# File 'lib/capybara/lightpanda/downloads.rb', line 108 def clear @mutex.synchronize do @files.clear @pending.clear @started = 0 end end |
#enable(save_path) ⇒ Object
Opt into downloads, writing completed files under save_path. No-op
when save_path is blank — downloads then stay at Lightpanda's deny
default. Connection-scoped (browser.command) like Network.enable;
Browser#create_page calls this after the context is loaded (the CDP
method is a no-op without one). Subscribe BEFORE the wire toggle so no
event can slip past, and roll the handlers back if the command fails.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/capybara/lightpanda/downloads.rb', line 50 def enable(save_path) return if @enabled || save_path.to_s.empty? @path = File.(save_path.to_s) FileUtils.mkdir_p(@path) subscribe begin browser.command("Browser.setDownloadBehavior", behavior: "allow", downloadPath: @path, eventsEnabled: true) rescue StandardError unsubscribe @path = nil raise end @enabled = true end |
#files ⇒ Object
Absolute paths of downloads completed this session (newest last).
68 69 70 |
# File 'lib/capybara/lightpanda/downloads.rb', line 68 def files @mutex.synchronize { @files.dup } end |
#in_progress? ⇒ Boolean
79 80 81 |
# File 'lib/capybara/lightpanda/downloads.rb', line 79 def in_progress? @mutex.synchronize { @pending.any? } end |
#reset ⇒ Object
Wipe local state after Target.disposeBrowserContext, which drops both the subscriptions and the per-connection download config — leaving
119 120 121 122 123 124 |
# File 'lib/capybara/lightpanda/downloads.rb', line 119 def reset unsubscribe clear @enabled = false @path = nil end |
#started_count ⇒ Object
Monotonic count of downloads that have begun this session. Used by #wait to detect "a download started since I was called" even after the event queue has drained and @pending is empty again.
75 76 77 |
# File 'lib/capybara/lightpanda/downloads.rb', line 75 def started_count @mutex.synchronize { @started } end |
#wait(timeout: 5) ⇒ Object
Block until in-flight downloads finish, then return the completed-file list. Non-raising, matching Network#wait_for_idle.
The triggering click returns (its wait_for_idle settles on the
navigation's network response) slightly BEFORE the downloadWillBegin
frame is processed, so a naive wait until !in_progress? would see an
empty queue and return before the download even registered. Instead:
give a download up to DOWNLOAD_BEGIN_GRACE to begin (tracked by the
monotonic @started counter, immune to the queue already having drained),
then wait up to timeout for every in-flight download to finish.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/capybara/lightpanda/downloads.rb', line 93 def wait(timeout: 5) baseline = started_count clock = -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) } hard_deadline = clock.call + timeout begin_deadline = clock.call + [timeout, DOWNLOAD_BEGIN_GRACE].min loop do began = started_count > baseline done = (began ? !in_progress? : clock.call >= begin_deadline) || clock.call >= hard_deadline return files if done sleep 0.02 end end |