bidi2pdf 0.1.8 → 0.1.10
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 +4 -4
- data/CHANGELOG.md +88 -3
- data/README.md +146 -7
- data/docker/Dockerfile.chromedriver +23 -5
- data/docker/entrypoint.sh +41 -0
- data/lib/bidi2pdf/bidi/auth_interceptor.rb +3 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +60 -19
- data/lib/bidi2pdf/bidi/client.rb +7 -5
- data/lib/bidi2pdf/bidi/commands/cdp_get_session.rb +21 -0
- data/lib/bidi2pdf/bidi/commands/page_print.rb +101 -0
- data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +4 -1
- data/lib/bidi2pdf/bidi/commands.rb +2 -0
- data/lib/bidi2pdf/bidi/connection_manager.rb +3 -0
- data/lib/bidi2pdf/bidi/event_manager.rb +34 -4
- data/lib/bidi2pdf/bidi/interceptor.rb +12 -2
- data/lib/bidi2pdf/bidi/session.rb +35 -2
- data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +5 -5
- data/lib/bidi2pdf/chromedriver_manager.rb +25 -11
- data/lib/bidi2pdf/cli.rb +9 -2
- data/lib/bidi2pdf/test_helpers/configuration.rb +67 -0
- data/lib/bidi2pdf/test_helpers/images/extractor.rb +99 -0
- data/lib/bidi2pdf/test_helpers/images/image_similarity_checker.rb +50 -0
- data/lib/bidi2pdf/test_helpers/images/tiff_helper.rb +204 -0
- data/lib/bidi2pdf/test_helpers/images.rb +12 -0
- data/lib/bidi2pdf/test_helpers/matchers/contains_pdf_image.rb +29 -0
- data/lib/bidi2pdf/test_helpers/pdf_file_helper.rb +39 -0
- data/lib/bidi2pdf/test_helpers/spec_paths_helper.rb +60 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +0 -6
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +103 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rb +21 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb +53 -0
- data/lib/bidi2pdf/test_helpers/testcontainers.rb +17 -0
- data/lib/bidi2pdf/test_helpers.rb +7 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/sig/bidi2pdf/bidi/event_manager.rbs +19 -13
- metadata +55 -10
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module TestHelpers
|
5
|
+
module Images
|
6
|
+
# rubocop: disable Metrics/ModuleLength, Metrics/AbcSize
|
7
|
+
module TIFFHelper
|
8
|
+
# TIFF Tag IDs
|
9
|
+
IMAGE_WIDTH = 256
|
10
|
+
IMAGE_LENGTH = 257
|
11
|
+
BITS_PER_SAMPLE = 258
|
12
|
+
COMPRESSION = 259
|
13
|
+
PHOTOMETRIC_INTERPRETATION = 262
|
14
|
+
STRIP_OFFSETS = 273
|
15
|
+
SAMPLES_PER_PIXEL = 277
|
16
|
+
ROWS_PER_STRIP = 278
|
17
|
+
STRIP_BYTE_COUNTS = 279
|
18
|
+
PLANAR_CONFIGURATION = 284
|
19
|
+
INK_SET = 332
|
20
|
+
|
21
|
+
# TIFF Data Types
|
22
|
+
TYPE_SHORT = 3
|
23
|
+
TYPE_LONG = 4
|
24
|
+
|
25
|
+
# TIFF Compression Types
|
26
|
+
COMPRESSION_NONE = 1
|
27
|
+
COMPRESSION_CCITT_G3 = 3
|
28
|
+
COMPRESSION_CCITT_G4 = 4
|
29
|
+
|
30
|
+
# TIFF Photometric Interpretations
|
31
|
+
PHOTO_WHITE_IS_ZERO = 0
|
32
|
+
PHOTO_BLACK_IS_ZERO = 1
|
33
|
+
PHOTO_RGB = 2
|
34
|
+
PHOTO_SEPARATION = 5
|
35
|
+
|
36
|
+
# Planar Configuration
|
37
|
+
PLANAR_CHUNKY = 1
|
38
|
+
|
39
|
+
def tiff_header(hash, data)
|
40
|
+
cs_entry = hash[:ColorSpace]
|
41
|
+
|
42
|
+
cs = if cs_entry.is_a?(Array) && cs_entry.first == :ICCBased
|
43
|
+
icc_stream = cs_entry[1]
|
44
|
+
icc_stream.hash[:Alternate]
|
45
|
+
else
|
46
|
+
cs_entry
|
47
|
+
end
|
48
|
+
|
49
|
+
case cs
|
50
|
+
when :DeviceCMYK then tiff_header_for_CMYK(hash, data)
|
51
|
+
when :DeviceGray then tiff_header_for_gray(hash, data)
|
52
|
+
when :DeviceRGB then tiff_header_for_rgb(hash, data)
|
53
|
+
else
|
54
|
+
logger.warn("Unsupported color space '#{cs}' for compressed image with filter '#{hash[:Filter]}'. Skipping image.")
|
55
|
+
nil # Skip processing this image
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# See:
|
60
|
+
# * https://gist.github.com/gstorer/f6a9f1dfe41e8e64dcf58d07afa9ab2a
|
61
|
+
# * https://github.com/yob/pdf-reader/blob/main/examples/extract_images.rb
|
62
|
+
def pack_tiff(entries)
|
63
|
+
fields = entries.size
|
64
|
+
fmt = [
|
65
|
+
"a2", # Byte order ("II")
|
66
|
+
"S<", # TIFF magic (42)
|
67
|
+
"L<", # Offset to first IFD (8)
|
68
|
+
"S<", # Number of directory entries
|
69
|
+
("S< S< L< L<" * fields), # each tag: id, type, count, value
|
70
|
+
"L<" # Next IFD offset (0 = end)
|
71
|
+
].join(" ")
|
72
|
+
|
73
|
+
# Build the flat array: ['II', 42, 8, fields, tag1, type1, count1, value1, …, 0]
|
74
|
+
values = ["II", 42, 8, fields] + entries.flatten + [0]
|
75
|
+
values.pack(fmt)
|
76
|
+
end
|
77
|
+
|
78
|
+
def tiff_header_for_gray(hash, data)
|
79
|
+
width = hash[:Width]
|
80
|
+
height = hash[:Height]
|
81
|
+
bpc = hash[:BitsPerComponent] || 8
|
82
|
+
img_size = data.bytesize
|
83
|
+
|
84
|
+
# 9 tags, no extra arrays needed
|
85
|
+
fields = 9
|
86
|
+
# size of header+IFD before the image data starts
|
87
|
+
header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
|
88
|
+
data_offset = header_ifd_size
|
89
|
+
|
90
|
+
entries = [
|
91
|
+
[IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
|
92
|
+
[IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
|
93
|
+
[BITS_PER_SAMPLE, TYPE_SHORT, 1, bpc], # BitsPerSample
|
94
|
+
[COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
|
95
|
+
[PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_BLACK_IS_ZERO], # PhotometricInterpretation (1 = BlackIsZero)
|
96
|
+
[STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
|
97
|
+
[SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 1], # SamplesPerPixel
|
98
|
+
[STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size], # StripByteCounts
|
99
|
+
[PLANAR_CONFIGURATION, TYPE_SHORT, 1, PLANAR_CHUNKY] # PlanarConfiguration (1 = chunky)
|
100
|
+
]
|
101
|
+
|
102
|
+
pack_tiff(entries)
|
103
|
+
end
|
104
|
+
|
105
|
+
def tiff_header_for_ccitt(hash, data)
|
106
|
+
dp = hash[:DecodeParms] || {}
|
107
|
+
width = dp[:Columns] || hash[:Width]
|
108
|
+
height = hash[:Height]
|
109
|
+
k = dp[:K] || 0
|
110
|
+
group = (k.positive? ? COMPRESSION_CCITT_G3 : COMPRESSION_CCITT_G4)
|
111
|
+
img_size = data.bytesize
|
112
|
+
|
113
|
+
# We’ll emit exactly 8 tags:
|
114
|
+
fields = 8
|
115
|
+
# Calculate where the image data will start:
|
116
|
+
header_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
|
117
|
+
|
118
|
+
entries = [
|
119
|
+
[IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
|
120
|
+
[IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
|
121
|
+
[BITS_PER_SAMPLE, TYPE_SHORT, 1, 1], # BitsPerSample
|
122
|
+
[COMPRESSION, TYPE_SHORT, 1, group], # Compression (3=G3, 4=G4)
|
123
|
+
[PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_WHITE_IS_ZERO], # PhotometricInterpretation (0 = WhiteIsZero)
|
124
|
+
[STRIP_OFFSETS, TYPE_LONG, 1, header_size], # StripOffsets
|
125
|
+
[ROWS_PER_STRIP, TYPE_LONG, 1, height], # RowsPerStrip
|
126
|
+
[STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size] # StripByteCounts
|
127
|
+
]
|
128
|
+
|
129
|
+
pack_tiff(entries)
|
130
|
+
end
|
131
|
+
|
132
|
+
def tiff_header_for_cmyk(hash, data)
|
133
|
+
width = hash[:Width]
|
134
|
+
height = hash[:Height]
|
135
|
+
bpc = hash[:BitsPerComponent] || 8
|
136
|
+
img_size = data.bytesize
|
137
|
+
|
138
|
+
# CMYK needs 10 tags + a 4×SHORT BitsPerSample array
|
139
|
+
fields = 10
|
140
|
+
bits_array_size = 4 * 2 # 4 channels × 2 bytes each
|
141
|
+
|
142
|
+
# Size of header + IFD (before the bits array)
|
143
|
+
header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
|
144
|
+
# Where the pixel data will really start:
|
145
|
+
data_offset = header_ifd_size + bits_array_size
|
146
|
+
|
147
|
+
entries = [
|
148
|
+
[IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
|
149
|
+
[IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
|
150
|
+
[BITS_PER_SAMPLE, TYPE_SHORT, 4, header_ifd_size], # BitsPerSample (pointer to array)
|
151
|
+
[COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
|
152
|
+
[PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_SEPARATION], # PhotometricInterpretation (5 = Separation)
|
153
|
+
[STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
|
154
|
+
[SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 4], # SamplesPerPixel
|
155
|
+
[STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size], # StripByteCounts
|
156
|
+
[PLANAR_CONFIGURATION, TYPE_SHORT, 1, PLANAR_CHUNKY], # PlanarConfiguration (1 = chunky)
|
157
|
+
[INK_SET, TYPE_SHORT, 1, 1] # InkSet (1 = CMYK)
|
158
|
+
]
|
159
|
+
|
160
|
+
header = pack_tiff(entries)
|
161
|
+
# Append the 4-channel BitsPerSample array as little‐endian SHORTs:
|
162
|
+
header << [bpc, bpc, bpc, bpc].pack("S<S<S<S<")
|
163
|
+
header
|
164
|
+
end
|
165
|
+
|
166
|
+
def tiff_header_for_rgb(hash, data)
|
167
|
+
width = hash[:Width]
|
168
|
+
height = hash[:Height]
|
169
|
+
bpc = hash[:BitsPerComponent] || 8
|
170
|
+
img_size = data.bytesize
|
171
|
+
|
172
|
+
# 8 tags + a 3×SHORT BitsPerSample array
|
173
|
+
fields = 8
|
174
|
+
bits_array_size = 3 * 2 # 3 channels × 2 bytes each
|
175
|
+
|
176
|
+
# size of header + IFD before the bits array
|
177
|
+
header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
|
178
|
+
# where the pixel data really starts:
|
179
|
+
data_offset = header_ifd_size + bits_array_size
|
180
|
+
|
181
|
+
entries = [
|
182
|
+
[IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
|
183
|
+
[IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
|
184
|
+
[BITS_PER_SAMPLE, TYPE_SHORT, 3, header_ifd_size], # BitsPerSample → pointer to our array
|
185
|
+
[COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
|
186
|
+
[PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_RGB], # PhotometricInterpretation (2 = RGB)
|
187
|
+
[STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
|
188
|
+
[SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 3], # SamplesPerPixel
|
189
|
+
[STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size] # StripByteCounts
|
190
|
+
]
|
191
|
+
|
192
|
+
# pack the IFD
|
193
|
+
header = pack_tiff(entries)
|
194
|
+
|
195
|
+
# append the 3-channel BitsPerSample as little-endian SHORTs
|
196
|
+
header << [bpc, bpc, bpc].pack("S<S<S<")
|
197
|
+
|
198
|
+
header
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
# rubocop: enable Metrics/ModuleLength, Metrics/AbcSize
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
%w[vips dhash-vips].each do |dep|
|
4
|
+
require dep
|
5
|
+
rescue LoadError
|
6
|
+
warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf image test helpers."
|
7
|
+
end
|
8
|
+
|
9
|
+
require_relative "images/tiff_helper"
|
10
|
+
require_relative "images/extractor"
|
11
|
+
require_relative "images/image_similarity_checker"
|
12
|
+
require_relative "matchers/contains_pdf_image"
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec::Matchers.define :contains_pdf_image do |expected, tolerance: 10|
|
4
|
+
chain :at_page do |page_number|
|
5
|
+
@page_number = page_number
|
6
|
+
end
|
7
|
+
|
8
|
+
chain :at_position do |i|
|
9
|
+
@image_number = i
|
10
|
+
end
|
11
|
+
|
12
|
+
match do |actual_pdf|
|
13
|
+
extractor = Bidi2pdf::TestHelpers::Images::Extractor.new(actual_pdf)
|
14
|
+
@images = if @page_number
|
15
|
+
@image_number ? [extractor.image_on_page(@page_number, @image_number)].compact : extractor.images_on_page(@page_number)
|
16
|
+
else
|
17
|
+
extractor.all_images
|
18
|
+
end
|
19
|
+
|
20
|
+
@checkers = @images.map { |image| Bidi2pdf::TestHelpers::Images::ImageSimilarityChecker.new(expected, image) }
|
21
|
+
|
22
|
+
@checkers.any? { |checker| checker.similar?(tolerance:) }
|
23
|
+
end
|
24
|
+
|
25
|
+
failure_message do |_actual_pdf|
|
26
|
+
"expected to find one image #{"on page #{@page_number}" if @page_number}#{" at position #{@image_number}" if @image_number} to be perceptually similar (distance ≤ #{tolerance}), " \
|
27
|
+
"but Hamming distances have been #{@checkers.map(&:distance).join(", ")}"
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module TestHelpers
|
5
|
+
# This module provides helper methods for handling PDF files in tests.
|
6
|
+
# It includes methods for debugging, storing, and managing PDF files.
|
7
|
+
module PdfFileHelper
|
8
|
+
# Executes a block with the given PDF data and handles debugging in case of test failures.
|
9
|
+
# If an expectation fails, the PDF data is saved to a file for debugging purposes.
|
10
|
+
# @param [String] pdf_data the PDF data to debug
|
11
|
+
# @yield [String] yields the PDF data to the given block
|
12
|
+
# @raise [RSpec::Expectations::ExpectationNotMetError] re-raises the exception after saving the PDF
|
13
|
+
def with_pdf_debug(pdf_data)
|
14
|
+
yield pdf_data
|
15
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
16
|
+
failure_output = store_pdf_file pdf_data, "test-failure"
|
17
|
+
puts "Test failed! PDF saved to: #{failure_output}"
|
18
|
+
raise e
|
19
|
+
end
|
20
|
+
|
21
|
+
# Stores the given PDF data to a file with a specified filename prefix.
|
22
|
+
# The file is saved in a temporary directory.
|
23
|
+
# @param [String] pdf_data the PDF data to store
|
24
|
+
# @param [String] filename_prefix the prefix for the generated filename (default: "test")
|
25
|
+
# @return [String] the full path to the saved PDF file
|
26
|
+
def store_pdf_file(pdf_data, filename_prefix = "test")
|
27
|
+
pdf_file = tmp_file("pdf-files", "#{filename_prefix}-#{Time.now.to_i}.pdf")
|
28
|
+
FileUtils.mkdir_p(File.dirname(pdf_file))
|
29
|
+
File.binwrite(pdf_file, pdf_data)
|
30
|
+
|
31
|
+
pdf_file
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec.configure do |config|
|
36
|
+
config.include PdfFileHelper, pdf: true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module provides helper methods for managing paths in test environments.
|
4
|
+
# It includes methods to retrieve directories and generate temporary file paths.
|
5
|
+
module Bidi2pdf
|
6
|
+
module TestHelpers
|
7
|
+
# This submodule contains path-related helper methods and configuration for tests.
|
8
|
+
module SpecPathsHelper
|
9
|
+
# Retrieves the directory path for Docker files.
|
10
|
+
# @return [String] the Docker directory path
|
11
|
+
def fixture_dir
|
12
|
+
TestHelpers.configuration.fixture_dir
|
13
|
+
end
|
14
|
+
|
15
|
+
# Retrieves the directory path for fixtures.
|
16
|
+
# @return [String] the fixture directory path
|
17
|
+
def fixture_file(*)
|
18
|
+
File.join(fixture_dir, *)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieves the directory path for specs.
|
22
|
+
# @return [String] the spec directory path
|
23
|
+
def spec_dir
|
24
|
+
TestHelpers.configuration.spec_dir
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves the directory path for temporary files.
|
28
|
+
# @return [String] the temporary directory path
|
29
|
+
def tmp_dir
|
30
|
+
TestHelpers.configuration.tmp_dir
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generates a path for a temporary file by joining the temporary directory with the given parts.
|
34
|
+
# @param [Array<String>] parts the parts of the file path to join
|
35
|
+
# @return [String] the full path to the temporary file
|
36
|
+
def tmp_file(*parts)
|
37
|
+
File.join(tmp_dir, *parts)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generates a random temporary directory path.
|
41
|
+
# @param [Array<String>] dirs additional directory components to include in the path
|
42
|
+
# @param [String, nil] prefix an optional prefix for the directory name
|
43
|
+
# @return [String] the full path to the random temporary directory
|
44
|
+
def random_tmp_dir(*dirs, prefix: nil)
|
45
|
+
base_dirs = [tmp_dir] + dirs.compact
|
46
|
+
pfx = prefix || TestHelpers.configuration.prefix
|
47
|
+
File.join(*base_dirs, "#{pfx}#{SecureRandom.hex(8)}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Configures RSpec to include and extend SpecPathsHelper for examples with the `:pdf` metadata.
|
52
|
+
RSpec.configure do |config|
|
53
|
+
# Includes SpecPathsHelper methods in examples with `:pdf` metadata.
|
54
|
+
config.include SpecPathsHelper, pdf: true
|
55
|
+
|
56
|
+
# Extends SpecPathsHelper methods to example groups with `:pdf` metadata.
|
57
|
+
config.extend SpecPathsHelper, pdf: true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -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(Bidi2pdf::TestHelpers.configuration.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&.info&.[]("Name")
|
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
|
@@ -6,8 +6,15 @@ rescue LoadError
|
|
6
6
|
warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf test helpers."
|
7
7
|
end
|
8
8
|
|
9
|
+
require "bidi2pdf/test_helpers/configuration"
|
10
|
+
require "bidi2pdf/test_helpers/pdf_file_helper"
|
11
|
+
require "bidi2pdf/test_helpers/spec_paths_helper"
|
9
12
|
require "bidi2pdf/test_helpers/pdf_text_sanitizer"
|
10
13
|
require "bidi2pdf/test_helpers/pdf_reader_utils"
|
11
14
|
require "bidi2pdf/test_helpers/matchers/match_pdf_text"
|
12
15
|
require "bidi2pdf/test_helpers/matchers/contains_pdf_text"
|
13
16
|
require "bidi2pdf/test_helpers/matchers/have_pdf_page_count"
|
17
|
+
|
18
|
+
# don't require "bidi2pdf/test_helpers/matchers/contains_pdf_image.rb" directly, use
|
19
|
+
# require "bidi2pdf/test_helpers/images" instead, because it requires
|
20
|
+
# ruby-vips and dhash-vips, and not every one wants to use them
|
data/lib/bidi2pdf/version.rb
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
module Bidi2pdf
|
2
2
|
module Bidi
|
3
3
|
class EventManager
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
class Listener
|
5
|
+
attr_reader block: untyped
|
6
|
+
attr_reader id: String
|
7
7
|
|
8
|
-
|
8
|
+
def initialize: (untyped block, ?String id) -> void
|
9
9
|
|
10
|
-
|
10
|
+
def call: (*untyped args) -> untyped
|
11
11
|
|
12
|
-
|
12
|
+
def ==: (untyped other) -> bool
|
13
13
|
|
14
|
-
|
14
|
+
def eql?: (untyped other) -> bool
|
15
15
|
|
16
|
-
|
16
|
+
def hash: () -> Integer
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
+
@listeners: untyped
|
20
|
+
@type: untyped
|
19
21
|
|
20
|
-
|
22
|
+
attr_reader type: untyped
|
21
23
|
|
22
|
-
def
|
24
|
+
def initialize: (untyped type) -> void
|
23
25
|
|
24
|
-
def
|
26
|
+
def on: (*untyped event_names, &untyped block) -> Listener
|
25
27
|
|
26
|
-
def
|
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
|