pdf 0.1.0 → 0.1.1
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/.claude/execution/00-overview.md +121 -0
- data/.claude/execution/01-core.md +324 -0
- data/.claude/execution/02-renderer.md +237 -0
- data/.claude/execution/03-components.md +551 -0
- data/.claude/execution/04-builders.md +322 -0
- data/.claude/execution/05-view-layout.md +362 -0
- data/.claude/execution/06-evaluators.md +494 -0
- data/.claude/execution/07-entry-extensibility.md +435 -0
- data/.claude/execution/08-integration-tests.md +978 -0
- data/Rakefile +7 -3
- data/examples/01_basic_invoice.rb +139 -0
- data/examples/02_report_with_layout.rb +266 -0
- data/examples/03_inherited_views.rb +318 -0
- data/examples/04_conditional_content.rb +421 -0
- data/examples/05_custom_components.rb +442 -0
- data/examples/README.md +123 -0
- data/lib/pdf/blueprint.rb +50 -0
- data/lib/pdf/builders/content_builder.rb +96 -0
- data/lib/pdf/builders/footer_builder.rb +24 -0
- data/lib/pdf/builders/header_builder.rb +31 -0
- data/lib/pdf/component.rb +43 -0
- data/lib/pdf/components/alert.rb +42 -0
- data/lib/pdf/components/context.rb +16 -0
- data/lib/pdf/components/date.rb +28 -0
- data/lib/pdf/components/heading.rb +12 -0
- data/lib/pdf/components/hr.rb +12 -0
- data/lib/pdf/components/logo.rb +15 -0
- data/lib/pdf/components/paragraph.rb +12 -0
- data/lib/pdf/components/qr_code.rb +38 -0
- data/lib/pdf/components/spacer.rb +11 -0
- data/lib/pdf/components/span.rb +12 -0
- data/lib/pdf/components/subtitle.rb +12 -0
- data/lib/pdf/components/table.rb +48 -0
- data/lib/pdf/components/title.rb +12 -0
- data/lib/pdf/content_evaluator.rb +218 -0
- data/lib/pdf/dynamic_components.rb +17 -0
- data/lib/pdf/footer_evaluator.rb +66 -0
- data/lib/pdf/header_evaluator.rb +56 -0
- data/lib/pdf/layout.rb +61 -0
- data/lib/pdf/renderer.rb +153 -0
- data/lib/pdf/resolver.rb +36 -0
- data/lib/pdf/version.rb +1 -1
- data/lib/pdf/view.rb +113 -0
- data/lib/pdf.rb +74 -1
- metadata +127 -2
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
# Phase 8: Integration Tests
|
|
2
|
+
|
|
3
|
+
Comprehensive integration tests covering full PDF generation workflows.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
- `test/integration/basic_view_test.rb`
|
|
7
|
+
- `test/integration/layout_test.rb`
|
|
8
|
+
- `test/integration/inheritance_test.rb`
|
|
9
|
+
- `test/integration/conditional_rendering_test.rb`
|
|
10
|
+
- `test/integration/components_test.rb`
|
|
11
|
+
- `test/integration/error_handling_test.rb`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 8.1 Basic View Integration
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# test/integration/basic_view_test.rb
|
|
19
|
+
# frozen_string_literal: true
|
|
20
|
+
|
|
21
|
+
require "test_helper"
|
|
22
|
+
|
|
23
|
+
class BasicViewIntegrationTest < Minitest::Test
|
|
24
|
+
def test_simple_view_generates_valid_pdf
|
|
25
|
+
view_class = Class.new(Pdf::View) do
|
|
26
|
+
title "Test Document"
|
|
27
|
+
subtitle "A Subtitle"
|
|
28
|
+
paragraph "This is a test paragraph with some content."
|
|
29
|
+
hr
|
|
30
|
+
spacer amount: 20
|
|
31
|
+
heading "Section Heading"
|
|
32
|
+
span "Some inline text"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
pdf = view_class.new.to_pdf
|
|
36
|
+
assert pdf.start_with?("%PDF")
|
|
37
|
+
assert pdf.length > 1000
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_simple_view_contains_expected_text
|
|
41
|
+
view_class = Class.new(Pdf::View) do
|
|
42
|
+
title "Test Document"
|
|
43
|
+
subtitle "A Subtitle"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
pdf = view_class.new.to_pdf
|
|
47
|
+
assert_includes pdf, "Test Document"
|
|
48
|
+
assert_includes pdf, "A Subtitle"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_view_with_data_binding_resolves_symbols
|
|
52
|
+
view_class = Class.new(Pdf::View) do
|
|
53
|
+
title :document_title
|
|
54
|
+
paragraph :description
|
|
55
|
+
date :created_at
|
|
56
|
+
|
|
57
|
+
def document_title = data[:title]
|
|
58
|
+
def description = "Created by #{data[:author]}"
|
|
59
|
+
def created_at = data[:date]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
view = view_class.new(
|
|
63
|
+
title: "Dynamic Title",
|
|
64
|
+
author: "John Doe",
|
|
65
|
+
date: Date.new(2024, 6, 15)
|
|
66
|
+
)
|
|
67
|
+
pdf = view.to_pdf
|
|
68
|
+
assert_includes pdf, "Dynamic Title"
|
|
69
|
+
assert_includes pdf, "John Doe"
|
|
70
|
+
assert_includes pdf, "June 15, 2024"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_view_with_table_renders_from_array_of_hashes
|
|
74
|
+
view_class = Class.new(Pdf::View) do
|
|
75
|
+
title "Table Report"
|
|
76
|
+
table :items, columns: [
|
|
77
|
+
{ key: :name, label: "Name" },
|
|
78
|
+
{ key: :qty, label: "Quantity" },
|
|
79
|
+
{ key: :price, label: "Price" }
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
def items = data[:items]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
view = view_class.new(items: [
|
|
86
|
+
{ name: "Widget", qty: 5, price: "$10" },
|
|
87
|
+
{ name: "Gadget", qty: 3, price: "$25" }
|
|
88
|
+
])
|
|
89
|
+
pdf = view.to_pdf
|
|
90
|
+
assert pdf.start_with?("%PDF")
|
|
91
|
+
assert_includes pdf, "Widget"
|
|
92
|
+
assert_includes pdf, "Gadget"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_view_with_empty_table
|
|
96
|
+
view_class = Class.new(Pdf::View) do
|
|
97
|
+
title "Table Report"
|
|
98
|
+
table :items, columns: [:name, :qty]
|
|
99
|
+
def items = data[:items]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
view = view_class.new(items: [])
|
|
103
|
+
view.to_pdf # Should not raise
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_view_with_sections
|
|
107
|
+
view_class = Class.new(Pdf::View) do
|
|
108
|
+
title "Sectioned Document"
|
|
109
|
+
|
|
110
|
+
section "First Section" do
|
|
111
|
+
paragraph "Content of first section"
|
|
112
|
+
table :section1_data, columns: [:key, :value]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
section "Second Section" do
|
|
116
|
+
paragraph "Content of second section"
|
|
117
|
+
alert title: "Notice", color: "blue"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def section1_data
|
|
121
|
+
[{ key: "A", value: "1" }, { key: "B", value: "2" }]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
pdf = view_class.new.to_pdf
|
|
126
|
+
assert_includes pdf, "First Section"
|
|
127
|
+
assert_includes pdf, "Second Section"
|
|
128
|
+
assert_includes pdf, "Content of first section"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_view_with_each_loop_iterates
|
|
132
|
+
view_class = Class.new(Pdf::View) do
|
|
133
|
+
title "Items List"
|
|
134
|
+
|
|
135
|
+
each(:items) do |item|
|
|
136
|
+
heading item[:name]
|
|
137
|
+
paragraph item[:description]
|
|
138
|
+
hr
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def items = data[:items]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
view = view_class.new(items: [
|
|
145
|
+
{ name: "Item 1", description: "First item" },
|
|
146
|
+
{ name: "Item 2", description: "Second item" },
|
|
147
|
+
{ name: "Item 3", description: "Third item" }
|
|
148
|
+
])
|
|
149
|
+
pdf = view.to_pdf
|
|
150
|
+
assert_includes pdf, "Item 1"
|
|
151
|
+
assert_includes pdf, "Item 2"
|
|
152
|
+
assert_includes pdf, "Item 3"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_view_with_empty_each_collection
|
|
156
|
+
view_class = Class.new(Pdf::View) do
|
|
157
|
+
each(:items) { |item| paragraph item[:name] }
|
|
158
|
+
def items = data[:items]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
view = view_class.new(items: [])
|
|
162
|
+
view.to_pdf # Should not raise
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 8.2 Layout Integration
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# test/integration/layout_test.rb
|
|
173
|
+
# frozen_string_literal: true
|
|
174
|
+
|
|
175
|
+
require "test_helper"
|
|
176
|
+
|
|
177
|
+
class LayoutIntegrationTest < Minitest::Test
|
|
178
|
+
def test_view_with_layout_applies_margins
|
|
179
|
+
layout_class = Class.new(Pdf::Layout) do
|
|
180
|
+
header do
|
|
181
|
+
context :header_lines
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
footer do
|
|
185
|
+
text :footer_text, size: 8
|
|
186
|
+
page_number position: :right, format: "Page %<page>d of %<total>d"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
margins top: 80, bottom: 70, left: 50, right: 50
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
lc = layout_class
|
|
193
|
+
view_class = Class.new(Pdf::View) do
|
|
194
|
+
layout lc
|
|
195
|
+
|
|
196
|
+
title "Document with Layout"
|
|
197
|
+
paragraph :content
|
|
198
|
+
|
|
199
|
+
def header_lines
|
|
200
|
+
["Company: #{data[:company]}", "Date: #{Date.today}"]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def footer_text = "Confidential - #{data[:company]}"
|
|
204
|
+
def content = data[:content]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
pdf = view_class.new(company: "Acme Corp", content: "Test content").to_pdf
|
|
208
|
+
assert pdf.start_with?("%PDF")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_view_with_layout_renders_header
|
|
212
|
+
layout_class = Class.new(Pdf::Layout) do
|
|
213
|
+
header do
|
|
214
|
+
context :header_lines
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
lc = layout_class
|
|
219
|
+
view_class = Class.new(Pdf::View) do
|
|
220
|
+
layout lc
|
|
221
|
+
title "Doc"
|
|
222
|
+
def header_lines = ["Company: #{data[:company]}"]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
pdf = view_class.new(company: "Test Co").to_pdf
|
|
226
|
+
assert_includes pdf, "Test Co"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_layout_with_qr_code_generates_from_data
|
|
230
|
+
layout_class = Class.new(Pdf::Layout) do
|
|
231
|
+
header do
|
|
232
|
+
qr_code data: :qr_data, size: 50, position: :right
|
|
233
|
+
context :header_info
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
footer do
|
|
237
|
+
page_number
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
lc = layout_class
|
|
242
|
+
view_class = Class.new(Pdf::View) do
|
|
243
|
+
layout lc
|
|
244
|
+
|
|
245
|
+
title "QR Code Document"
|
|
246
|
+
paragraph "Document with QR code in header"
|
|
247
|
+
|
|
248
|
+
def qr_data = "https://example.com/doc/#{data[:id]}"
|
|
249
|
+
def header_info = ["Document ##{data[:id]}"]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
pdf = view_class.new(id: "12345").to_pdf
|
|
253
|
+
assert pdf.start_with?("%PDF")
|
|
254
|
+
assert pdf.length > 5000 # QR code adds size
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_multi_page_document_with_footer
|
|
258
|
+
layout_class = Class.new(Pdf::Layout) do
|
|
259
|
+
footer do
|
|
260
|
+
text "Footer on every page"
|
|
261
|
+
page_number position: :right
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
lc = layout_class
|
|
266
|
+
view_class = Class.new(Pdf::View) do
|
|
267
|
+
layout lc
|
|
268
|
+
|
|
269
|
+
title "Multi-Page Document"
|
|
270
|
+
|
|
271
|
+
each(:pages) do |page|
|
|
272
|
+
heading page[:title]
|
|
273
|
+
paragraph page[:content]
|
|
274
|
+
page_break
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def pages = data[:pages]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
pages = 5.times.map { |i| { title: "Page #{i + 1}", content: "Content " * 100 } }
|
|
281
|
+
pdf = view_class.new(pages: pages).to_pdf
|
|
282
|
+
assert pdf.start_with?("%PDF")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 8.3 Inheritance Integration
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# test/integration/inheritance_test.rb
|
|
293
|
+
# frozen_string_literal: true
|
|
294
|
+
|
|
295
|
+
require "test_helper"
|
|
296
|
+
|
|
297
|
+
class InheritanceIntegrationTest < Minitest::Test
|
|
298
|
+
def test_child_includes_parent_elements
|
|
299
|
+
base_view = Class.new(Pdf::View) do
|
|
300
|
+
title "Base Report"
|
|
301
|
+
hr
|
|
302
|
+
spacer amount: 10
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
child_view = Class.new(base_view) do
|
|
306
|
+
subtitle "Child Section"
|
|
307
|
+
paragraph :content
|
|
308
|
+
|
|
309
|
+
def content = data[:content]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
pdf = child_view.new(content: "Child content").to_pdf
|
|
313
|
+
assert_includes pdf, "Base Report"
|
|
314
|
+
assert_includes pdf, "Child Section"
|
|
315
|
+
assert_includes pdf, "Child content"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def test_parent_remains_unchanged_after_child_access
|
|
319
|
+
base_view = Class.new(Pdf::View) do
|
|
320
|
+
title "Base Report"
|
|
321
|
+
hr
|
|
322
|
+
spacer amount: 10
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
child_view = Class.new(base_view) do
|
|
326
|
+
subtitle "Child Section"
|
|
327
|
+
paragraph :content
|
|
328
|
+
def content = data[:content]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Trigger child
|
|
332
|
+
child_view.new(content: "test").to_pdf
|
|
333
|
+
|
|
334
|
+
# Parent should still only have its own elements
|
|
335
|
+
assert_equal 3, base_view.blueprint.elements.size
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def test_maintains_element_order
|
|
339
|
+
base_view = Class.new(Pdf::View) do
|
|
340
|
+
title "Base Report"
|
|
341
|
+
hr
|
|
342
|
+
spacer amount: 10
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
child_view = Class.new(base_view) do
|
|
346
|
+
subtitle "Child Section"
|
|
347
|
+
paragraph "Content"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
elements = child_view.blueprint.elements
|
|
351
|
+
types = elements.map { |e| e[:type] }
|
|
352
|
+
|
|
353
|
+
assert_equal [:title, :hr, :spacer, :subtitle, :paragraph], types
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def test_multi_level_inheritance_accumulates_elements
|
|
357
|
+
level1 = Class.new(Pdf::View) do
|
|
358
|
+
title "Level 1"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
level2 = Class.new(level1) do
|
|
362
|
+
subtitle "Level 2"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
level3 = Class.new(level2) do
|
|
366
|
+
paragraph "Level 3"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
assert_equal 1, level1.blueprint.elements.size
|
|
370
|
+
assert_equal 2, level2.blueprint.elements.size
|
|
371
|
+
assert_equal 3, level3.blueprint.elements.size
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_multi_level_inheritance_generates_pdf_with_all_elements
|
|
375
|
+
level1 = Class.new(Pdf::View) do
|
|
376
|
+
title "Level 1"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
level2 = Class.new(level1) do
|
|
380
|
+
subtitle "Level 2"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
level3 = Class.new(level2) do
|
|
384
|
+
paragraph "Level 3"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
pdf = level3.new.to_pdf
|
|
388
|
+
assert_includes pdf, "Level 1"
|
|
389
|
+
assert_includes pdf, "Level 2"
|
|
390
|
+
assert_includes pdf, "Level 3"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def test_child_inherits_layout_from_parent
|
|
394
|
+
shared_layout = Class.new(Pdf::Layout) do
|
|
395
|
+
footer do
|
|
396
|
+
text "Shared Footer"
|
|
397
|
+
page_number
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
lc = shared_layout
|
|
402
|
+
base_with_layout = Class.new(Pdf::View) do
|
|
403
|
+
layout lc
|
|
404
|
+
title "Base with Layout"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
child_with_layout = Class.new(base_with_layout) do
|
|
408
|
+
paragraph "Extended content"
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
assert_equal shared_layout, child_with_layout.blueprint.layout_class
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_generates_pdf_with_inherited_layout
|
|
415
|
+
shared_layout = Class.new(Pdf::Layout) do
|
|
416
|
+
footer do
|
|
417
|
+
text "Shared Footer"
|
|
418
|
+
page_number
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
lc = shared_layout
|
|
423
|
+
base_with_layout = Class.new(Pdf::View) do
|
|
424
|
+
layout lc
|
|
425
|
+
title "Base with Layout"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
child_with_layout = Class.new(base_with_layout) do
|
|
429
|
+
paragraph "Extended content"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
pdf = child_with_layout.new.to_pdf
|
|
433
|
+
assert_includes pdf, "Base with Layout"
|
|
434
|
+
assert_includes pdf, "Extended content"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 8.4 Conditional Rendering Integration
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# test/integration/conditional_rendering_test.rb
|
|
445
|
+
# frozen_string_literal: true
|
|
446
|
+
|
|
447
|
+
require "test_helper"
|
|
448
|
+
|
|
449
|
+
class ConditionalRenderingIntegrationTest < Minitest::Test
|
|
450
|
+
def test_render_if_renders_when_condition_true
|
|
451
|
+
view_class = Class.new(Pdf::View) do
|
|
452
|
+
title "Conditional Document"
|
|
453
|
+
|
|
454
|
+
render_if(:show_summary) do
|
|
455
|
+
alert title: "Summary", description: "This is the summary", color: "blue"
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
render_if(:show_details) do
|
|
459
|
+
section "Details" do
|
|
460
|
+
table :detail_items, columns: [:key, :value]
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
paragraph "Always visible"
|
|
465
|
+
|
|
466
|
+
def show_summary = data[:summary]
|
|
467
|
+
def show_details = data[:details]
|
|
468
|
+
def detail_items = [{ key: "A", value: "1" }]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
pdf = view_class.new(summary: true, details: false).to_pdf
|
|
472
|
+
assert_includes pdf, "Summary"
|
|
473
|
+
refute_includes pdf, "Details"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def test_render_if_skips_when_condition_false
|
|
477
|
+
view_class = Class.new(Pdf::View) do
|
|
478
|
+
title "Conditional Document"
|
|
479
|
+
|
|
480
|
+
render_if(:show_summary) do
|
|
481
|
+
alert title: "Summary", color: "blue"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
paragraph "Always visible"
|
|
485
|
+
|
|
486
|
+
def show_summary = data[:summary]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
pdf = view_class.new(summary: false).to_pdf
|
|
490
|
+
refute_includes pdf, "Summary"
|
|
491
|
+
assert_includes pdf, "Always visible"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def test_render_if_handles_multiple_conditions
|
|
495
|
+
view_class = Class.new(Pdf::View) do
|
|
496
|
+
render_if(:show_summary) do
|
|
497
|
+
alert title: "Summary", color: "blue"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
render_if(:show_details) do
|
|
501
|
+
section "Details" do
|
|
502
|
+
paragraph "Detail content"
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def show_summary = data[:summary]
|
|
507
|
+
def show_details = data[:details]
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
pdf = view_class.new(summary: true, details: true).to_pdf
|
|
511
|
+
assert_includes pdf, "Summary"
|
|
512
|
+
assert_includes pdf, "Details"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def test_render_unless_renders_when_condition_false
|
|
516
|
+
view_class = Class.new(Pdf::View) do
|
|
517
|
+
title "Document"
|
|
518
|
+
|
|
519
|
+
render_unless(:is_draft) do
|
|
520
|
+
alert title: "FINAL", color: "green"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
render_unless(:hide_watermark) do
|
|
524
|
+
paragraph "CONFIDENTIAL"
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def is_draft = data[:draft]
|
|
528
|
+
def hide_watermark = data[:no_watermark]
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
pdf = view_class.new(draft: false, no_watermark: false).to_pdf
|
|
532
|
+
assert_includes pdf, "FINAL"
|
|
533
|
+
assert_includes pdf, "CONFIDENTIAL"
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def test_render_unless_skips_when_condition_true
|
|
537
|
+
view_class = Class.new(Pdf::View) do
|
|
538
|
+
title "Document"
|
|
539
|
+
|
|
540
|
+
render_unless(:is_draft) do
|
|
541
|
+
alert title: "FINAL", color: "green"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
render_unless(:hide_watermark) do
|
|
545
|
+
paragraph "CONFIDENTIAL"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def is_draft = data[:draft]
|
|
549
|
+
def hide_watermark = data[:no_watermark]
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
pdf = view_class.new(draft: true, no_watermark: true).to_pdf
|
|
553
|
+
refute_includes pdf, "FINAL"
|
|
554
|
+
refute_includes pdf, "CONFIDENTIAL"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def test_nested_conditionals
|
|
558
|
+
view_class = Class.new(Pdf::View) do
|
|
559
|
+
title "Nested Conditions"
|
|
560
|
+
|
|
561
|
+
render_if(:level1) do
|
|
562
|
+
heading "Level 1 Visible"
|
|
563
|
+
|
|
564
|
+
render_if(:level2) do
|
|
565
|
+
paragraph "Level 2 Also Visible"
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def level1 = data[:l1]
|
|
570
|
+
def level2 = data[:l2]
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
pdf = view_class.new(l1: true, l2: true).to_pdf
|
|
574
|
+
assert_includes pdf, "Level 1 Visible"
|
|
575
|
+
assert_includes pdf, "Level 2 Also Visible"
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def test_nested_conditionals_outer_false_prevents_inner
|
|
579
|
+
view_class = Class.new(Pdf::View) do
|
|
580
|
+
title "Nested Conditions"
|
|
581
|
+
|
|
582
|
+
render_if(:level1) do
|
|
583
|
+
heading "Level 1 Visible"
|
|
584
|
+
|
|
585
|
+
render_if(:level2) do
|
|
586
|
+
paragraph "Level 2 Also Visible"
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def level1 = data[:l1]
|
|
591
|
+
def level2 = data[:l2]
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
pdf = view_class.new(l1: false, l2: true).to_pdf
|
|
595
|
+
refute_includes pdf, "Level 1 Visible"
|
|
596
|
+
refute_includes pdf, "Level 2 Also Visible"
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def test_conditional_with_complex_boolean_logic
|
|
600
|
+
view_class = Class.new(Pdf::View) do
|
|
601
|
+
title "Complex Logic"
|
|
602
|
+
|
|
603
|
+
render_if(:has_premium_features) do
|
|
604
|
+
section "Premium Features" do
|
|
605
|
+
paragraph "Exclusive content"
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def has_premium_features
|
|
610
|
+
data[:plan] == "premium" && data[:active]
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
premium_active = view_class.new(plan: "premium", active: true).to_pdf
|
|
615
|
+
assert_includes premium_active, "Premium Features"
|
|
616
|
+
|
|
617
|
+
premium_inactive = view_class.new(plan: "premium", active: false).to_pdf
|
|
618
|
+
refute_includes premium_inactive, "Premium Features"
|
|
619
|
+
|
|
620
|
+
basic_active = view_class.new(plan: "basic", active: true).to_pdf
|
|
621
|
+
refute_includes basic_active, "Premium Features"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## 8.5 Components Integration
|
|
629
|
+
|
|
630
|
+
```ruby
|
|
631
|
+
# test/integration/components_test.rb
|
|
632
|
+
# frozen_string_literal: true
|
|
633
|
+
|
|
634
|
+
require "test_helper"
|
|
635
|
+
|
|
636
|
+
class ComponentsIntegrationTest < Minitest::Test
|
|
637
|
+
def test_all_built_in_components_render_without_error
|
|
638
|
+
view_class = Class.new(Pdf::View) do
|
|
639
|
+
title "Component Showcase", size: 24
|
|
640
|
+
subtitle "All Components Demo"
|
|
641
|
+
date :today, format: "%Y-%m-%d"
|
|
642
|
+
hr
|
|
643
|
+
spacer amount: 15
|
|
644
|
+
heading "Main Heading", size: 16
|
|
645
|
+
paragraph "A paragraph with content.", leading: 4
|
|
646
|
+
span "Inline span text"
|
|
647
|
+
|
|
648
|
+
alert title: "Info Alert", description: "This is informational", color: "blue"
|
|
649
|
+
alert title: "Success", color: "green"
|
|
650
|
+
alert title: "Warning", color: "orange"
|
|
651
|
+
alert title: "Error", color: "red"
|
|
652
|
+
|
|
653
|
+
section "Data Section" do
|
|
654
|
+
table :sample_table, columns: [:id, :name, :status]
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
context :context_lines, label: "METADATA"
|
|
658
|
+
|
|
659
|
+
def today = Date.today
|
|
660
|
+
def sample_table
|
|
661
|
+
[
|
|
662
|
+
{ id: 1, name: "Alpha", status: "Active" },
|
|
663
|
+
{ id: 2, name: "Beta", status: "Pending" }
|
|
664
|
+
]
|
|
665
|
+
end
|
|
666
|
+
def context_lines
|
|
667
|
+
["Line 1: Value", "Line 2: Another Value"]
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
view_class.new.to_pdf # Should not raise
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def test_all_components_generate_substantial_pdf
|
|
675
|
+
view_class = Class.new(Pdf::View) do
|
|
676
|
+
title "Component Showcase", size: 24
|
|
677
|
+
subtitle "All Components Demo"
|
|
678
|
+
alert title: "Info", color: "blue"
|
|
679
|
+
section "Data" do
|
|
680
|
+
table :items, columns: [:a, :b]
|
|
681
|
+
end
|
|
682
|
+
def items = [{ a: "1", b: "2" }]
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
pdf = view_class.new.to_pdf
|
|
686
|
+
assert pdf.length > 10_000
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def test_date_component_formats_dates_correctly
|
|
690
|
+
view_class = Class.new(Pdf::View) do
|
|
691
|
+
date :report_date
|
|
692
|
+
date :custom_date, format: "%d/%m/%Y"
|
|
693
|
+
date "String date"
|
|
694
|
+
|
|
695
|
+
def report_date = Date.new(2024, 12, 25)
|
|
696
|
+
def custom_date = Date.new(2024, 1, 15)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
pdf = view_class.new.to_pdf
|
|
700
|
+
assert_includes pdf, "December 25, 2024"
|
|
701
|
+
assert_includes pdf, "15/01/2024"
|
|
702
|
+
assert_includes pdf, "String date"
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def test_table_with_various_data_formats
|
|
706
|
+
view_class = Class.new(Pdf::View) do
|
|
707
|
+
# Array of hashes with columns
|
|
708
|
+
table :hash_data, columns: [:a, :b]
|
|
709
|
+
|
|
710
|
+
# Pre-formatted data
|
|
711
|
+
table :formatted_data
|
|
712
|
+
|
|
713
|
+
# With percentage widths
|
|
714
|
+
table :width_data, columns: [:col1, :col2], widths: ["70%", "30%"]
|
|
715
|
+
|
|
716
|
+
def hash_data
|
|
717
|
+
[{ a: "A1", b: "B1" }, { a: "A2", b: "B2" }]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def formatted_data
|
|
721
|
+
{ headers: ["X", "Y"], rows: [["x1", "y1"], ["x2", "y2"]] }
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def width_data
|
|
725
|
+
[{ col1: "Wide", col2: "Narrow" }]
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
pdf = view_class.new.to_pdf
|
|
730
|
+
assert_includes pdf, "A1"
|
|
731
|
+
assert_includes pdf, "x1"
|
|
732
|
+
assert_includes pdf, "Wide"
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def test_custom_registered_component_in_dsl
|
|
736
|
+
badge_class = Class.new(Pdf::Component) do
|
|
737
|
+
def render(text, style: "default", **_opts)
|
|
738
|
+
@pdf.text "[#{style.upcase}] #{text}", size: 10
|
|
739
|
+
move_down 5
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
Pdf.register_component(:badge, badge_class)
|
|
743
|
+
|
|
744
|
+
view_class = Class.new(Pdf::View) do
|
|
745
|
+
title "Custom Component Test"
|
|
746
|
+
badge "Active", style: "success"
|
|
747
|
+
badge "Warning", style: "warn"
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
pdf = view_class.new.to_pdf
|
|
751
|
+
assert_includes pdf, "[SUCCESS] Active"
|
|
752
|
+
assert_includes pdf, "[WARN] Warning"
|
|
753
|
+
ensure
|
|
754
|
+
Pdf.instance_variable_get(:@components).delete(:badge)
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 8.6 Error Handling Integration
|
|
762
|
+
|
|
763
|
+
```ruby
|
|
764
|
+
# test/integration/error_handling_test.rb
|
|
765
|
+
# frozen_string_literal: true
|
|
766
|
+
|
|
767
|
+
require "test_helper"
|
|
768
|
+
|
|
769
|
+
class ErrorHandlingIntegrationTest < Minitest::Test
|
|
770
|
+
def test_raises_file_not_found_error_for_missing_logo
|
|
771
|
+
renderer = Pdf::Renderer.new.setup
|
|
772
|
+
logo = Pdf::Components::Logo.new(renderer.pdf)
|
|
773
|
+
|
|
774
|
+
error = assert_raises(Pdf::FileNotFoundError) do
|
|
775
|
+
logo.render("/nonexistent/logo.png")
|
|
776
|
+
end
|
|
777
|
+
assert_match(/Logo not found/, error.message)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def test_raises_file_not_found_error_for_missing_image
|
|
781
|
+
renderer = Pdf::Renderer.new.setup
|
|
782
|
+
|
|
783
|
+
error = assert_raises(Pdf::FileNotFoundError) do
|
|
784
|
+
renderer.image("/nonexistent/image.png")
|
|
785
|
+
end
|
|
786
|
+
assert_match(/Image not found/, error.message)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def test_raises_resolution_error_for_undefined_symbols
|
|
790
|
+
view_class = Class.new(Pdf::View) do
|
|
791
|
+
title :undefined_method
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
view = view_class.new
|
|
795
|
+
error = assert_raises(Pdf::ResolutionError) do
|
|
796
|
+
view.to_pdf
|
|
797
|
+
end
|
|
798
|
+
assert_match(/Cannot resolve/, error.message)
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def test_raises_argument_error_for_non_component_class
|
|
802
|
+
error = assert_raises(ArgumentError) do
|
|
803
|
+
Pdf.register_component(:bad, String)
|
|
804
|
+
end
|
|
805
|
+
assert_match(/must inherit from Pdf::Component/, error.message)
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def test_raises_error_for_unknown_component
|
|
809
|
+
error = assert_raises(Pdf::Error) do
|
|
810
|
+
Pdf.component(:nonexistent)
|
|
811
|
+
end
|
|
812
|
+
assert_match(/Unknown component/, error.message)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def test_raises_for_invalid_table_width_format
|
|
816
|
+
renderer = Pdf::Renderer.new.setup
|
|
817
|
+
table = Pdf::Components::Table.new(renderer.pdf)
|
|
818
|
+
|
|
819
|
+
error = assert_raises(ArgumentError) do
|
|
820
|
+
table.render(headers: ["A"], rows: [["1"]], widths: [{ invalid: true }])
|
|
821
|
+
end
|
|
822
|
+
assert_match(/Invalid width/, error.message)
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def test_raises_for_nil_context_lines
|
|
826
|
+
renderer = Pdf::Renderer.new.setup
|
|
827
|
+
context = Pdf::Components::Context.new(renderer.pdf)
|
|
828
|
+
|
|
829
|
+
error = assert_raises(ArgumentError) do
|
|
830
|
+
context.render(nil)
|
|
831
|
+
end
|
|
832
|
+
assert_match(/cannot be nil/, error.message)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def test_handles_empty_collections_gracefully
|
|
836
|
+
view_class = Class.new(Pdf::View) do
|
|
837
|
+
title "Safe Document"
|
|
838
|
+
|
|
839
|
+
# Empty collection - should not error
|
|
840
|
+
each(:items) { |i| paragraph i[:name] }
|
|
841
|
+
|
|
842
|
+
# Empty context - should not error
|
|
843
|
+
render_if(:has_context) do
|
|
844
|
+
context :ctx_lines
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def items = []
|
|
848
|
+
def has_context = false
|
|
849
|
+
def ctx_lines = []
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
view_class.new.to_pdf # Should not raise
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## 8.7 Raw Prawn Access Integration
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
# test/integration/raw_access_test.rb
|
|
863
|
+
# frozen_string_literal: true
|
|
864
|
+
|
|
865
|
+
require "test_helper"
|
|
866
|
+
|
|
867
|
+
class RawAccessIntegrationTest < Minitest::Test
|
|
868
|
+
def test_raw_block_executes_prawn_commands
|
|
869
|
+
view_class = Class.new(Pdf::View) do
|
|
870
|
+
title "Document with Raw Access"
|
|
871
|
+
|
|
872
|
+
raw do |pdf|
|
|
873
|
+
pdf.stroke_color "FF0000"
|
|
874
|
+
pdf.line_width 2
|
|
875
|
+
pdf.stroke_horizontal_line 0, pdf.bounds.width
|
|
876
|
+
pdf.stroke_color "000000"
|
|
877
|
+
pdf.line_width 1
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
paragraph "Content after raw block"
|
|
881
|
+
|
|
882
|
+
raw do |pdf|
|
|
883
|
+
pdf.fill_color "CCCCCC"
|
|
884
|
+
pdf.fill_rectangle [0, pdf.cursor], 100, 20
|
|
885
|
+
pdf.fill_color "000000"
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
pdf = view_class.new.to_pdf
|
|
890
|
+
assert pdf.start_with?("%PDF")
|
|
891
|
+
assert_includes pdf, "Content after raw block"
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def test_complex_raw_drawing
|
|
895
|
+
view_class = Class.new(Pdf::View) do
|
|
896
|
+
title "Custom Drawing"
|
|
897
|
+
|
|
898
|
+
raw do |pdf|
|
|
899
|
+
# Draw a simple chart-like element
|
|
900
|
+
pdf.stroke_color "333333"
|
|
901
|
+
|
|
902
|
+
# Axes
|
|
903
|
+
pdf.stroke do
|
|
904
|
+
pdf.line [50, 200], [50, 50] # Y axis
|
|
905
|
+
pdf.line [50, 50], [250, 50] # X axis
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Bars
|
|
909
|
+
pdf.fill_color "4CAF50"
|
|
910
|
+
pdf.fill_rectangle [70, 150], 30, 100
|
|
911
|
+
pdf.fill_color "2196F3"
|
|
912
|
+
pdf.fill_rectangle [120, 120], 30, 70
|
|
913
|
+
pdf.fill_color "FF9800"
|
|
914
|
+
pdf.fill_rectangle [170, 180], 30, 130
|
|
915
|
+
|
|
916
|
+
pdf.fill_color "000000"
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
paragraph "Chart rendered above"
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
view_class.new.to_pdf # Should not raise
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def test_renderer_raw_yields_prawn_document
|
|
926
|
+
renderer = Pdf::Renderer.new.setup
|
|
927
|
+
yielded = nil
|
|
928
|
+
|
|
929
|
+
renderer.raw { |pdf| yielded = pdf }
|
|
930
|
+
|
|
931
|
+
assert_equal renderer.pdf, yielded
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## Running Integration Tests
|
|
939
|
+
|
|
940
|
+
```bash
|
|
941
|
+
# Run all tests
|
|
942
|
+
bundle exec rake test
|
|
943
|
+
|
|
944
|
+
# Run specific test file
|
|
945
|
+
bundle exec ruby -Itest test/integration/basic_view_test.rb
|
|
946
|
+
|
|
947
|
+
# Run with verbose output
|
|
948
|
+
bundle exec rake test TESTOPTS="--verbose"
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
## Test Helpers
|
|
952
|
+
|
|
953
|
+
```ruby
|
|
954
|
+
# test/test_helper.rb
|
|
955
|
+
# frozen_string_literal: true
|
|
956
|
+
|
|
957
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
958
|
+
|
|
959
|
+
require "pdf"
|
|
960
|
+
require "minitest/autorun"
|
|
961
|
+
|
|
962
|
+
# Optional: Add helper methods available to all tests
|
|
963
|
+
module PdfTestHelpers
|
|
964
|
+
def extract_text(pdf_binary)
|
|
965
|
+
# Basic text extraction - for more robust extraction use pdf-reader gem
|
|
966
|
+
pdf_binary.scan(/\((.*?)\)/).flatten.join(" ")
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def pdf_page_count(pdf_binary)
|
|
970
|
+
pdf_binary.scan(/\/Type\s*\/Page[^s]/).size
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Include helpers in all tests
|
|
975
|
+
class Minitest::Test
|
|
976
|
+
include PdfTestHelpers
|
|
977
|
+
end
|
|
978
|
+
```
|