spec/unit/form_builder_spec.rb in activeadmin-0.6.6 vs spec/unit/form_builder_spec.rb in activeadmin-1.0.0.pre1

- old
+ new

@@ -1,6 +1,7 @@ -require 'spec_helper' +require 'rails_helper' +require "rspec/mocks/standalone" describe ActiveAdmin::FormBuilder do # Setup an ActionView::Base object which can be used for # generating the form for. @@ -13,11 +14,11 @@ def view.protect_against_forgery? false end def view.url_for(*args) - if args.first == {:action => "index"} + if args.first == {action: "index"} posts_path else super end end @@ -28,204 +29,247 @@ view end def build_form(options = {}, form_object = Post.new, &block) - options = {:url => helpers.posts_path}.merge(options) + options = {url: helpers.posts_path}.merge(options) - render_arbre_component({:form_object => form_object, :form_options => options, :form_block => block}, helpers)do - text_node active_admin_form_for(assigns[:form_object], assigns[:form_options], &assigns[:form_block]) + form = render_arbre_component({form_object: form_object, form_options: options, form_block: block}, helpers) do + active_admin_form_for(assigns[:form_object], assigns[:form_options], &assigns[:form_block]) end.to_s + + Capybara.string(form) end + context "in general" do + context "it without custom settings" do + let :body do + build_form do |f| + f.inputs do + f.input :title + f.input :body + end + end + end + + it "should generate a fieldset with a inputs class" do + expect(body).to have_selector("fieldset.inputs") + end + end + + context "it with custom settings" do + let :body do + build_form do |f| + f.inputs class: "custom_class" do + f.input :title + f.input :body + end + end + end + + it "should generate a fieldset with a inputs and custom class" do + expect(body).to have_selector("fieldset.inputs.custom_class") + end + end + end + context "in general with actions" do let :body do build_form do |f| f.inputs do f.input :title f.input :body end f.actions do - f.action :submit, :label => "Submit Me" - f.action :submit, :label => "Another Button" + f.action :submit, label: "Submit Me" + f.action :submit, label: "Another Button" end end end it "should generate a text input" do - body.should have_tag("input", :attributes => { :type => "text", - :name => "post[title]" }) + expect(body).to have_selector("input[type=text][name='post[title]']") end it "should generate a textarea" do - body.should have_tag("textarea", :attributes => { :name => "post[body]" }) + expect(body).to have_selector("textarea[name='post[body]']") end it "should only generate the form once" do - body.scan(/Title/).size.should == 1 + expect(body).to have_selector("form", count: 1) end it "should generate actions" do - body.should have_tag("input", :attributes => { :type => "submit", - :value => "Submit Me" }) - body.should have_tag("input", :attributes => { :type => "submit", - :value => "Another Button" }) + expect(body).to have_selector("input[type=submit][value='Submit Me']") + expect(body).to have_selector("input[type=submit][value='Another Button']") end end context "when polymorphic relationship" do it "should raise error" do - lambda { + expect { comment = ActiveAdmin::Comment.new - build_form({:url => "admins/comments"}, comment) do |f| + build_form({url: "admins/comments"}, comment) do |f| f.inputs :resource end - }.should raise_error(Formtastic::PolymorphicInputWithoutCollectionError) + }.to raise_error(Formtastic::PolymorphicInputWithoutCollectionError) end end describe "passing in options with actions" do let :body do - build_form :html => { :multipart => true } do |f| + build_form html: { multipart: true } do |f| f.inputs :title f.actions end end it "should pass the options on to the form" do - body.should have_tag("form", :attributes => { :enctype => "multipart/form-data" }) + expect(body).to have_selector("form[enctype='multipart/form-data']") end end - describe "passing in options with actions" do - let :body do - build_form :html => { :multipart => true } do |f| - f.inputs :title - f.actions + if Rails::VERSION::MAJOR > 3 + context "file input present" do + let :body do + build_form do |f| + f.input :body, as: :file + end end + + it "adds multipart attribute automatically" do + expect(body).to have_selector("form[enctype='multipart/form-data']") + end end - it "should pass the options on to the form" do - body.should have_tag("form", :attributes => { :enctype => "multipart/form-data" }) - end end - context "with actions" do it "should generate the form once" do body = build_form do |f| f.inputs do f.input :title end f.actions end - body.scan(/id="post_title"/).size.should == 1 + expect(body).to have_selector("[id=post_title]", count: 1) end it "should generate one button and a cancel link" do body = build_form do |f| f.actions end - body.scan(/type="submit"/).size.should == 1 - body.scan(/class="cancel"/).size.should == 1 + expect(body).to have_selector("[type=submit]", count: 1) + expect(body).to have_selector("[class=cancel]", count: 1) end it "should generate multiple actions" do body = build_form do |f| f.actions do - f.action :submit, :label => "Create & Continue" - f.action :submit, :label => "Create & Edit" + f.action :submit, label: "Create & Continue" + f.action :submit, label: "Create & Edit" end end - body.scan(/type="submit"/).size.should == 2 - body.scan(/class="cancel"/).size.should == 0 + expect(body).to have_selector("[type=submit]", count: 2) + expect(body).to have_selector("[class=cancel]", count: 0) end end - context "with actions" do - it "should generate the form once" do + context "with Arbre inside" do + it "should render the Arbre in the expected place" do body = build_form do |f| + div do + h1 'Heading' + end f.inputs do + span 'Top note' f.input :title + span 'Bottom note' end + h3 'Footer' f.actions end - body.scan(/id="post_title"/).size.should == 1 + + expect(body).to have_selector("div > h1") + expect(body).to have_selector("h1", count: 1) + expect(body).to have_selector(".inputs > ol > span") + expect(body).to have_selector("span", count: 2) end - it "should generate one button and a cancel link" do + it "should allow a simplified syntax" do body = build_form do |f| - f.actions - end - body.scan(/type="submit"/).size.should == 1 - body.scan(/class="cancel"/).size.should == 1 - end - it "should generate multiple actions" do - body = build_form do |f| - f.actions do - f.action :submit, :label => "Create & Continue" - f.action :submit, :label => "Create & Edit" + div do + h1 'Heading' end + inputs do + span 'Top note' + input :title + span 'Bottom note' + end + h3 'Footer' + actions end - body.scan(/type="submit"/).size.should == 2 - body.scan(/class="cancel"/).size.should == 0 + + expect(body).to have_selector("div > h1") + expect(body).to have_selector("h1", count: 1) + expect(body).to have_selector(".inputs > ol > span") + expect(body).to have_selector("span", count: 2) end end context "without passing a block to inputs" do let :body do build_form do |f| f.inputs :title, :body end end it "should have a title input" do - body.should have_tag("input", :attributes => { :type => "text", - :name => "post[title]" }) + expect(body).to have_selector("input[type=text][name='post[title]']") end it "should have a body textarea" do - body.should have_tag("textarea", :attributes => { :name => "post[body]" }) + expect(body).to have_selector("textarea[name='post[body]']") end end context "with semantic fields for" do let :body do build_form do |f| f.inputs do f.input :title f.input :body end - f.instance_eval do + f.form_builder.instance_eval do @object.author = User.new end f.semantic_fields_for :author do |author| author.inputs :first_name, :last_name end end end it "should generate a nested text input once" do - body.scan("post_author_attributes_first_name_input").size.should == 1 + expect(body).to have_selector("[id=post_author_attributes_first_name_input]", count: 1) end end context "with collection inputs" do before do - User.create :first_name => "John", :last_name => "Doe" - User.create :first_name => "Jane", :last_name => "Doe" + User.create first_name: "John", last_name: "Doe" + User.create first_name: "Jane", last_name: "Doe" end describe "as select" do let :body do build_form do |f| - f.input :author + f.input :author, include_blank: false end end it "should create 2 options" do - body.scan(/<option/).size.should == 3 + expect(body).to have_selector("option", count: 2) end end describe "as radio buttons" do let :body do build_form do |f| - f.input :author, :as => :radio + f.input :author, as: :radio end end it "should create 2 radio buttons" do - body.scan(/type="radio"/).size.should == 2 + expect(body).to have_selector("[type=radio]", count: 2) end end end @@ -234,245 +278,475 @@ build_form do |f| f.inputs do f.input :title f.input :body end - f.instance_eval do + f.form_builder.instance_eval do @object.author = User.new end - f.inputs :name => 'Author', :for => :author do |author| + f.inputs name: 'Author', for: :author do |author| author.inputs :first_name, :last_name end end end it "should generate a nested text input once" do - body.scan("post_author_attributes_first_name_input").size.should == 1 + expect(body).to have_selector("[id=post_author_attributes_first_name_input]", count: 1) + expect(body).to have_selector("[id=post_author_attributes_last_name_input]", count: 1) end - it "should add an author first name field" do - body.should have_tag("input", :attributes => { :name => "post[author_attributes][first_name]"}) + it "should add author first and last name fields" do + expect(body).to have_selector("input[name='post[author_attributes][first_name]']") + expect(body).to have_selector("input[name='post[author_attributes][last_name]']") end end + context "with two input fields 'for'" do + let :body do + build_form do |f| + f.inputs do + f.input :title + f.input :body + end + f.form_builder.instance_eval do + @object.author = User.new + end + f.inputs name: 'Author', for: :author do |author| + author.input :first_name + author.input :last_name + end + end + end + it "should generate a nested text input once" do + expect(body).to have_selector("[id=post_author_attributes_first_name_input]", count: 1) + expect(body).to have_selector("[id=post_author_attributes_last_name_input]", count: 1) + end + it "should add author first and last name fields" do + expect(body).to have_selector("input[name='post[author_attributes][first_name]']") + expect(body).to have_selector("input[name='post[author_attributes][last_name]']") + end + end + context "with wrapper html" do it "should set a class" do body = build_form do |f| - f.input :title, :wrapper_html => { :class => "important" } + f.input :title, wrapper_html: { class: "important" } end - body.should have_tag("li", :attributes => {:class => "important string input optional stringish"}) + expect(body).to have_selector("li[class='important string input optional stringish']") end end + context "with inputs twice" do + let :body do + build_form do |f| + f.inputs do + f.input :title + f.input :body + end + f.inputs do + f.input :author + f.input :published_at + end + end + end + it "should render four inputs" do + expect(body).to have_selector("input[name='post[title]']", count: 1) + expect(body).to have_selector("textarea[name='post[body]']", count: 1) + expect(body).to have_selector("select[name='post[author_id]']", count: 1) + expect(body).to have_selector("select[name='post[published_at(1i)]']", count: 1) + expect(body).to have_selector("select[name='post[published_at(2i)]']", count: 1) + expect(body).to have_selector("select[name='post[published_at(3i)]']", count: 1) + expect(body).to have_selector("select[name='post[published_at(4i)]']", count: 1) + end + end + context "with has many inputs" do describe "with simple block" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| + build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build f.has_many :posts do |p| p.input :title + p.input :body end + f.inputs end end + let(:valid_html_id) { /^[A-Za-z]+[\w\-\:\.]*$/ } + it "should translate the association name in header" do - begin - I18n.backend.store_translations(:en, :activerecord => { :models => { :post => { :one => "Blog Post", :other => "Blog Posts" } } }) - body.should have_tag('h3', 'Blog Posts') - ensure - I18n.backend.reload! + with_translation activerecord: {models: {post: {one: 'Blog Post', other: 'Blog Posts'}}} do + expect(body).to have_selector("h3", text: "Blog Posts") end end it "should use model name when there is no translation for given model in header" do - body.should have_tag('h3', 'Post') + expect(body).to have_selector("h3", text: "Post") end it "should translate the association name in has many new button" do - begin - I18n.backend.store_translations(:en, :activerecord => { :models => { :post => { :one => "Blog Post", :other => "Blog Posts" } } }) - body.should have_tag('a', 'Add New Blog Post') - ensure - I18n.backend.reload! + with_translation activerecord: {models: {post: {one: 'Blog Post', other: 'Blog Posts'}}} do + expect(body).to have_selector("a", text: "Add New Blog Post") end end it "should translate the attribute name" do - begin - I18n.backend.store_translations :en, :activerecord => { :attributes => { :post => { :title => 'A very nice title' } } } - body.should have_tag 'label', 'A very nice title' - ensure - I18n.backend.reload! + with_translation activerecord: {attributes: {post: {title: 'A very nice title'}}} do + expect(body).to have_selector("label", text: "A very nice title") end end it "should use model name when there is no translation for given model in has many new button" do - body.should have_tag('a', 'Add New Post') + expect(body).to have_selector("a", text: "Add New Post") end it "should render the nested form" do - body.should have_tag("input", :attributes => {:name => "category[posts_attributes][0][title]"}) + expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") + expect(body).to have_selector("textarea[name='category[posts_attributes][0][body]']") end it "should add a link to remove new nested records" do - Capybara.string(body).should have_css(".has_many > fieldset > ol > li.has_many_delete > a", :class => "button", :href => "#", :content => "Delete") + expect(body).to have_selector(".has_many_container > fieldset > ol > li > a.button.has_many_remove[href='#']", text: "Remove") end - it "should include the nested record's class name in the js" do - body.should have_tag("a", :attributes => { :onclick => /NEW_POST_RECORD/ }) + it "should add a link to add new nested records" do + expect(body).to have_selector(".has_many_container > a.button.has_many_add[href='#']", text: "Add New Post") end - it "should add a link to add new nested records" do - Capybara.string(body).should have_css(".has_many > fieldset > ol > li > a", :class => "button", :href => "#", :content => "Add New Post") + it "should set an HTML-id valid placeholder" do + link = body.find('.has_many_container > a.button.has_many_add') + expect(link[:'data-placeholder']).to match valid_html_id end + + describe "with namespaced model" do + it "should set an HTML-id valid placeholder" do + allow(Post).to receive(:name).and_return "ActiveAdmin::Post" + link = body.find('.has_many_container > a.button.has_many_add') + expect(link[:'data-placeholder']).to match valid_html_id + end + end end describe "with complex block" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| + build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build f.has_many :posts do |p,i| - p.input :title, :label => "Title #{i}" + p.input :title, label: "Title #{i}" end end end it "should accept a block with a second argument" do - body.should have_tag("label", "Title 1") + expect(body).to have_selector("label", text: "Title 1") end it "should add a custom header" do - body.should have_tag('h3', 'Post') - end + expect(body).to have_selector("h3", text: "Post") + end end describe "without heading and new record link" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| + build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build - f.has_many :posts, :heading => false, :new_record => false do |p| + f.has_many :posts, heading: false, new_record: false do |p| p.input :title end end end it "should not add a header" do - body.should_not have_tag('h3', 'Post') - end + expect(body).not_to have_selector("h3", text: "Post") + end it "should not add link to new nested records" do - body.should_not have_tag('a', 'Add New Post') - end + expect(body).not_to have_selector("a", text: "Add New Post") + end - end + it "should render the nested form" do + expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") + end + end describe "with custom heading" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| + build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build - f.has_many :posts, :heading => "Test heading" do |p| + f.has_many :posts, heading: "Test heading" do |p| p.input :title end end end it "should add a custom header" do - body.should have_tag('h3', 'Test heading') - end + expect(body).to have_selector("h3", "Test heading") + end - end + end + describe "with custom new record link" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build + f.has_many :posts, new_record: 'My Custom New Post' do |p| + p.input :title + end + end + end + + it "should add a custom new record link" do + expect(body).to have_selector("a", text: "My Custom New Post") + end + + end + describe "with allow destroy" do context "with an existing post" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| - f.object.posts.build.stub!(:new_record? => false) - f.has_many :posts, :allow_destroy => true do |p| + build_form({url: '/categories'}, Category.new) do |f| + allow(f.object.posts.build).to receive(:new_record?).and_return(false) + f.has_many :posts, allow_destroy: true do |p| p.input :title end end end it "should include a boolean field for _destroy" do - body.should have_tag("input", :attributes => {:name => "category[posts_attributes][0][_destroy]"}) + expect(body).to have_selector("input[name='category[posts_attributes][0][_destroy]']") end it "should have a check box with 'Remove' as its label" do - body.should have_tag("label", :attributes => {:for => "category_posts_attributes_0__destroy"}, :content => "Remove") + expect(body).to have_selector("label[for=category_posts_attributes_0__destroy]", text: "Delete") end - it "should wrap the destroy field in an li with class 'has_many_remove'" do - Capybara.string(body).should have_css(".has_many > fieldset > ol > li.has_many_remove > input") + it "should wrap the destroy field in an li with class 'has_many_delete'" do + expect(body).to have_selector(".has_many_container > fieldset > ol > li.has_many_delete > input", count: 1) end end context "with a new post" do let :body do - build_form({:url => '/categories'}, Category.new) do |f| + build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build - f.has_many :posts, :allow_destroy => true do |p| + f.has_many :posts, allow_destroy: true do |p| p.input :title end end end it "should not have a boolean field for _destroy" do - body.should_not have_tag("input", :attributes => {:name => "category[posts_attributes][0][_destroy]"}) + expect(body).not_to have_selector("input[name='category[posts_attributes][0][_destroy]']") end it "should not have a check box with 'Remove' as its label" do - body.should_not have_tag("label", :attributes => {:for => "category_posts_attributes_0__destroy"}, :content => "Remove") + expect(body).not_to have_selector("label[for=category_posts_attributes_0__destroy]", text: "Remove") end end end - pending "should render the block if it returns nil" do - body = build_form({:url => '/categories'}, Category.new) do |f| + describe "sortable" do + # TODO: it doesn't make any sense to use your foreign key as something that's sortable (and therefore editable) + context "with a new post" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build + f.has_many :posts, sortable: :position do |p| + p.input :title + end + end + end + + it "shows the nested fields for unsaved records" do + expect(body).to have_selector("fieldset.inputs.has_many_fields") + end + + end + + context "with post returning nil for the sortable attribute" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build position: 3 + f.object.posts.build + f.has_many :posts, sortable: :position do |p| + p.input :title + end + end + end + + it "shows the nested fields for unsaved records" do + expect(body).to have_selector("fieldset.inputs.has_many_fields") + end + + end + + context "with existing and new posts" do + let! :category do + Category.create name: 'Name' + end + let! :post do + category.posts.create + end + let :body do + build_form({url: '/categories'}, category) do |f| + f.object.posts.build + f.has_many :posts, sortable: :position do |p| + p.input :title + end + end + end + + it "shows the nested fields for saved and unsaved records" do + expect(body).to have_selector("fieldset.inputs.has_many_fields") + end + end + + context "without sortable_start set" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build + f.has_many :posts, sortable: :position do |p| + p.input :title + end + end + end + + it "defaults to 0" do + expect(body).to have_selector("div.has_many_container[data-sortable-start='0']") + end + end + + context "with sortable_start set" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build + f.has_many :posts, sortable: :position, sortable_start: 15 do |p| + p.input :title + end + end + end + + it "sets the data attribute" do + expect(body).to have_selector("div.has_many_container[data-sortable-start='15']") + end + end + end + + describe "with nesting" do + context "in an inputs block" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.inputs "Field Wrapper" do + f.object.posts.build + f.has_many :posts do |p| + p.input :title + end + end + end + end + + it "should wrap the has_many fieldset in an li" do + expect(body).to have_selector("ol > li.has_many_container") + end + + it "should have a direct fieldset child" do + expect(body).to have_selector("li.has_many_container > fieldset") + end + + it "should not contain invalid li children" do + expect(body).not_to have_selector("div.has_many_container > li") + end + end + + context "in another has_many block" do + let :body do + build_form({url: '/categories'}, Category.new) do |f| + f.object.posts.build + f.has_many :posts do |p| + p.object.taggings.build + p.has_many :taggings do |t| + t.input :tag + end + end + end + end + + it "should wrap the inner has_many fieldset in an ol > li" do + expect(body).to have_selector(".has_many_container ol > li.has_many_container > fieldset") + end + + it "should not contain invalid li children" do + expect(body).not_to have_selector(".has_many_container div.has_many_container > li") + end + end + end + + it "should render the block if it returns nil" do + body = build_form({url: '/categories'}, Category.new) do |f| f.object.posts.build f.has_many :posts do |p| p.input :title nil end end - body.should have_tag("input", :attributes => {:name => "category[posts_attributes][0][title]"}) + expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") end end { # Testing that the same input can be used multiple times - "f.input :title, :as => :string" => /id="post_title"/, - "f.input :title, :as => :text" => /id="post_title"/, - "f.input :created_at, :as => :time_select" => /id="post_created_at_2i"/, - "f.input :created_at, :as => :datetime_select" => /id="post_created_at_2i"/, - "f.input :created_at, :as => :date_select" => /id="post_created_at_2i"/, + "f.input :title, as: :string" => "post_title", + "f.input :title, as: :text" => "post_title", + "f.input :created_at, as: :time_select" => "post_created_at_2i", + "f.input :created_at, as: :datetime_select" => "post_created_at_2i", + "f.input :created_at, as: :date_select" => "post_created_at_2i", # Testing that return values don't screw up the form - "f.input :title; nil" => /id="post_title"/, - "f.input :title; []" => /id="post_title"/, - "[:title].each{ |r| f.input r }" => /id="post_title"/, - "[:title].map { |r| f.input r }" => /id="post_title"/, - }.each do |source, regex| + "f.input :title; nil" => "post_title", + "f.input :title; []" => "post_title", + "[:title].each{ |r| f.input r }" => "post_title", + "[:title].map { |r| f.input r }" => "post_title", + }.each do |source, selector| it "should properly buffer `#{source}`" do body = build_form do |f| f.inputs do eval source eval source end end - body.scan(regex).size.should == 2 + expect(body).to have_selector("[id=#{selector}]", count: 2) end end describe "datepicker input" do - let :body do - build_form do |f| - f.inputs do - f.input :created_at, :as => :datepicker + context 'with default options' do + let :body do + build_form do |f| + f.inputs do + f.input :created_at, as: :datepicker + end end end + it "should generate a text input with the class of datepicker" do + expect(body).to have_selector("input.datepicker[type=text][name='post[created_at]']") + end end - it "should generate a text input with the class of datepicker" do - body.should have_tag("input", :attributes => { :type => "text", - :class => "datepicker", - :name => "post[created_at]" }) + + context 'with date range options' do + let :body do + build_form do |f| + f.inputs do + f.input :created_at, as: :datepicker, + datepicker_options: { + min_date: Date.new(2013, 10, 18), + max_date: "2013-12-31" } + end + end + end + + it 'should generate a datepicker text input with data min and max dates' do + selector = "input.datepicker[type=text][name='post[created_at]']" + expect(body).to have_selector(selector) + expect(body.find(selector)["data-datepicker-options"]).to eq({ minDate: '2013-10-18', maxDate: '2013-12-31' }.to_json) + end end end - end