bidi2pdf 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d71c88a5941411b13770993de9b38f6321c263765ffce1a9bbd347fb960855ac
4
- data.tar.gz: aa64333d4dc4de54f6e1b627287a5d11661634f3244a8e3a213428c243f155f1
3
+ metadata.gz: 3d7fa3c853f53e21a110cadb25fcd66bf97b890eb00663f90e73e8ee9d1ce07e
4
+ data.tar.gz: 5900568c47f526e9b00d95a15f8293abeaee88f6c0f323bc6e79cdcb3aba38ef
5
5
  SHA512:
6
- metadata.gz: 1d598fe002552f46e53f803f46577adceeeb087b377a40b486d5d2ef7bf713463f429aa26b2687fb7d0b865d73aacf3262be71e17db154794ac82e1e4a245986
7
- data.tar.gz: 3b7cb02b0e857e551c720a665ac31d3669a9a27e8c9e3e5c1cdc497517b8fbcd3e917d6b0735113e3b956b23ded042b44c72bfff637cfdbf2431642bd98aaa2b
6
+ metadata.gz: 9ecffa81a6358c413dd24b1cef48c3b2282518f5934710558b2ea834a362b198d5a1e3f01a2b3eb8ed163dedffaae61fe2bb0c5f3edec5e3850ad375daf10304
7
+ data.tar.gz: 0b9d4b19f9d2d01a8babfe2a13c4256b9f93d34f2fcdc1ea2942fb780ce4ad02962050f1adffd9d2c5731e2e6c6d1dd83deaa3d31b348157197c982701a8995b
data/CHANGELOG.md CHANGED
@@ -11,6 +11,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  <!-- generated by git-cliff end -->
13
13
 
14
+ ## [0.1.9] - 2025-05-04
15
+
16
+ ### 🎨 Refactored
17
+
18
+ - Enhance reader thread management in Chromedriver by @dieter-medium
19
+ - Improve event listener logging by @dieter-medium
20
+ - Improve event listener management by @dieter-medium
21
+ - Centralize chromedriver test helpers for reuse in other projects by @dieter-medium
22
+
23
+ ### 🐛 Fixed
24
+
25
+ - Close event socket during session cleanup by @dieter-medium
26
+ - Add test for generating PDFs in parallel by @dieter-medium
27
+
28
+ ### 💄 Style
29
+
30
+ - Update Bootstrap stylesheet link by @dieter-medium
31
+
32
+ ### 🔧 Build
33
+
34
+ - Update Ruby version in action.yml by @dieter-medium
35
+ - Update Dockerfile.chromedriver and chromedriver.yml for enhancements by @dieter-medium
36
+ - Update Dockerfile.chromedriver for improved environment setup and vnc support by @dieter-medium
37
+
38
+ ### 🚀 Added
39
+
40
+ - Enhance testcontainers with shared network support by @dieter-medium
41
+
14
42
  ## [0.1.8] - 2025-04-22
15
43
 
16
44
  ### 🎨 Refactored
@@ -173,7 +201,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
173
201
  - Initial release
174
202
 
175
203
 
176
- - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..HEAD)
204
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.9..HEAD)
205
+ - [0.1.9](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..v0.1.9)
177
206
  - [0.1.8](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.7..v0.1.8)
178
207
  - [0.1.7](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.6..v0.1.7)
179
208
  - [0.1.6](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.5..v0.1.6)
@@ -1,13 +1,16 @@
1
- FROM ruby:3.3
1
+ FROM debian:bookworm-slim
2
2
 
3
3
  ARG CHROMEDRIVER_PORT=3000
4
4
 
5
5
  ENV DEBIAN_FRONTEND=noninteractive
6
+ ENV LANG=en_US.UTF-8
6
7
 
7
8
  # Install dependencies
8
- RUN apt-get update && apt-get upgrade -y && \
9
+ RUN echo "deb http://deb.debian.org/debian bookworm contrib non-free" > /etc/apt/sources.list.d/contrib.list &&\
10
+ echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections &&\
11
+ apt-get update && apt-get upgrade -y && \
9
12
  apt-get install -y --no-install-recommends\
10
- chromium chromium-driver\
13
+ chromium chromium-driver chromium-l10n chromium-sandbox\
11
14
  libglib2.0-0 \
12
15
  libnss3 \
