diff --git a/test/test_form_builder.rb b/test/test_form_builder.rb
new file mode 100644
index 0000000..cc70a4f
--- /dev/null
+++ b/test/test_form_builder.rb
@@ -0,0 +1,150 @@
+require 'minitest/autorun'
+require 'prism'
+
+class FormBuilderTest < Minitest::Test
+ FakeErrors = Struct.new(:store) do
+ def [](attribute)
+ store.fetch(attribute, [])
+ end
+ end
+
+ FakeModel = Struct.new(:id, :title, :published, :errors, :comments) do
+ def persisted?
+ !id.nil?
+ end
+ end
+
+ FakeComment = Struct.new(:id, :body, :errors)
+
+ def new_post(title: 'Hello', published: false, id: nil, errors: {}, comments: [])
+ FakeModel.new(id, title, published, FakeErrors.new(errors), comments)
+ end
+
+ def new_comment(id:, body:)
+ FakeComment.new(id, body, FakeErrors.new({}))
+ end
+
+ def builder_for(model, object_name: nil)
+ Prism::FormBuilder.new(model, object_name: object_name)
+ end
+
+ def test_text_field_uses_rails_style_nested_name_and_id
+ html = builder_for(new_post).text_field(:title)
+ assert_equal '', html
+ end
+
+ def test_text_field_value_can_be_overridden
+ html = builder_for(new_post).text_field(:title, value: 'Overridden')
+ assert_includes html, 'value="Overridden"'
+ end
+
+ def test_text_field_escapes_value
+ html = builder_for(new_post(title: '')).text_field(:title)
+ assert_includes html, 'value="<b>"'
+ end
+
+ def test_label_defaults_to_humanized_attribute
+ html = builder_for(new_post).label(:title)
+ assert_equal '', html
+ end
+
+ def test_label_accepts_custom_text
+ html = builder_for(new_post).label(:title, 'Post title')
+ assert_includes html, '>Post title'
+ end
+
+ def test_hidden_field
+ html = builder_for(new_post(id: 7)).hidden_field(:id)
+ assert_equal '', html
+ end
+
+ def test_text_area_renders_content_between_tags_not_as_value_attribute
+ html = builder_for(new_post(title: "multi\nline")).text_area(:title)
+ assert_equal "", html
+ end
+
+ def test_check_box_emits_hidden_fallback_and_reflects_current_value
+ html = builder_for(new_post(published: true)).check_box(:published)
+ assert_includes html, 'name="fake_model[published]" value="0"'
+ assert_includes html, 'type="checkbox"'
+ assert_includes html, 'checked="checked"'
+ end
+
+ def test_check_box_unchecked_when_value_falsey
+ html = builder_for(new_post(published: false)).check_box(:published)
+ refute_includes html, 'checked'
+ end
+
+ def test_select_marks_current_value_selected
+ html = builder_for(new_post(title: 'b')).select(:title, %w[a b c])
+ assert_includes html, ''
+ assert_includes html, ''
+ end
+
+ def test_select_with_label_value_pairs
+ html = builder_for(new_post(title: '2')).select(:title, [['One', '1'], ['Two', '2']])
+ assert_includes html, ''
+ end
+
+ def test_select_include_blank
+ html = builder_for(new_post).select(:title, %w[a b], include_blank: true)
+ assert_includes html, ''
+ end
+
+ def test_submit_default_and_custom_value
+ assert_equal '', builder_for(new_post).submit
+ assert_equal '', builder_for(new_post).submit('Go')
+ end
+
+ def test_errors_for_reads_from_object_errors
+ model = new_post(errors: { title: ['is too short'] })
+ assert_equal ['is too short'], builder_for(model).errors_for(:title)
+ end
+
+ def test_error_messages_renders_wrapper_only_when_present
+ clean = builder_for(new_post)
+ assert_equal '', clean.error_messages(:title)
+
+ dirty = builder_for(new_post(errors: { title: ['is blank', 'is too short'] }))
+ assert_equal 'is blank, is too short', dirty.error_messages(:title)
+ end
+
+ def test_object_name_can_be_overridden
+ html = builder_for(new_post, object_name: :post).text_field(:title)
+ assert_includes html, 'name="post[title]"'
+ assert_includes html, 'id="post_title"'
+ end
+
+ def test_fields_for_yields_one_nested_builder_per_record_with_indexed_names
+ post = new_post(id: 1, comments: [new_comment(id: 10, body: 'first'), new_comment(id: 11, body: 'second')])
+ builder = builder_for(post, object_name: :post)
+
+ rendered = []
+ builder.fields_for(:comments) { |f| rendered << f.text_field(:body) }
+
+ assert_equal 2, rendered.size
+ assert_equal '', rendered[0]
+ assert_equal '', rendered[1]
+ end
+
+ def test_fields_for_hidden_id_carries_the_record_id
+ post = new_post(id: 1, comments: [new_comment(id: 42, body: 'x')])
+ builder = builder_for(post, object_name: :post)
+
+ html = nil
+ builder.fields_for(:comments) { |f| html = f.hidden_field(:id) }
+ assert_equal '', html
+ end
+
+ def test_fields_for_accepts_an_explicit_collection_override
+ post = new_post(id: 1)
+ builder = builder_for(post, object_name: :post)
+
+ seen = []
+ builder.fields_for(:comments, [new_comment(id: 5, body: 'explicit')]) { |f| seen << f.object.id }
+ assert_equal [5], seen
+ end
+end