hexapdf 1.1.1 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/README.md +1 -1
- data/lib/hexapdf/cli/command.rb +63 -63
- data/lib/hexapdf/cli/inspect.rb +14 -5
- data/lib/hexapdf/cli/modify.rb +0 -1
- data/lib/hexapdf/cli/optimize.rb +5 -5
- data/lib/hexapdf/composer.rb +14 -0
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/graphics_state.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
- data/lib/hexapdf/document/annotations.rb +173 -0
- data/lib/hexapdf/document/layout.rb +45 -6
- data/lib/hexapdf/document.rb +28 -7
- data/lib/hexapdf/error.rb +11 -3
- data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
- data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -0
- data/lib/hexapdf/layout/style.rb +101 -7
- data/lib/hexapdf/object.rb +2 -2
- data/lib/hexapdf/pdf_array.rb +25 -3
- data/lib/hexapdf/tokenizer.rb +4 -1
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +7 -6
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
- data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
- data/lib/hexapdf/type/annotation.rb +71 -1
- data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
- data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
- data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
- data/lib/hexapdf/type/annotations/circle.rb +65 -0
- data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
- data/lib/hexapdf/type/annotations/line.rb +490 -0
- data/lib/hexapdf/type/annotations/square.rb +65 -0
- data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
- data/lib/hexapdf/type/annotations/widget.rb +52 -116
- data/lib/hexapdf/type/annotations.rb +8 -0
- data/lib/hexapdf/type/form.rb +2 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +0 -1
- data/lib/hexapdf/xref_section.rb +7 -4
- data/test/hexapdf/content/test_graphics_state.rb +2 -3
- data/test/hexapdf/content/test_operator.rb +4 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
- data/test/hexapdf/digital_signature/test_handler.rb +2 -3
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
- data/test/hexapdf/document/test_annotations.rb +55 -0
- data/test/hexapdf/document/test_layout.rb +24 -2
- data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
- data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
- data/test/hexapdf/layout/test_style.rb +27 -2
- data/test/hexapdf/task/test_optimize.rb +1 -1
- data/test/hexapdf/test_composer.rb +7 -0
- data/test/hexapdf/test_document.rb +11 -3
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_pdf_array.rb +36 -3
- data/test/hexapdf/test_stream.rb +1 -2
- data/test/hexapdf/test_xref_section.rb +1 -1
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
- data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +17 -1
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +482 -0
- data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
- data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
- data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
- data/test/hexapdf/type/annotations/test_line.rb +169 -0
- data/test/hexapdf/type/annotations/test_widget.rb +35 -81
- data/test/hexapdf/type/test_annotation.rb +55 -0
- data/test/hexapdf/type/test_form.rb +6 -0
- metadata +17 -2
@@ -127,6 +127,13 @@ describe HexaPDF::Composer do
|
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
130
|
+
describe "style?" do
|
131
|
+
it "delegates to layout.style?" do
|
132
|
+
@composer.document.layout.style(:header, font_size: 20)
|
133
|
+
assert(@composer.style?(:header))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
130
137
|
describe "styles" do
|
131
138
|
it "delegates to layout.styles" do
|
132
139
|
@composer.styles(base: {font_size: 30}, other: {font_size: 40})
|
@@ -54,7 +54,7 @@ describe HexaPDF::Document do
|
|
54
54
|
describe "::open" do
|
55
55
|
before do
|
56
56
|
@file = Tempfile.new('hexapdf-document')
|
57
|
-
@io_doc.write(@file)
|
57
|
+
@io_doc.write(@file, compact: false)
|
58
58
|
@file.close
|
59
59
|
end
|
60
60
|
|
@@ -370,7 +370,7 @@ describe HexaPDF::Document do
|
|
370
370
|
it "writes the document to a file" do
|
371
371
|
file = Tempfile.new('hexapdf-write')
|
372
372
|
file.close
|
373
|
-
@io_doc.write(file.path)
|
373
|
+
@io_doc.write(file.path, compact: false)
|
374
374
|
HexaPDF::Document.open(file.path) do |doc|
|
375
375
|
assert_equal(200, doc.object(2).value)
|
376
376
|
end
|
@@ -422,10 +422,18 @@ describe HexaPDF::Document do
|
|
422
422
|
|
423
423
|
it "allows optimizing the file by using object streams" do
|
424
424
|
io = StringIO.new(''.b)
|
425
|
-
@io_doc.write(io, optimize: true)
|
425
|
+
@io_doc.write(io, optimize: true, compact: false)
|
426
426
|
doc = HexaPDF::Document.new(io: io)
|
427
427
|
assert_equal(2, doc.each.count {|o| o.type == :ObjStm })
|
428
428
|
end
|
429
|
+
|
430
|
+
it "automatically compacts the file" do
|
431
|
+
io = StringIO.new(''.b)
|
432
|
+
@io_doc.write(io)
|
433
|
+
doc = HexaPDF::Document.new(io: io)
|
434
|
+
assert_equal(1, doc.revisions.count)
|
435
|
+
assert_equal(4, doc.each.count)
|
436
|
+
end
|
429
437
|
end
|
430
438
|
|
431
439
|
describe "version" do
|
data/test/hexapdf/test_object.rb
CHANGED
@@ -197,7 +197,7 @@ describe HexaPDF::Object do
|
|
197
197
|
@obj.define_singleton_method(:perform_validation) { raise "Unknown" }
|
198
198
|
invoked = []
|
199
199
|
refute(@obj.validate {|*a| invoked << a })
|
200
|
-
assert_equal([["
|
200
|
+
assert_equal([["Unexpected error encountered: Unknown", false, @obj]], invoked)
|
201
201
|
end
|
202
202
|
end
|
203
203
|
|
@@ -131,9 +131,42 @@ describe HexaPDF::PDFArray do
|
|
131
131
|
end
|
132
132
|
end
|
133
133
|
|
134
|
-
|
135
|
-
|
136
|
-
|
134
|
+
describe "reject!" do
|
135
|
+
it "allows deleting elements that are selected using a block" do
|
136
|
+
assert_same(@array, @array.reject! {|item| item == :data })
|
137
|
+
assert_equal([1, "deref", @array[2]], @array.to_a)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "returns nil if no elements were deleted" do
|
141
|
+
assert_nil(@array.reject! {|item| false })
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns an enumerator if no block is given" do
|
145
|
+
assert_kind_of(Enumerator, @array.reject!)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "map!" do
|
150
|
+
it "maps elements in-place to the return values of the block" do
|
151
|
+
assert_same(@array, @array.map! {|item| 5 })
|
152
|
+
assert_equal([5, 5, 5, 5], @array.to_a)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "returns an enumerator if no block is given" do
|
156
|
+
assert_kind_of(Enumerator, @array.reject!)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "compact!" do
|
161
|
+
it "removes all nil elements and returns self" do
|
162
|
+
@array << nil
|
163
|
+
assert_same(@array, @array.compact!)
|
164
|
+
assert_equal(4, @array.size)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "returns nil if no elements were removed" do
|
168
|
+
assert_nil(@array.compact!)
|
169
|
+
end
|
137
170
|
end
|
138
171
|
|
139
172
|
describe "index" do
|
data/test/hexapdf/test_stream.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
|
-
require 'ostruct'
|
5
4
|
require 'stringio'
|
6
5
|
require 'tempfile'
|
7
6
|
require 'hexapdf/configuration'
|
@@ -80,7 +79,7 @@ end
|
|
80
79
|
|
81
80
|
describe HexaPDF::Stream do
|
82
81
|
before do
|
83
|
-
@document =
|
82
|
+
@document = Struct.new(:config).new
|
84
83
|
@document.config = HexaPDF::Configuration.with_defaults
|
85
84
|
@document.instance_variable_set(:@version, '1.2')
|
86
85
|
def (@document).unwrap(obj); obj; end
|
@@ -66,7 +66,7 @@ describe HexaPDF::XRefSection do
|
|
66
66
|
@xref_section.add_in_use_entry(1, 0, 0)
|
67
67
|
@xref_section.add_in_use_entry(2, 0, 0)
|
68
68
|
result = @xref_section.each_subsection.map {|s| s.map {|e| [e.oid, e.type] }}
|
69
|
-
assert_equal([[[1, :in_use], [2, :in_use],
|
69
|
+
assert_equal([[[0, :free], [1, :in_use], [2, :in_use],
|
70
70
|
[3, :free], [4, :free], [5, :free],
|
71
71
|
[6, :in_use], [7, :in_use],
|
72
72
|
[8, :free],
|
@@ -373,12 +373,87 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
373
373
|
describe "push buttons" do
|
374
374
|
before do
|
375
375
|
@field.initialize_as_push_button
|
376
|
-
@widget = @field.create_widget(@page, Rect: [0, 0,
|
376
|
+
@widget = @field.create_widget(@page, Rect: [0, 0, 100, 50])
|
377
|
+
@widget.marker_style(style: 'Test')
|
377
378
|
@generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
|
378
379
|
end
|
379
380
|
|
380
|
-
it "
|
381
|
-
|
381
|
+
it "set the print flag on the widgets" do
|
382
|
+
@generator.create_appearances
|
383
|
+
assert(@widget.flagged?(:print))
|
384
|
+
end
|
385
|
+
|
386
|
+
it "removes the hidden flag on the widgets" do
|
387
|
+
@widget.flag(:hidden)
|
388
|
+
@generator.create_appearances
|
389
|
+
refute(@widget.flagged?(:hidden))
|
390
|
+
end
|
391
|
+
|
392
|
+
it "adds an appropriate form XObject" do
|
393
|
+
@generator.create_appearances
|
394
|
+
form = @widget[:AP][:N]
|
395
|
+
assert_equal(:XObject, form.type)
|
396
|
+
assert_equal(:Form, form[:Subtype])
|
397
|
+
assert_equal([0, 0, 100, 50], form[:BBox])
|
398
|
+
assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
|
399
|
+
end
|
400
|
+
|
401
|
+
it "re-uses the existing form XObject" do
|
402
|
+
@generator.create_appearances
|
403
|
+
form = @widget[:AP][:N]
|
404
|
+
form[:key] = :value
|
405
|
+
form.delete(:Subtype)
|
406
|
+
@widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Dictionary)
|
407
|
+
|
408
|
+
@generator.create_appearances
|
409
|
+
assert_equal(form, @widget[:AP][:N])
|
410
|
+
refute(form.key?(:key))
|
411
|
+
end
|
412
|
+
|
413
|
+
describe "takes the rotation into account" do
|
414
|
+
def check_rotation(angle, width, height, matrix)
|
415
|
+
@widget[:MK][:R] = angle
|
416
|
+
@generator.create_appearances
|
417
|
+
form = @widget[:AP][:N]
|
418
|
+
assert_equal([0, 0, width, height], form[:BBox].value)
|
419
|
+
assert_equal(matrix, form[:Matrix].value)
|
420
|
+
end
|
421
|
+
|
422
|
+
it "works for 0 degrees" do
|
423
|
+
check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
|
424
|
+
end
|
425
|
+
|
426
|
+
it "works for 90 degrees" do
|
427
|
+
check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
|
428
|
+
end
|
429
|
+
|
430
|
+
it "works for 180 degrees" do
|
431
|
+
check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
|
432
|
+
end
|
433
|
+
|
434
|
+
it "works for 270 degrees" do
|
435
|
+
check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
it "adds the button title in the center" do
|
440
|
+
@generator.create_appearances
|
441
|
+
assert_operators(@widget[:AP][:N].stream,
|
442
|
+
[[:save_graphics_state],
|
443
|
+
[:set_device_gray_non_stroking_color, [0.5]],
|
444
|
+
[:append_rectangle, [0, 0, 100, 50]],
|
445
|
+
[:fill_path_non_zero],
|
446
|
+
[:append_rectangle, [0.5, 0.5, 99.0, 49.0]],
|
447
|
+
[:stroke_path],
|
448
|
+
[:restore_graphics_state],
|
449
|
+
[:save_graphics_state],
|
450
|
+
[:set_font_and_size, [:F1, 9]],
|
451
|
+
[:begin_text],
|
452
|
+
[:move_text, [41.2475, 22.7005]],
|
453
|
+
[:show_text, ["Test"]],
|
454
|
+
[:end_text],
|
455
|
+
[:restore_graphics_state]],
|
456
|
+
)
|
382
457
|
end
|
383
458
|
end
|
384
459
|
end
|
@@ -236,6 +236,13 @@ describe HexaPDF::Type::AcroForm::ButtonField do
|
|
236
236
|
assert(widget[:AP][:N][:test])
|
237
237
|
end
|
238
238
|
|
239
|
+
it "works for push buttons" do
|
240
|
+
@field.initialize_as_push_button
|
241
|
+
@field.create_widget(@doc.pages.add, Rect: [0, 0, 100, 50])
|
242
|
+
@field.create_appearances
|
243
|
+
assert(@field[:AP][:N])
|
244
|
+
end
|
245
|
+
|
239
246
|
it "won't generate appearances if they already exist" do
|
240
247
|
widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
241
248
|
@field.create_appearances
|
@@ -262,12 +269,6 @@ describe HexaPDF::Type::AcroForm::ButtonField do
|
|
262
269
|
refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
|
263
270
|
end
|
264
271
|
|
265
|
-
it "fails for push buttons as they are not implemented yet" do
|
266
|
-
@field.flag(:push_button)
|
267
|
-
@field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
268
|
-
assert_raises(HexaPDF::Error) { @field.create_appearances }
|
269
|
-
end
|
270
|
-
|
271
272
|
it "uses the configuration option acro_form.appearance_generator" do
|
272
273
|
@doc.config['acro_form.appearance_generator'] = 'NonExistent'
|
273
274
|
assert_raises(Exception) { @field.create_appearances }
|
@@ -193,9 +193,14 @@ describe HexaPDF::Type::AcroForm::Field do
|
|
193
193
|
|
194
194
|
it "extracts an embedded widget into a standalone object if necessary" do
|
195
195
|
widget1 = @field.create_widget(@page, Rect: [1, 2, 3, 4])
|
196
|
+
# Make sure that the field/widget looks like as if it has been loaded from a file
|
197
|
+
@doc.revisions.current.update(widget1)
|
198
|
+
assert_equal(@field, widget1)
|
199
|
+
|
196
200
|
widget2 = @field.create_widget(@doc.pages.add, Rect: [2, 1, 4, 3])
|
197
201
|
kids = @field[:Kids]
|
198
202
|
|
203
|
+
assert_kind_of(HexaPDF::Type::AcroForm::Field, @doc.object(@field.oid))
|
199
204
|
assert_equal(2, kids.length)
|
200
205
|
refute_same(widget1, kids[0])
|
201
206
|
assert_same(widget2, kids[1])
|
@@ -558,13 +558,23 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
558
558
|
assert_equal(:Tx4, @acro_form[:Fields][2][:Kids][0][:T])
|
559
559
|
assert_equal(@acro_form[:Fields][2], @acro_form[:Fields][2][:Kids][0][:Parent])
|
560
560
|
end
|
561
|
+
|
562
|
+
it "ensures that objects loaded as widget are stored as field" do
|
563
|
+
@acro_form[:Fields][2] = @doc.add({T: :WidgetField, Type: :Annot, Subtype: :Widget})
|
564
|
+
assert_kind_of(HexaPDF::Type::Annotations::Widget, @acro_form[:Fields][2])
|
565
|
+
|
566
|
+
assert(@acro_form.validate)
|
567
|
+
field = @acro_form[:Fields][0]
|
568
|
+
assert_kind_of(HexaPDF::Type::AcroForm::Field, field)
|
569
|
+
assert_equal(:WidgetField, field.full_field_name)
|
570
|
+
end
|
561
571
|
end
|
562
572
|
|
563
573
|
describe "combining fields with the same name" do
|
564
574
|
before do
|
565
575
|
@acro_form[:Fields] = [
|
566
576
|
@doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
|
567
|
-
@doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
|
577
|
+
@merged_field = @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
|
568
578
|
@doc.add({T: 'Tx2'}),
|
569
579
|
@doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
|
570
580
|
]
|
@@ -576,6 +586,12 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
576
586
|
assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
|
577
587
|
@acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
|
578
588
|
end
|
589
|
+
|
590
|
+
it "deletes the combined and now unneeded field objects" do
|
591
|
+
assert(@acro_form.validate)
|
592
|
+
assert(@merged_field.null?)
|
593
|
+
assert(@doc.object(@merged_field.oid).null?)
|
594
|
+
end
|
579
595
|
end
|
580
596
|
|
581
597
|
describe "automatically creates the terminal fields; appearances" do
|
@@ -82,6 +82,20 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
|
|
82
82
|
assert_equal('1.234,57', value)
|
83
83
|
end
|
84
84
|
|
85
|
+
it "works with the special Infinity and NaN values" do
|
86
|
+
@value = 'Infinity'
|
87
|
+
assert_format('2, 2, 0, 0, "", false', "Inf", "black")
|
88
|
+
@value = '-Infinity'
|
89
|
+
assert_format('2, 2, 0, 0, "", false', "-Inf", "black")
|
90
|
+
@value = 'Nan'
|
91
|
+
assert_format('2, 2, 0, 0, "", false', "NaN", "black")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "works if the value is nil" do
|
95
|
+
@value = nil
|
96
|
+
assert_format('2, 2, 0, 0, "", false', "0,00", "black")
|
97
|
+
end
|
98
|
+
|
85
99
|
it "does nothing to the value if the JavaScript method could not be determined " do
|
86
100
|
assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
|
87
101
|
end
|
@@ -244,6 +258,13 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
|
|
244
258
|
assert_calculation('SUM', [@field1, @field2], "30.54")
|
245
259
|
end
|
246
260
|
|
261
|
+
it "works with the special values Infinity and NaN" do
|
262
|
+
@field1.field_value = "Infinity"
|
263
|
+
assert_calculation('SUM', [@field1, @field2], "Infinity")
|
264
|
+
@field1.field_value = "NaN"
|
265
|
+
assert_calculation('SUM', [@field1, @field2], "NaN")
|
266
|
+
end
|
267
|
+
|
247
268
|
it "returns nil if a field cannot be resolved" do
|
248
269
|
@action[:JS] = 'AFSimple_Calculate("SUM", ["unknown"]);'
|
249
270
|
assert_nil(@klass.calculate(@form, @action))
|
@@ -130,9 +130,12 @@ describe HexaPDF::Type::AcroForm::TextField do
|
|
130
130
|
assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
|
131
131
|
end
|
132
132
|
|
133
|
-
it "
|
133
|
+
it "calls acro_form.text_field.on_max_len_exceeded if the value exceeds the length set by /MaxLen" do
|
134
134
|
@field[:MaxLen] = 5
|
135
135
|
assert_raises(HexaPDF::Error) { @field.field_value = 'testdf' }
|
136
|
+
@doc.config['acro_form.text_field.on_max_len_exceeded'] = proc {|f, v| v }
|
137
|
+
@field.field_value = 'testdf'
|
138
|
+
assert_equal('testdf', @field[:V])
|
136
139
|
end
|
137
140
|
end
|
138
141
|
|
@@ -278,6 +281,9 @@ describe HexaPDF::Type::AcroForm::TextField do
|
|
278
281
|
assert(@field.validate)
|
279
282
|
@field[:MaxLen] = 2
|
280
283
|
refute(@field.validate)
|
284
|
+
@doc.config['acro_form.text_field.on_max_len_exceeded'] = proc {|field, str| "Hello" }
|
285
|
+
assert(@field.validate)
|
286
|
+
assert_equal('Hello', @field[:V])
|
281
287
|
@field[:V] = nil
|
282
288
|
assert(@field.validate)
|
283
289
|
end
|