phlex-rails icon indicating copy to clipboard operation
phlex-rails copied to clipboard

Test setup | Passing `ActionView::Helpers::FormBuilder` to component

Open phylor opened this issue 3 months ago • 1 comments

I have a component receiving a Rails form builder (e.g. created by form_for) as an argument. When trying to write tests for it, I can't manage to receive the complete output generated by calling methods on the form builder.

The component seems to only capture method calls directly wrapped within another element (e.g. a span { .. }).

Here's a test to reproduce it:

# frozen_string_literal: true

test "Rails form builder" do
  component = Class.new(Phlex::HTML) do
    attr_reader :form

    def initialize(form)
      @form = form
    end

    define_method :view_template do
      span do
        form.text_field :name
      end
      form.button "Submit"

      form.label :age do |label|
        strong { label.translation }
      end

      form.fields_for :a do |fields|
        fields.text_field :b
      end

      form.fields :c do |fields|
        fields.text_field :d
      end
    end
  end

  Cat = Data.define(:name, :age, :a, :c)

  form_builder = ActionView::Helpers::FormBuilder.new(
    :cat,
    Cat.new(name: "Ella", age: 3, a: nil, c: nil),
    controller.view_context,
    {}
  )

  output = render(component.new(form_builder))

  assert_equivalent_html output, <<~HTML
    <input type="text" name="name" value="Ella">
    <button name="button" type="submit">Submit</button>
    <label>
      <strong>Age</strong>
    </label>
    <input type="text" name="[a][b]" id="_a_b">
    <input type="text" name="[c][d]">
  HTML
end

This is the test output:

Actual                                                                Expected
1 <span>                                                              .
2   <input type="text" value="Ella" name="cat[name]" id="cat_name">   1 <input type="text" name="name" value="Ella">
.                                                                     2 <button name="button" type="submit">
.                                                                     3   Submit
3 </span>                                                             4 </button>
.                                                                     5 <label>
4 <strong>                                                            6   <strong>
5   Age                                                               7     Age
6 </strong>                                                           8   </strong>
                                                                      9 </label>
                                                                     10 <input type="text" name="[a][b]" id="_a_b">
                                                                     11 <input type="text" name="[c][d]">

If I remove the span element, the name text field is not rendered as well.

Note that this is only a testing issue - the component works fine in a normal request flow.

I'll add a couple of crazy, desperate things I tried below.

Using `Phlex::Rails::Builder`
# frozen_string_literal: true

test "Rails form builder" do
  component = Class.new(Phlex::HTML) do
    attr_reader :form

    def initialize(form)
      @form = form
    end

    define_method :view_template do
      span do
        form.text_field :name
      end
      form.button "Submit"

      form.label :age do |label|
        strong { label.translation }
      end

      form.fields_for :a do |fields|
        fields.text_field :b
      end

      form.fields :c do |fields|
        fields.text_field :d
      end
    end
  end

  Cat = Data.define(:name, :age, :a, :c)

  form_builder = Phlex::Rails::Builder.new(
    ActionView::Helpers::FormBuilder.new(
      :cat,
      Cat.new(name: "Ella", age: 3, a: nil, c: nil),
      controller.view_context,
      {}
    ),
    component: Class.new(Phlex::HTML).new
  ).tap do |builder|
    state = Phlex::SGML::State.new(
      user_context: {},
      output_buffer: +"",
      fragments: nil
    )

    builder.instance_variable_set(:@_state, state)
  end

  output = render(component.new(form_builder))

  assert_equivalent_html output, <<~HTML
    <input type="text" name="name" value="Ella">
    <button name="button" type="submit">Submit</button>
    <label>
      <strong>Age</strong>
    </label>
    <input type="text" name="[a][b]" id="_a_b">
    <input type="text" name="[c][d]">
  HTML
end
Not using `render`, but `call` with a buffer
# frozen_string_literal: true

test "Rails form builder" do
  component = Class.new(Phlex::HTML) do
    attr_reader :form

    def initialize(form)
      @form = form
    end

    define_method :view_template do
      span do
        form.text_field :name
      end
      form.button "Submit"

      form.label :age do |label|
        strong { label.translation }
      end

      form.fields_for :a do |fields|
        fields.text_field :b
      end

      form.fields :c do |fields|
        fields.text_field :d
      end
    end
  end

  Cat = Data.define(:name, :age, :a, :c)

  form_builder = Phlex::Rails::Builder.new(
    ActionView::Helpers::FormBuilder.new(
      :cat,
      Cat.new(name: "Ella", age: 3, a: nil, c: nil),
      view_context,
      {}
    ),
    component: Class.new(Phlex::HTML).new
  ).tap do |builder|
    state = Phlex::SGML::State.new(
      user_context: {},
      output_buffer: +"",
      fragments: nil
    )

    builder.instance_variable_set(:@_state, state)
  end

  buffer = StringIO.new
  output = component.new(form_builder).call(buffer).string

  assert_equivalent_html output, <<~HTML
    <input type="text" name="name" value="Ella">
    <button name="button" type="submit">Submit</button>
    <label>
      <strong>Age</strong>
    </label>
    <input type="text" name="[a][b]" id="_a_b">
    <input type="text" name="[c][d]">
  HTML
end

phylor avatar Sep 16 '25 12:09 phylor

Thanks for the issue. 🙏 Will try to take a look soon after EuRuKo.

joeldrapper avatar Sep 16 '25 22:09 joeldrapper