htmlToPdf 2.0.0 → 3.0.0

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.
data/lib/htmlToPdf.rb CHANGED
@@ -1,90 +1,97 @@
1
1
  require "securerandom"
2
+ require "active_support/concern"
3
+ require "active_support/core_ext/object/blank"
2
4
  require "htmlToPdf/version"
3
5
  require "htmlToPdf/configuration"
6
+ require "htmlToPdf/browser_pool"
7
+ require "htmlToPdf/print_options"
4
8
 
5
9
  module HtmlToPdf
10
+ extend ActiveSupport::Concern
11
+
12
+ # Wire up with `include HtmlToPdf` then `around_action :html_to_pdf` (not
13
+ # `before_action`) in the controller(s) that should support it — typically
14
+ # ApplicationController. An around_action is required: it lets the rest of
15
+ # the callback chain and the action itself run normally via `yield` (so
16
+ # other before_actions' instance variables are available), rather than
17
+ # re-invoking the action out-of-band the way earlier versions of this gem
18
+ # did.
19
+ def html_to_pdf
20
+ unless pdf_request?
21
+ yield
22
+ return
23
+ end
6
24
 
7
- def html_to_pdf()
8
25
  pdf_options = params[:pdf_options] ? params[:pdf_options] : {}
9
- if request.format == 'application/pdf'
10
- initialize_downloads(pdf_options)
11
- _tmp_dir = HtmlToPdf.configuration.tmp_dir || File.join(Rails.root, 'tmp')
12
- FileUtils.mkdir_p(_tmp_dir)
13
- _unique_id = "#{Time.now.to_i}_#{SecureRandom.hex(8)}"
14
- _source_file = File.join(_tmp_dir, "#{_unique_id}.html")
15
- _destination_file = File.join(_tmp_dir, "#{_unique_id}.pdf")
16
- _controller = ("#{controller_name}Controller").camelize.constantize.new
17
- _controller.request = request
18
- _controller.response = response
19
- _controller.params = params
20
- _controller.method(action_name).call
21
-
22
- _controller.instance_variables.each do |var|
23
- instance_variable_set(var, _controller.instance_variable_get(var))
24
- end
26
+ initialize_downloads(pdf_options)
27
+
28
+ # Run the real callback chain/action with the format forced to :html, so
29
+ # Rails' own implicit rendering resolves the normal HTML template instead
30
+ # of raising UnknownFormat for a nonexistent .pdf template. That render's
31
+ # output is discarded — render_to_string below (not response.body) is
32
+ # what actually gets converted — this only needs to happen so the action
33
+ # itself runs in its ordinary place in the chain.
34
+ original_format = request.format
35
+ request.format = :html
36
+ begin
37
+ yield
38
+ ensure
39
+ request.format = original_format
40
+ end
25
41
 
