Class: Capybara::Lightpanda::Downloads

Inherits:
Object
  • Object
show all
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 timeout for it to finish.

1.0

Instance Attribute Summary collapse

Instance Method Summary collapse

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

#browserObject (readonly)

Returns the value of attribute browser.



24
25
26
# File 'lib/capybara/lightpanda/downloads.rb', line 24

def browser
  @browser
end

#pathObject (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

#clearObject



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.expand_path(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

#filesObject

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

Returns:

  • (Boolean)


79
80
81
# File 'lib/capybara/lightpanda/downloads.rb', line 79

def in_progress?
  @mutex.synchronize { @pending.any? }
end

#resetObject

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_countObject

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