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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/execution/00-overview.md +121 -0
  3. data/.claude/execution/01-core.md +324 -0
  4. data/.claude/execution/02-renderer.md +237 -0
  5. data/.claude/execution/03-components.md +551 -0
  6. data/.claude/execution/04-builders.md +322 -0
  7. data/.claude/execution/05-view-layout.md +362 -0
  8. data/.claude/execution/06-evaluators.md +494 -0
  9. data/.claude/execution/07-entry-extensibility.md +435 -0
  10. data/.claude/execution/08-integration-tests.md +978 -0
  11. data/Rakefile +7 -3
  12. data/examples/01_basic_invoice.rb +139 -0
  13. data/examples/02_report_with_layout.rb +266 -0
  14. data/examples/03_inherited_views.rb +318 -0
  15. data/examples/04_conditional_content.rb +421 -0
  16. data/examples/05_custom_components.rb +442 -0
  17. data/examples/README.md +123 -0
  18. data/lib/pdf/blueprint.rb +50 -0
  19. data/lib/pdf/builders/content_builder.rb +96 -0
  20. data/lib/pdf/builders/footer_builder.rb +24 -0
  21. data/lib/pdf/builders/header_builder.rb +31 -0
  22. data/lib/pdf/component.rb +43 -0
  23. data/lib/pdf/components/alert.rb +42 -0
  24. data/lib/pdf/components/context.rb +16 -0
  25. data/lib/pdf/components/date.rb +28 -0
  26. data/lib/pdf/components/heading.rb +12 -0
  27. data/lib/pdf/components/hr.rb +12 -0
  28. data/lib/pdf/components/logo.rb +15 -0
  29. data/lib/pdf/components/paragraph.rb +12 -0
  30. data/lib/pdf/components/qr_code.rb +38 -0
  31. data/lib/pdf/components/spacer.rb +11 -0
  32. data/lib/pdf/components/span.rb +12 -0
  33. data/lib/pdf/components/subtitle.rb +12 -0
  34. data/lib/pdf/components/table.rb +48 -0
  35. data/lib/pdf/components/title.rb +12 -0
  36. data/lib/pdf/content_evaluator.rb +218 -0
  37. data/lib/pdf/dynamic_components.rb +17 -0
  38. data/lib/pdf/footer_evaluator.rb +66 -0
  39. data/lib/pdf/header_evaluator.rb +56 -0
  40. data/lib/pdf/layout.rb +61 -0
  41. data/lib/pdf/renderer.rb +153 -0
  42. data/lib/pdf/resolver.rb +36 -0
  43. data/lib/pdf/version.rb +1 -1
  44. data/lib/pdf/view.rb +113 -0
  45. data/lib/pdf.rb +74 -1
  46. 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
+ ```