26
- _html = render_to_string(template: "#{controller_name}/#{action_name}", :layout => @layout, locals: { url: Rails.application.routes.url_helpers })
27
- file_content = _html.gsub(/\\"/, "")
42
+ html = render_to_string(template: "#{controller_name}/#{action_name}", layout: @layout, locals: { url: Rails.application.routes.url_helpers })
43
+ file_content = html.gsub(/\\"/, "")
28
44
 
45
+ tmp_dir = HtmlToPdf.configuration.tmp_dir || File.join(Rails.root, 'tmp')
46
+ FileUtils.mkdir_p(tmp_dir)
47
+ source_file = File.join(tmp_dir, "#{Time.now.to_i}_#{SecureRandom.hex(8)}.html")
48
+ File.write(source_file, file_content)
29
49
 
30
- File.open(_source_file, "w+") do |f|
31
- f.write(file_content)
32
- end
33
- system(HtmlToPdf.configuration.wkhtmltopdf_path, *wkhtmltopdf_args, _source_file, _destination_file)
34
- File.delete(_source_file) if File.exist?(_source_file)
35
- send_data File.open(_destination_file, "rb") { |f| f.read }, :disposition => 'attachment', :filename => "#{sanitized_pdf_name}.pdf"
36
- File.delete(_destination_file)
50
+ begin
51
+ pdf_bytes = render_pdf(source_file)
52
+ ensure
53
+ File.delete(source_file) if File.exist?(source_file)
37
54
  end
55
+
56
+ deliver_pdf(pdf_bytes)
38
57
  end
39
58
 
40
59
  def initialize_downloads(pdf_options = {})
41
60
  pdf_options ||= {}
42
61
  @name = pdf_options[:title].nil? ? "#{Time.now.to_i.to_s}" : pdf_options[:title]
43
62
  @layout = pdf_options[:layout].nil? ? HtmlToPdf.configuration.default_layout : pdf_options[:layout]
44
- @wkhtmltopdf_options = build_wkhtmltopdf_options(pdf_options)
63
+ @print_options, @javascript_delay = HtmlToPdf::PrintOptions.build(pdf_options)
45
64
  end
46
65
 
47
66
  def sanitized_pdf_name
48
67
  @name.to_s.gsub(/[^a-zA-Z0-9_\- ]/, '').strip.presence || Time.now.to_i.to_s
49
68
  end
50
69
 
51
- # Merges, in increasing precedence: HtmlToPdf.configuration.default_options,
52
- # pdf_options[:padding] (shorthand applied to all four margins), then
53
- # pdf_options[:wkhtmltopdf_options] (any wkhtmltopdf flag, e.g. orientation:
54
- # 'Landscape', page_size: 'A4', grayscale: true). Keys are dasherized so
55
- # either underscore or dash form works.
56
- def build_wkhtmltopdf_options(pdf_options)
57
- options = dasherize_keys(HtmlToPdf.configuration.default_options)
58
-
59
- if pdf_options[:padding].present?
60
- padding = pdf_options[:padding].to_s
61
- %w[margin-top margin-bottom margin-left margin-right].each { |flag| options[flag] = padding }
70
+ # Renders the given HTML file to PDF bytes via a pooled headless Chrome
71
+ # instance. Navigating to a file:// URL (rather than feeding Chrome the
72
+ # markup directly) gives relative asset paths a real base URI to resolve
73
+ # against, matching how a browser would load the page.
74
+ def render_pdf(source_file)
75
+ HtmlToPdf::BrowserPool.with_page do |page|
76
+ page.go_to("file://#{source_file}")
77
+ sleep(@javascript_delay / 1000.0) if @javascript_delay.to_i.positive?
78
+ page.pdf(encoding: :binary, **(@print_options || {}))
62
79
  end
63
-
64
- options.merge!(dasherize_keys(pdf_options[:wkhtmltopdf_options] || {}))
65
- options
66
80
  end
67
81
 
68
- def dasherize_keys(hash)
69
- hash.each_with_object({}) { |(key, value), acc| acc[key.to_s.tr('_', '-')] = value }
70
- end
71
-
72
- # Query-string values arrive as strings (e.g. grayscale: true serializes to
73
- # "true" in a link_to href and comes back as that literal string), so plain
74
- # booleans aren't enough to detect a falsy/truthy flag.
75
- FALSEY_VALUES = [nil, false, '', '0', 'false', 'off'].freeze
76
- TRUTHY_FLAG_VALUES = [true, 'true'].freeze
77
-
78
- def wkhtmltopdf_args
79
- (@wkhtmltopdf_options || {}).each_with_object([]) do |(flag, value), args|
80
- normalized = value.is_a?(String) ? value.downcase : value
81
- next if FALSEY_VALUES.include?(normalized)
82
+ private
82
83
 
83
- args << "--#{flag}"
84
- args << value.to_s unless TRUTHY_FLAG_VALUES.include?(normalized)
85
- end
84
+ def pdf_request?
85
+ request.format == 'application/pdf'
86
86
  end
87
87
 
88
+ # Equivalent to send_data, but assigns response_body directly instead of
89
+ # going through #render. #render refuses a second render in the same
90
+ # action (AbstractController::DoubleRenderError) — and by this point one
91
+ # already happened, deliberately, via the forced-:html yield above.
92
+ def deliver_pdf(pdf_bytes)
93
+ send_file_headers!(disposition: 'attachment', filename: "#{sanitized_pdf_name}.pdf")
94
+ self.status = 200
95
+ self.response_body = pdf_bytes
96
+ end
88
97
  end
89
-
90
- ActionController::Base.send(:include, HtmlToPdf)
@@ -0,0 +1,89 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe HtmlToPdf::BrowserPool do
4
+ after { described_class.reset! }
5
+
6
+ describe "pooling (stubbed Ferrum, no real Chrome)" do
7
+ let(:fake_browser) { instance_double(Ferrum::Browser, quit: nil) }
8
+
9
+ before do
10
+ allow(Ferrum::Browser).to receive(:new).and_return(fake_browser)
11
+ end
12
+
13
+ it "memoizes one browser across multiple with_page calls" do
14
+ allow(fake_browser).to receive(:create_page).and_yield(double("page")).twice
15
+
16
+ described_class.with_page { |_page| nil }
17
+ described_class.with_page { |_page| nil }
18
+
19
+ expect(Ferrum::Browser).to have_received(:new).once
20
+ end
21
+
22
+ it "quits and drops the memoized browser on reset!" do
23
+ described_class.browser
24
+
25
+ described_class.reset!
26
+
27
+ expect(fake_browser).to have_received(:quit)
28
+ allow(Ferrum::Browser).to receive(:new).and_return(fake_browser)
29
+ described_class.browser
30
+ expect(Ferrum::Browser).to have_received(:new).twice
31
+ end
32
+
33
+ it "retries once after a recoverable error, then succeeds on the new browser" do
34
+ call_count = 0
35
+ allow(fake_browser).to receive(:create_page) do |&block|
36
+ call_count += 1
37
+ raise Ferrum::DeadBrowserError if call_count == 1
38
+
39
+ block.call(double("page"))
40
+ end
41
+
42
+ result = described_class.with_page { |_page| :ok }
43
+
44
+ expect(result).to eq(:ok)
45
+ expect(call_count).to eq(2)
46
+ end
47
+
48
+ it "gives up and raises after the retry also fails" do
49
+ allow(fake_browser).to receive(:create_page).and_raise(Ferrum::DeadBrowserError)
50
+
51
+ expect { described_class.with_page { |_page| nil } }.to raise_error(Ferrum::DeadBrowserError)
52
+ end
53
+
54
+ it "does not retry a non-recoverable error" do
55
+ allow(fake_browser).to receive(:create_page).and_raise(ArgumentError, "boom")
56
+
57
+ expect { described_class.with_page { |_page| nil } }.to raise_error(ArgumentError)
58
+ expect(fake_browser).to have_received(:create_page).once
59
+ end
60
+
61
+ it "passes configuration.browser_options straight to Ferrum::Browser.new" do
62
+ HtmlToPdf.configure { |c| c.browser_options = { timeout: 42 } }
63
+ allow(fake_browser).to receive(:create_page).and_yield(double("page"))
64
+
65
+ described_class.with_page { |_page| nil }
66
+
67
+ expect(Ferrum::Browser).to have_received(:new).with(timeout: 42)
68
+ end
69
+ end
70
+
71
+ describe "against a real Chrome process", :chrome do
72
+ it "reuses the same browser process across renders" do
73
+ pids = Array.new(3) do
74
+ described_class.with_page do |page|
75
+ page.go_to("about:blank")
76
+ described_class.browser.process.pid
77
+ end
78
+ end
79
+
80
+ expect(pids.uniq.size).to eq(1)
81
+ end
82
+
83
+ it "does not leak a browser context per render (block form disposes it)" do
84
+ 5.times { described_class.with_page { |page| page.go_to("about:blank") } }
85
+
86
+ expect(described_class.browser.contexts.size).to eq(0)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe HtmlToPdf::Configuration do
4
+ describe "defaults" do
5
+ subject(:config) { described_class.new }
6
+
7
+ it "has no browser_options, letting Ferrum auto-detect Chrome" do
8
+ expect(config.browser_options).to eq({})
9
+ end
10
+
11
+ it "defaults to the application layout" do
12
+ expect(config.default_layout).to eq("application")
13
+ end
14
+
15
+ it "prints backgrounds by default" do
16
+ expect(config.default_options).to eq(print_background: true)
17
+ end
18
+
19
+ it "does not honor request-supplied header/footer templates by default" do
20
+ expect(config.allow_request_templates).to eq(false)
21
+ end
22
+
23
+ it "caps javascript_delay at 10 seconds by default" do
24
+ expect(config.max_javascript_delay_ms).to eq(10_000)
25
+ end
26
+ end
27
+
28
+ describe "HtmlToPdf.configure" do
29
+ after { HtmlToPdf.reset_configuration! }
30
+
31
+ it "yields the shared configuration for mutation" do
32
+ HtmlToPdf.configure { |c| c.default_layout = "pdf" }
33
+
34
+ expect(HtmlToPdf.configuration.default_layout).to eq("pdf")
35
+ end
36
+
37
+ it "memoizes the same configuration instance across calls" do
38
+ expect(HtmlToPdf.configuration).to equal(HtmlToPdf.configuration)
39
+ end
40
+ end
41
+
42
+ describe ".reset_configuration!" do
43
+ it "replaces the configuration with fresh defaults" do
44
+ HtmlToPdf.configure { |c| c.default_layout = "pdf" }
45
+
46
+ HtmlToPdf.reset_configuration!
47
+
48
+ expect(HtmlToPdf.configuration.default_layout).to eq("application")
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,154 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe HtmlToPdf::PrintOptions do
4
+ def build(pdf_options)
5
+ described_class.build(pdf_options)
6
+ end
7
+
8
+ describe "precedence" do
9
+ it "lets a per-request :padding override a gem-wide default margin" do
10
+ HtmlToPdf.configure { |c| c.default_options = { margin_top: "1in" } }
11
+
12
+ options, = build(padding: "20mm")
13
+
14
+ expect(options[:margin_top]).to be_within(0.0001).of(20 / 25.4)
15
+ end
16
+
17
+ it "lets an explicit print_options margin win over :padding" do
18
+ options, = build(padding: "20mm", print_options: { margin_top: "5mm" })
19
+
20
+ expect(options[:margin_top]).to be_within(0.0001).of(5 / 25.4)
21
+ expect(options[:margin_bottom]).to be_within(0.0001).of(20 / 25.4)
22
+ end
23
+
24
+ it "lets explicit paper_width/paper_height override a gem-wide default :format" do
25
+ HtmlToPdf.configure { |c| c.default_options = { format: "A4" } }
26
+
27
+ options, = build(print_options: { paper_width: 5, paper_height: 5 })
28
+
29
+ expect(options[:paper_width]).to eq(5.0)
30
+ expect(options[:paper_height]).to eq(5.0)
31
+ end
32
+
33
+ it "resolves a per-request :format, overriding the default" do
34
+ HtmlToPdf.configure { |c| c.default_options = { format: "A4" } }
35
+
36
+ options, = build(print_options: { format: "Letter" })
37
+
38
+ expect(options[:paper_width]).to eq(8.50)
39
+ expect(options[:paper_height]).to eq(11.00)
40
+ end
41
+
42
+ it "falls back to the resolved default format when nothing is passed" do
43
+ HtmlToPdf.configure { |c| c.default_options = { format: "A4" } }
44
+
45
+ options, = build({})
46
+
47
+ expect(options[:paper_width]).to eq(8.27)
48
+ expect(options[:paper_height]).to eq(11.70)
49
+ end
50
+ end
51
+
52
+ describe "unit conversion" do
53
+ %w[mm cm in px].each do |unit|
54
+ it "accepts #{unit} units on size keys" do
55
+ options, = build(print_options: { margin_top: "10#{unit}" })
56
+
57
+ expect(options[:margin_top]).to be_a(Float)
58
+ end
59
+ end
60
+
61
+ it "treats a bare number as inches" do
62
+ options, = build(print_options: { margin_top: 2 })
63
+
64
+ expect(options[:margin_top]).to eq(2.0)
65
+ end
66
+
67
+ it "raises on an unrecognized size value" do
68
+ expect { build(print_options: { margin_top: "lots" }) }.to raise_error(ArgumentError)
69
+ end
70
+ end
71
+
72
+ describe "paper format" do
73
+ it "raises on an unknown format name" do
74
+ expect { build(print_options: { format: "Poster" }) }.to raise_error(ArgumentError)
75
+ end
76
+
77
+ it "is case-insensitive" do
78
+ options, = build(print_options: { format: "a4" })
79
+
80
+ expect(options[:paper_width]).to eq(8.27)
81
+ end
82
+ end
83
+
84
+ describe "header_template / footer_template" do
85
+ it "strips them from request-supplied print_options by default" do
86
+ options, = build(print_options: { header_template: "<script>evil()</script>" })
87
+
88
+ expect(options).not_to have_key(:header_template)
89
+ end
90
+
91
+ it "still honors them from config.default_options, which is trusted app code" do
92
+ HtmlToPdf.configure { |c| c.default_options = { header_template: "<span>trusted</span>" } }
93
+
94
+ options, = build({})
95
+
96
+ expect(options[:header_template]).to eq("<span>trusted</span>")
97
+ end
98
+
99
+ it "honors a request-supplied template only when explicitly opted in" do
100
+ HtmlToPdf.configure { |c| c.allow_request_templates = true }
101
+
102
+ options, = build(print_options: { footer_template: "<span>ok</span>" })
103
+
104
+ expect(options[:footer_template]).to eq("<span>ok</span>")
105
+ end
106
+ end
107
+
108
+ describe "javascript_delay" do
109
+ it "is extracted out of the CDP options hash" do
110
+ options, delay = build(print_options: { javascript_delay: 250 })
111
+
112
+ expect(options).not_to have_key(:javascript_delay)
113
+ expect(delay).to eq(250)
114
+ end
115
+
116
+ it "is clamped to config.max_javascript_delay_ms" do
117
+ HtmlToPdf.configure { |c| c.max_javascript_delay_ms = 1_000 }
118
+
119
+ _, delay = build(print_options: { javascript_delay: 999_999 })
120
+
121
+ expect(delay).to eq(1_000)
122
+ end
123
+
124
+ it "never goes negative" do
125
+ _, delay = build(print_options: { javascript_delay: -500 })
126
+
127
+ expect(delay).to eq(0)
128
+ end
129
+
130
+ it "is nil when not given" do
131
+ _, delay = build({})
132
+
133
+ expect(delay).to be_nil
134
+ end
135
+ end
136
+
137
+ describe "legacy flag names" do
138
+ it "no longer recognizes wkhtmltopdf-style orientation/page_size" do
139
+ options, = build(print_options: { orientation: "Landscape", page_size: "A4" })
140
+
141
+ expect(options[:landscape]).to be_nil
142
+ expect(options[:orientation]).to eq("Landscape")
143
+ expect(options[:paper_width]).to be_nil
144
+ end
145
+ end
146
+
147
+ describe "key formatting" do
148
+ it "accepts dasherized keys the same as underscored ones" do
149
+ options, = build(print_options: { "margin-top" => "10mm" })
150
+
151
+ expect(options[:margin_top]).to be_within(0.0001).of(10 / 25.4)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ # These exercise the controller hook (include HtmlToPdf + around_action)
4
+ # against a real Rails dispatch, not a stub. An earlier version of this gem
5
+ # hooked in as a before_action that quietly rebuilt the controller and
6
+ # re-ran the action on the copy; Rails' own `response=` setter is documented
7
+ # to mark the response as already rendered the moment it's assigned, and
8
+ # copying the throwaway controller's instance variables back onto the real
9
+ # one carried that flag along, so `send_data` raised DoubleRenderError on
10
+ # every single PDF request once run against a real app rather than a stub.
11
+ # It also meant a before_action declared after html_to_pdf never got to run
12
+ # before the view was captured. These specs pin both down so they can't
13
+ # silently come back.
14
+ RSpec.describe "HtmlToPdf controller integration", type: :request do
15
+ around do |example|
16
+ HtmlToPdf.configure { |c| c.default_options = { print_background: true } }
17
+ example.run
18
+ ensure
19
+ HtmlToPdf::BrowserPool.reset!
20
+ end
21
+
22
+ it "does not raise DoubleRenderError and returns a real PDF" do
23
+ get "/widgets/42", params: { format: :pdf }
24
+
25
+ expect(response).to have_http_status(:ok)
26
+ expect(response.media_type).to eq("application/pdf")
27
+ expect(response.body.byteslice(0, 4)).to eq("%PDF")
28
+ end
29
+
30
+ it "runs a before_action declared after the around_action before capturing the view" do
31
+ get "/widgets/42", params: { format: :pdf }
32
+
33
+ pdf_text = extract_text(response.body)
34
+ expect(pdf_text).to include("Widget #42")
35
+ end
36
+
37
+ it "runs the action body exactly once" do
38
+ WidgetsController.show_invocations = 0
39
+
40
+ get "/widgets/7", params: { format: :pdf }
41
+
42
+ expect(WidgetsController.show_invocations).to eq(1)
43
+ end
44
+
45
+ it "leaves ordinary HTML requests completely unaffected" do
46
+ get "/widgets/42"
47
+
48
+ expect(response).to have_http_status(:ok)
49
+ expect(response.media_type).to eq("text/html")
50
+ expect(response.body).to include("Widget #42")
51
+ end
52
+
53
+ it "honors a custom layout passed via pdf_options" do
54
+ get "/widgets/5", params: { format: :pdf, pdf_options: { layout: "pdf" } }
55
+
56
+ expect(extract_text(response.body)).to include("PDF LAYOUT")
57
+ end
58
+
59
+ it "still runs rescue_from for an exception raised inside the action" do
60
+ get "/booms/1", params: { format: :pdf }
61
+
62
+ expect(response).to have_http_status(:unprocessable_content)
63
+ expect(response.body).to eq("handled")
64
+ end
65
+
66
+ def extract_text(pdf_bytes)
67
+ require "tempfile"
68
+ file = Tempfile.new(["spec", ".pdf"], binmode: true)
69
+ file.write(pdf_bytes)
70
+ file.flush
71
+ `pdftotext #{file.path} -`
72
+ ensure
73
+ file&.close!
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ include HtmlToPdf
3
+ end
@@ -0,0 +1,16 @@
1
+ class BoomsController < ApplicationController
2
+ class Boom < StandardError; end
3
+
4
+ around_action :html_to_pdf, only: [:show]
5
+ rescue_from Boom, with: :handle_boom
6
+
7
+ def show
8
+ raise Boom, "deliberate failure for spec coverage"
9
+ end
10
+
11
+ private
12
+
13
+ def handle_boom
14
+ render plain: "handled", status: :unprocessable_content
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ class WidgetsController < ApplicationController
2
+ around_action :html_to_pdf, only: [:show]
3
+
4
+ # Declared after the around_action, matching the real-world ordering that
5
+ # exposed the original gem's bug: anything this sets must still be visible
6
+ # to the view when the request is being converted to a PDF.
7
+ before_action :set_widget, only: [:show]
8
+
9
+ cattr_accessor :show_invocations
10
+ self.show_invocations = 0
11
+
12
+ def index
13
+ render plain: "widgets index"
14
+ end
15
+
16
+ def show
17
+ self.class.show_invocations += 1
18
+ end
19
+
20
+ private
21
+
22
+ def set_widget
23
+ @widget = "Widget ##{params[:id]}"
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body><%= yield %></body>
4
+ </html>
@@ -0,0 +1,4 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body><strong>PDF LAYOUT</strong><%= yield %></body>
4
+ </html>
@@ -0,0 +1,2 @@
1
+ <h1><%= @widget %></h1>
2
+ <p>Rendered at <%= Time.now.to_i %></p>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ resources :widgets, only: [:index, :show]
5
+ resources :booms, only: [:show]
6
+ end
@@ -0,0 +1 @@
1
+ *.log
File without changes
@@ -0,0 +1,31 @@
1
+ require "combustion"
2
+ Combustion.initialize! :action_controller, :action_view
3
+
4
+ require "rspec/rails"
5
+ require "htmlToPdf"
6
+
7
+ RSpec.configure do |config|
8
+ config.expect_with :rspec do |c|
9
+ c.syntax = :expect
10
+ end
11
+
12
+ config.before do
13
+ HtmlToPdf.reset_configuration!
14
+ end
15
+
16
+ # Specs tagged :chrome exercise a real headless Chrome/Chromium process.
17
+ # Skip them with a clear reason rather than a confusing Ferrum crash when
18
+ # no browser is installed, so running the suite stays contributor-friendly
19
+ # on a machine that just doesn't have Chrome.
20
+ chrome_available =
21
+ begin
22
+ browser = Ferrum::Browser.new
23
+ true
24
+ rescue Ferrum::BinaryNotFoundError
25
+ false
26
+ ensure
27
+ browser&.quit
28
+ end
29
+
30
+ config.filter_run_excluding(chrome: true) unless chrome_available
31
+ end