13
16
  libxss1 \
@@ -18,10 +21,21 @@ RUN apt-get update && apt-get upgrade -y && \
18
21
  curl \
19
22
  unzip \
20
23
  xvfb \
24
+ x11vnc \
25
+ fluxbox \
26
+ xterm \
27
+ wmctrl \
28
+ net-tools xauth \
29
+ fonts-liberation fonts-dejavu-core fonts-noto-core fonts-noto-cjk fonts-noto-color-emoji fonts-symbola fontconfig ttf-mscorefonts-installer\
30
+ libnss3 libatk1.0-0 \
31
+ libx11-6 libxss1 libgtk-3-0 libgbm1 \
32
+ locales sed \
33
+ && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
34
+ && locale-gen en_US.UTF-8 \
21
35
  && rm -rf /var/lib/apt/lists/*
22
36
 
23
37
  # Create a non-root user
24
- RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
38
+ RUN groupadd -r appuser && useradd -r -g appuser -G audio,video -m -d /home/appuser appuser
25
39
 
26
40
  COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
27
41
  RUN chmod +x /usr/local/bin/entrypoint.sh
@@ -36,12 +50,16 @@ RUN mkdir -p /home/appuser/.webdrivers && ln -s /usr/bin/chromedriver /home/appu
36
50
  # Set working directory
37
51
  WORKDIR /app
38
52
 
53
+ RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
54
+
39
55
  # Switch to non-root user
40
56
  USER appuser
41
57
 
42
- RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
58
+ # RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
43
59
 
44
60
  ENV CHROMEDRIVER_PORT=${CHROMEDRIVER_PORT}
45
61
  EXPOSE ${CHROMEDRIVER_PORT}
62
+ # VNC
63
+ EXPOSE 5900
46
64
 
47
65
  CMD ["/usr/local/bin/entrypoint.sh"]
data/docker/entrypoint.sh CHANGED
@@ -3,6 +3,47 @@
3
3
  USER_DATA_DIR=/home/appuser/.cache
4
4
  mkdir -p ${USER_DATA_DIR}
5
5
 
6
+ if [ "$ENABLE_XVFB" = "true" ]; then
7
+ rm -rf /tmp/.X99-lock
8
+
9
+ export DISPLAY=:99
10
+ Xvfb :99 -screen 0 1920x1080x24 &
11
+
12
+ old_umask=$(umask)
13
+ umask 077
14
+
15
+ touch /home/appuser/.Xauthority
16
+ export XAUTHORITY=/home/appuser/.Xauthority
17
+
18
+ xauth generate :99 . trusted
19
+
20
+ umask $old_umask
21
+
22
+
23
+
24
+ until xdpyinfo -display ${DISPLAY} >/dev/null 2>&1; do
25
+ sleep 0.2
26
+ done
27
+
28
+ fluxbox &
29
+
30
+ until wmctrl -m > /dev/null 2>&1; do
31
+ sleep 0.2
32
+ done
33
+ fi
34
+
35
+ if [ "$ENABLE_VNC" = "true" ]; then
36
+ VNC_PASS=${VNC_PASS:-$(tr -dc A-Za-z0-9 </dev/urandom | head -c 12)}
37
+ echo "VNC password: $VNC_PASS"
38
+ old_umask=$(umask)
39
+ umask 077
40
+ mkdir -p /home/appuser/.vnc
41
+ x11vnc -storepasswd $VNC_PASS /home/appuser/.vnc/passwd
42
+ umask $old_umask
43
+ x11vnc -display WAIT:99 -xkb -noxrecord -noxfixes -noxdamage -forever -shared -noshm -usepw -rfbauth /home/appuser/.vnc/passwd &
44
+ fi
45
+
46
+ # DISPLAY=:99 /home/appuser/.webdrivers/chromedriver --port=33259 --whitelisted-ips="" --allowed-origins="*" --disable-dev-shm-usage --disable-gpu --verbose
6
47
  /home/appuser/.webdrivers/chromedriver --port=${CHROMEDRIVER_PORT} \
7
48
  --headless \
8
49
  --whitelisted-ips="" \
@@ -141,7 +141,7 @@ module Bidi2pdf
141
141
  headers:,
142
142
  url_patterns:
143
143
  )
144
- AddHeadersInterceptor.new(
144
+ @header_interceptor = AddHeadersInterceptor.new(
145
145
  context: browsing_context_id,
146
146
  url_patterns: url_patterns,
147
147
  headers: headers
@@ -155,7 +155,7 @@ module Bidi2pdf
155
155
  # @param [Array<String>] url_patterns The URL patterns to match.
156
156
  # @return [AuthInterceptor] The interceptor instance.
157
157
  def basic_auth(username:, password:, url_patterns:)
158
- AuthInterceptor.new(
158
+ @basic_auth_interceptor = AuthInterceptor.new(
159
159
  context: browsing_context_id,
160
160
  url_patterns: url_patterns,
161
161
  username: username, password: password
@@ -430,14 +430,14 @@ module Bidi2pdf
430
430
  return if @event_handlers_registered
431
431
 
432
432
  @event_handlers_registered = true
433
+ @listener_refs ||= {}
433
434
 
434
- client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
435
- &network_events.method(:handle_event))
435
+ @listener_refs[:network] = client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
436
+ &network_events.method(:handle_event))
436
437
 
437
- client.on_event("log.entryAdded",
438
- &logger_events.method(:handle_event))
438
+ @listener_refs[:logger] = client.on_event("log.entryAdded", &logger_events.method(:handle_event))
439
439
 
440
- client.on_event("browsingContext.navigationFailed", &navigation_failed_events.method(:handle_event))
440
+ @listener_refs[:navigation_failed] = client.on_event("browsingContext.navigationFailed", &navigation_failed_events.method(:handle_event))
441
441
  end
442
442
 
443
443
  def handle_injection_exception(response, url, exception_class)
@@ -568,16 +568,27 @@ module Bidi2pdf
568
568
  end
569
569
 
570
570
  # Removes event listeners for the browser tab.
571
+ # rubocop:disable Metrics/AbcSize
571
572
  def remove_event_listeners
573
+ return if @listener_refs.nil? || @listener_refs.empty?
574
+
572
575
  Bidi2pdf.logger.debug2 "Network events: #{network_events.all_events.map(&:to_s)}"
573
576
 
574
- client.remove_event_listener "network.responseStarted", "network.responseCompleted", "network.fetchError",
575
- &network_events.method(:handle_event)
577
+ client.remove_event_listener("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
578
+ @listener_refs[:network])
579
+
580
+ client.remove_event_listener("log.entryAdded", @listener_refs[:logger])
576
581
 
577
- client.remove_event_listener("log.entryAdded",
578
- &logger_events.method(:handle_event))
582
+ @header_interceptor&.unregister_with_client(client: client)
583
+ @basic_auth_interceptor&.unregister_with_client(client: client)
584
+
585
+ @header_interceptor = nil
586
+ @basic_auth_interceptor = nil
587
+ @listener_refs = {}
579
588
  end
580
589
 
590
+ # rubocop:enable Metrics/AbcSize
591
+
581
592
  # Closes all tabs associated with the browser tab.
582
593
  def close_tabs
583
594
  tabs.each do |tab|
@@ -125,23 +125,25 @@ module Bidi2pdf
125
125
  #
126
126
  # @param [Array<String>] names The names of the events to subscribe to.
127
127
  # @yield [event_data] A block to handle the event data.
128
- def on_event(*names, &block)
129
- names.each { |name| dispatcher.on_event(name, &block) }
128
+ def on_event(*names, &)
129
+ listener = dispatcher.on_event(*names, &)
130
130
  cmd = Bidi2pdf::Bidi::Commands::SessionSubscribe.new(events: names)
131
131
  send_cmd(cmd) if names.any?
132
+
133
+ listener
132
134
  end
133
135
 
134
136
  # Removes a message listener.
135
137
  #
136
138
  # @param [Proc] block The listener block to remove.
137
- def remove_message_listener(block) = dispatcher.remove_message_listener(block)
139
+ def remove_message_listener(listener) = dispatcher.remove_message_listener(listener)
138
140
 
139
141
  # Removes event listeners for specific events.
140
142
  #
141
143
  # @param [Array<String>] names The names of the events to unsubscribe from.
142
144
  # @param [Proc] block The listener block to remove.
143
- def remove_event_listener(*names, &block)
144
- names.each { |event_name| dispatcher.remove_event_listener(event_name, block) }
145
+ def remove_event_listener(*names, listener)
146
+ names.each { |event_name| dispatcher.remove_event_listener(event_name, listener) }
145
147
  end
146
148
 
147
149
  # Closes the WebSocket connection.
@@ -3,6 +3,27 @@
3
3
  module Bidi2pdf
4
4
  module Bidi
5
5
  class EventManager
6
+ Listener = Struct.new(:block, :id, :source_location) do
7
+ def initialize(block, id = SecureRandom.uuid)
8
+ super
9
+ self.source_location = block.source_location
10
+ end
11
+
12
+ def call(*args)
13
+ block.call(*args)
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(Listener) && id == other.id
18
+ end
19
+
20
+ alias_method :eql?, :==
21
+
22
+ def hash
23
+ id.hash
24
+ end
25
+ end
26
+
6
27
  attr_reader :type
7
28
 
8
29
  def initialize(type)
@@ -11,12 +32,21 @@ module Bidi2pdf
11
32
  end
12
33
 
13
34
  def on(*event_names, &block)
14
- event_names.each { |event_name| @listeners[event_name.to_sym] << block }
15
-
16
- block
35
+ Listener.new(block).tap do |listener|
36
+ event_names.each do |event_name|
37
+ @listeners[event_name.to_sym] << listener
38
+ log_msg("Adding #{event_name} listener", listener)
39
+ end
40
+ end
17
41
  end
18
42
 
19
- def off(event_name, block) = @listeners[event_name.to_sym].delete(block)
43
+ def off(event_name, listener)
44
+ raise ArgumentError, "Listener not registered" unless listener.is_a?(Listener)
45
+
46
+ log_msg("Removing #{event_name} listener", listener)
47
+
48
+ @listeners[event_name.to_sym].delete(listener)
49
+ end
20
50
 
21
51
  def dispatch(event_name, *args)
22
52
  listeners = @listeners[event_name.to_sym] || []
@@ -27,14 +27,24 @@ module Bidi2pdf
27
27
  client.send_cmd_and_wait(cmd) do |response|
28
28
  @interceptor_id = response["result"]["intercept"]
29
29
 
30
- Bidi2pdf.logger.debug "Interceptor added: #{@interceptor_id}"
30
+ Bidi2pdf.logger.debug2 "Interceptor added: #{@interceptor_id}"
31
31
 
32
- client.on_event(*self.class.events, &method(:handle_event))
32
+ @handle_event_listener = client.on_event(*self.class.events, &method(:handle_event))
33
33
 
34
34
  self
35
35
  end
36
36
  end
37
37
 
38
+ def unregister_with_client(client:)
39
+ return unless @handle_event_listener
40
+
41
+ client.remove_event_listener(*self.class.events, @handle_event_listener)
42
+
43
+ Bidi2pdf.logger.debug2 "Interceptor removed: #{@interceptor_id}"
44
+
45
+ @handle_event_listener = nil
46
+ end
47
+
38
48
  # rubocop: disable Metrics/AbcSize
39
49
  def handle_event(response)
40
50
  event_response = response["params"]
@@ -165,6 +165,7 @@ module Bidi2pdf
165
165
  Bidi2pdf.logger.info "Subscribing to events"
166
166
 
167
167
  Bidi::Client.new(websocket_url).tap do |event_client|
168
+ @event_socket = event_client
168
169
  event_client.start
169
170
  event_client.wait_until_open
170
171
 
@@ -320,6 +321,7 @@ module Bidi2pdf
320
321
  # Cleans up resources associated with the session.
321
322
  def cleanup
322
323
  @client&.close
324
+ @event_socket&.close
323
325
  @client = @websocket_url = @browser = nil
324
326
  end
325
327
  end
@@ -24,7 +24,7 @@ module Bidi2pdf
24
24
 
25
25
  def on_message(&) = socket_events.on(:message, &)
26
26
 
27
- def on_event(name, &) = session_events.on(name, &)
27
+ def on_event(*event_names, &) = session_events.on(*event_names, &)
28
28
 
29
29
  def on_open(&) = socket_events.on(:open, &)
30
30
 
@@ -34,13 +34,13 @@ module Bidi2pdf
34
34
 
35
35
  def remove_message_listener(block) = socket_events.off(:message, block)
36
36
 
37
- def remove_event_listener(name, block) = session_events.off(name, block)
37
+ def remove_event_listener(name, listener) = session_events.off(name, listener)
38
38
 
39
- def remove_open_listener(block) = socket_events.off(:open, block)
39
+ def remove_open_listener(listener) = socket_events.off(:open, listener)
40
40
 
41
- def remove_close_listener(block) = socket_events.off(:close, block)
41
+ def remove_close_listener(listener) = socket_events.off(:close, listener)
42
42
 
43
- def remove_error_listener(block) = socket_events.off(:error, block)
43
+ def remove_error_listener(listener) = socket_events.off(:error, listener)
44
44
 
45
45
  private
46
46
 
@@ -8,6 +8,7 @@ module Bidi2pdf
8
8
  include Chromedriver::Binary::Platform
9
9
 
10
10
  attr_reader :port, :pid, :started, :headless, :chrome_args, :shutdown_mutex
11
+ attr_accessor :reader_thread
11
12
 
12
13
  def initialize(port: 0, headless: true, chrome_args: Bidi::Session::DEFAULT_CHROME_ARGS)
13
14
  @port = port
@@ -49,10 +50,20 @@ module Bidi2pdf
49
50
  "http://localhost:#{@port}/session"
50
51
  end
51
52
 
53
+ # rubocop: disable Metrics/AbcSize
52
54
  def stop(timeout: 5)
53
55
  shutdown_mutex.synchronize do
54
56
  return unless @pid
55
57
 
58
+ if reader_thread&.alive?
59
+ begin
60
+ reader_thread.kill
61
+ reader_thread.join
62
+ rescue StandardError => e
63
+ Bidi2pdf.logger.error "Error killing reader thread: #{e.message}"
64
+ end
65
+ end
66
+
56
67
  @started = false
57
68
 
58
69
  close_session
@@ -72,6 +83,8 @@ module Bidi2pdf
72
83
  end
73
84
  end
74
85
 
86
+ # rubocop: enable Metrics/AbcSize
87
+
75
88
  private
76
89
 
77
90
  def spawn_process(cmd)
@@ -184,25 +197,26 @@ module Bidi2pdf
184
197
 
185
198
  # rubocop: disable Metrics/AbcSize
186
199
  def parse_port_from_output(io, timeout: 5)
187
- Thread.new do
188
- io.each_line do |line|
189
- Bidi2pdf.logger.debug1 line.chomp
200
+ port_event = Concurrent::Event.new
190
201
 
191
- next unless line =~ /ChromeDriver was started successfully on port (\d+)/
192
-
193
- Bidi2pdf.logger.debug "Found port: #{::Regexp.last_match(1).to_i} setup port: #{@port}"
194
-
195
- @port = ::Regexp.last_match(1).to_i if @port.nil? || @port.zero?
202
+ self.reader_thread = Thread.new do
203
+ io.each_line do |line|
204
+ Bidi2pdf.logger.info "[chromedriver] #{line.chomp}"
196
205
 
197
- break
206
+ if line =~ /ChromeDriver was started successfully on port (\d+)/
207
+ @port = ::Regexp.last_match(1).to_i if @port.nil? || @port.zero?
208
+ port_event.set
209
+ end
198
210
  end
199
211
  rescue IOError
200
212
  # reader closed
201
213
  ensure
202
214
  io.close unless io.closed?
203
- end.join(timeout)
215
+ end
216
+
217
+ return if port_event.wait(timeout)
204
218
 
205
- raise "Chromedriver did not report a usable port in #{timeout}s" if @port.nil?
219
+ raise "Chromedriver did not report a usable port in #{timeout}s"
206
220
  end
207
221
 
208
222
  # rubocop: enable Metrics/AbcSize
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "testcontainers"
5
- rescue LoadError
6
- warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf test helpers."
7
- end
8
-
9
3
  module Bidi2pdf
10
4
  module TestHelpers
11
5
  module Testcontainers
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shared_docker_network"
4
+
5
+ module Bidi2pdf
6
+ module TestHelpers
7
+ module Testcontainers
8
+ module ChromedriverTestHelper
9
+ def session_url
10
+ chromedriver_container.session_url
11
+ end
12
+
13
+ def chromedriver_container
14
+ RSpec.configuration.chromedriver_container
15
+ end
16
+ end
17
+
18
+ module SessionTestHelper
19
+ def chrome_args
20
+ chrome_args = Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.dup
21
+
22
+ # within github actions, the sandbox is not supported, when we start our own container
23
+ # some privileges are not available ???
24
+ if ENV["DISABLE_CHROME_SANDBOX"]
25
+ chrome_args << "--no-sandbox"
26
+
27
+ puts "🚨 Chrome sandbox disabled"
28
+ end
29
+ chrome_args
30
+ end
31
+
32
+ def create_session(session_url)
33
+ Bidi2pdf::Bidi::Session.new(session_url: session_url, headless: true, chrome_args: chrome_args)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ RSpec.configure do |config|
41
+ config.add_setting :chromedriver_container, default: nil
42
+
43
+ config.include Bidi2pdf::TestHelpers::Testcontainers::ChromedriverTestHelper, chromedriver: true
44
+ config.include Bidi2pdf::TestHelpers::Testcontainers::SessionTestHelper, session: true
45
+
46
+ config.before(:suite) do
47
+ if chromedriver_tests_present?
48
+ config.chromedriver_container = start_chromedriver_container(
49
+ build_dir: File.join(config.docker_dir, ".."),
50
+ mounts: config.respond_to?(:chromedriver_mounts) ? config.chromedriver_mounts : {},
51
+ shared_network: config.shared_network
52
+ )
53
+
54
+ puts "🚀 chromedriver container started for tests"
55
+ end
56
+ end
57
+
58
+ config.after(:suite) do
59
+ stop_container config.chromedriver_container
60
+ end
61
+ end
62
+
63
+ def stop_container(container)
64
+ if container&.running?
65
+
66
+ if ENV["SHOW_CONTAINER_LOGS"]
67
+ puts "Container logs:"
68
+ logs_std, logs_error = container.logs
69
+
70
+ puts logs_error
71
+ puts logs_std
72
+ end
73
+
74
+ puts "🧹 #{container.image} stopping container..."
75
+ container.stop
76
+ end
77
+ container&.remove
78
+ end
79
+
80
+ def chromedriver_tests_present?
81
+ test_of_kind_present? :chromedriver
82
+ end
83
+
84
+ def test_of_kind_present?(type)
85
+ RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[type] }
86
+ end
87
+
88
+ # alias the long class name
89
+ ChromedriverTestcontainer = Bidi2pdf::TestHelpers::Testcontainers::ChromedriverContainer
90
+
91
+ def start_chromedriver_container(build_dir:, mounts:, shared_network:)
92
+ container = ChromedriverTestcontainer.new(ChromedriverTestcontainer::DEFAULT_IMAGE,
93
+ build_dir: build_dir,
94
+ docker_file: "docker/Dockerfile.chromedriver")
95
+ .with_network(shared_network)
96
+ .with_network_aliases("remote-chrome")
97
+
98
+ container.with_filesystem_binds(mounts) if mounts&.any?
99
+
100
+ container.start
101
+
102
+ container
103
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.add_setting :shared_network, default: nil
5
+
6
+ config.before(:suite) do
7
+ examples = RSpec.world.filtered_examples.values.flatten
8
+ uses_containers = examples.any? do |ex|
9
+ ex.metadata[:nginx] || ex.metadata[:chrome] || ex.metadata[:chromedriver] || ex.metadata[:container]
10
+ end
11
+
12
+ if uses_containers
13
+ config.shared_network = Docker::Network.create("bidi2pdf-test-net-#{SecureRandom.hex(4)}")
14
+ puts "🕸️ started shared network #{config.shared_network}"
15
+ end
16
+ end
17
+
18
+ config.after(:suite) do
19
+ config.shared_network&.remove
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ module TestcontainersRefinement
6
+ def id
7
+ @_id
8
+ end
9
+
10
+ def aliases
11
+ @aliases ||= []
12
+ end
13
+
14
+ def aliases=(aliases)
15
+ @aliases = aliases
16
+ end
17
+
18
+ def network
19
+ @_network
20
+ end
21
+
22
+ def with_network(network)
23
+ @_network = network
24
+ self
25
+ end
26
+
27
+ def with_network_aliases(*aliases)
28
+ self.aliases += aliases
29
+ self
30
+ end
31
+
32
+ def _container_create_options
33
+ opts = super
34
+ network_name = network ? network.info["Name"] : nil
35
+ opts["HostConfig"]["NetworkMode"] = network_name
36
+
37
+ if network && aliases.any?
38
+ opts["NetworkingConfig"] = {
39
+ "EndpointsConfig" => {
40
+ network_name => {
41
+ "Aliases" => aliases
42
+ }
43
+ }
44
+ }
45
+ end
46
+
47
+ opts.compact
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ Testcontainers::DockerContainer.prepend(Bidi2pdf::TestHelpers::TestcontainersRefinement)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[docker testcontainers].each do |dep|
4
+ require dep
5
+ rescue LoadError
6
+ warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf test helpers."
7
+ end
8
+
9
+ module Bidi2pdf
10
+ module TestHelpers
11
+ module Testcontainers
12
+ require_relative "testcontainers/testcontainers_refinement"
13
+ require_relative "testcontainers/chromedriver_container"
14
+ require_relative "testcontainers/chromedriver_test_helper"
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
@@ -1,29 +1,35 @@
1
1
  module Bidi2pdf
2
2
  module Bidi
3
3
  class EventManager
4
- @browser: Bidi2pdf::Bidi::Browser
5
- @listeners: Hash[String, Array[Proc]]
6
- @active: bool
4
+ class Listener
5
+ attr_reader block: untyped
6
+ attr_reader id: String
7
7
 
8
- def initialize: (browser: Bidi2pdf::Bidi::Browser) -> void
8
+ def initialize: (untyped block, ?String id) -> void
9
9
 
10
- def on: (String event_name) { (Hash[String, untyped]) -> void } -> void
10
+ def call: (*untyped args) -> untyped
11
11
 
12
- def off: (String event_name) -> void
12
+ def ==: (untyped other) -> bool
13
13
 
14
- def emit: (String event_name, Hash[String, untyped] params) -> void
14
+ def eql?: (untyped other) -> bool
15
15
 
16
- def start_listening: () -> void
16
+ def hash: () -> Integer
17
+ end
17
18
 
18
- def stop_listening: () -> void
19
+ @listeners: untyped
20
+ @type: untyped
19
21
 
20
- private
22
+ attr_reader type: untyped
21
23
 
22
- def register_browser_events: () -> void
24
+ def initialize: (untyped type) -> void
23
25
 
24
- def handle_event: (String event_name, Hash[String, untyped] params) -> void
26
+ def on: (*untyped event_names, &untyped block) -> Listener
25
27
 
26
- def process_event: (String event_name, Hash[String, untyped] params) -> void
28
+ def off: (untyped event_name, Listener listener) -> void
29
+
30
+ def dispatch: (untyped event_name, *untyped args) -> void
31
+
32
+ def clear: (?untyped event_name) -> void
27
33
  end
28
34
  end
29
35
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bidi2pdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dieter S.
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-04-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -425,7 +424,11 @@ files:
425
424
  - lib/bidi2pdf/test_helpers/matchers/match_pdf_text.rb
426
425
  - lib/bidi2pdf/test_helpers/pdf_reader_utils.rb
427
426
  - lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb
427
+ - lib/bidi2pdf/test_helpers/testcontainers.rb
428
428
  - lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb
429
+ - lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb
430
+ - lib/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rb
431
+ - lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb
429
432
  - lib/bidi2pdf/verbose_logger.rb
430
433
  - lib/bidi2pdf/version.rb
431
434
  - sig/bidi2pdf.rbs
@@ -484,7 +487,6 @@ metadata:
484
487
  source_code_uri: https://github.com/dieter-medium/bidi2pdf
485
488
  changelog_uri: https://github.com/dieter-medium/bidi2pdf/blob/master/CHANGELOG.md
486
489
  rubygems_mfa_required: 'true'
487
- post_install_message:
488
490
  rdoc_options: []
489
491
  require_paths:
490
492
  - lib
@@ -499,8 +501,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
499
501
  - !ruby/object:Gem::Version
500
502
  version: '0'
501
503
  requirements: []
502
- rubygems_version: 3.5.11
503
- signing_key:
504
+ rubygems_version: 3.6.8
504
505
  specification_version: 4
505
506
  summary: A Ruby gem that generates PDFs from web pages using Chrome's BiDi protocol,
506
507
  providing high-quality PDF documents from any URL with full support for modern web