ash_phoenix/test/form_test.exs
2024-06-24 22:16:39 -04:00

1958 lines
56 KiB
Elixir

defmodule AshPhoenix.FormTest do
use ExUnit.Case
import ExUnit.CaptureLog
alias AshPhoenix.Form
alias AshPhoenix.Test.{Domain, Artist, Author, Comment, Post, PostWithDefault}
alias Phoenix.HTML.FormData
defp form_for(form, _) do
Phoenix.HTML.FormData.to_form(form, [])
end
defp inputs_for(form, key) do
form[key].value
end
describe "validate_opts" do
test "errors are not set on the parent and list child form" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
)
|> Form.add_form([:comments], validate_opts: [errors: false])
|> form_for("action")
assert form.errors == []
assert Form.errors(form.source, for_path: [:comments, 0]) == []
end
test "unknown errors produce warnings" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
params: %{"text" => "bar"},
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create_with_unknown_error
]
]
)
|> Form.add_form([:comments], params: %{"text" => "foo"}, validate_opts: [errors: true])
|> form_for("action")
assert capture_log(fn ->
Form.errors(form.source, for_path: [:comments, 0]) == []
end) =~
"Unhandled error in form submission for AshPhoenix.Test.Comment.create_with_unknown_error"
end
test "empty atom field" do
Post
|> Form.for_create(:create,
domain: Domain,
params: %{}
)
|> Form.submit!(
params: %{"inline_atom_field" => "", "custom_atom_field" => "", "text" => "text"}
)
end
test "update_form marks touched by default" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
params: %{"text" => "bar"},
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create_with_unknown_error
]
]
)
|> Form.add_form([:comments], params: %{"text" => "foo"})
|> Form.update_form([:comments, 0], & &1)
assert MapSet.member?(form.touched_forms, "comments")
end
test "errors are not set on the parent and single child form" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :single,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form([:post], validate_opts: [errors: false])
|> form_for("action")
assert form.errors == []
assert Form.errors(form.source, for_path: [:post]) == []
end
end
describe "clear_value/1" do
test "it clears attributes" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{"text" => "text"})
assert Form.value(form, :text) == "text"
assert form.source.attributes == %{text: "text"}
assert form.source.params == %{"text" => "text"}
assert form.params == %{"text" => "text"}
form = Form.clear_value(form, :text)
assert Form.value(form, :text) == nil
assert form.source.attributes == %{}
assert form.source.params == %{}
assert form.params == %{}
end
test "it clears arguments" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{"excerpt" => "text"})
assert Form.value(form, :excerpt) == "text"
assert form.source.arguments == %{excerpt: "text"}
assert form.source.params == %{"excerpt" => "text"}
assert form.params == %{"excerpt" => "text"}
form = Form.clear_value(form, :excerpt)
assert Form.value(form, :excerpt) == nil
assert form.source.arguments == %{}
assert form.source.params == %{}
assert form.params == %{}
end
test "it clears multiple fields" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{"excerpt" => "text", "text" => "text"})
assert Form.value(form, :excerpt) == "text"
assert Form.value(form, :text) == "text"
assert form.source.attributes == %{text: "text"}
assert form.source.arguments == %{excerpt: "text"}
assert form.source.params == %{"excerpt" => "text", "text" => "text"}
assert form.params == %{"excerpt" => "text", "text" => "text"}
form = Form.clear_value(form, [:excerpt, :text])
assert Form.value(form, :text) == nil
assert Form.value(form, :excerpt) == nil
assert form.params == %{}
assert form.source.arguments == %{}
assert form.source.attributes == %{}
assert form.source.params == %{}
end
end
describe "validations and form values" do
test "validation errors don't clear fields" do
form =
AshPhoenix.Test.User
|> AshPhoenix.Form.for_create(:register)
|> AshPhoenix.Form.validate(%{"password" => "f"})
|> AshPhoenix.Form.validate(%{"password" => "fo"})
|> AshPhoenix.Form.validate(%{"password" => "fo", "password_confirmation" => "foo"})
assert AshPhoenix.Form.value(form, :password) == "fo"
end
test "form values are retrieved casted for un-changing arguments" do
form =
AshPhoenix.Test.User
|> AshPhoenix.Form.for_create(:register)
|> AshPhoenix.Form.validate(%{"password" => "f"})
|> AshPhoenix.Form.validate(%{"password" => :f})
assert AshPhoenix.Form.value(form, :password) == "f"
end
test "lists with invalid values return those invalid values when getting them" do
form =
Post
|> Form.for_create(:create_author_required, domain: Domain, forms: [auto?: true])
|> Form.validate(%{"list_of_ints" => %{"0" => %{"map" => "of stuff"}}})
assert AshPhoenix.Form.value(form, :list_of_ints) == [%{"map" => "of stuff"}]
end
end
describe "has_form?" do
test "checks for the existence of a list of forms" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
)
|> Form.add_form([:comments])
# assert Form.has_form?(form, [:comments])
assert Form.has_form?(form, [:comments, 0])
assert Form.has_form?(form, "form[comments][0]")
refute Form.has_form?(form, [:comments, 1])
refute Form.has_form?(form, "form[comments][1]")
end
test "checks for the existence of a single form" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :single,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form([:post])
assert Form.has_form?(form, [:post])
assert Form.has_form?(form, "form[post]")
refute Form.has_form?(form, [:unknown])
refute Form.has_form?(form, "form[unknown]")
end
end
describe "form_for fields" do
test "it should show simple field values" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{"text" => "text"})
|> form_for("action")
assert FormData.input_value(form.source, form, :text) == "text"
end
test "it sets the default id of a form" do
assert Form.for_create(Post, :create).id == "form"
assert Form.for_create(Post, :create, as: "post").id == "post"
end
end
test "a read will validate attributes" do
form =
Post
|> Form.for_read(:read)
|> Form.validate(%{"text" => [1, 2, 3]})
|> form_for("action")
assert form.errors[:text] == {"is invalid", []}
end
test "validation errors are attached to fields" do
form = Form.for_create(PostWithDefault, :create, domain: Domain)
form = AshPhoenix.Form.validate(form, %{"text" => ""}, errors: form.submitted_once?)
{:error, form} = Form.submit(form, params: %{"text" => ""})
assert %{errors: [text: {"is required", []}]} = form_for(form, "foo")
assert form.valid? == false
end
test "blank form values unset - helps support dead view forms" do
form =
Form.for_create(PostWithDefault, :create,
domain: Domain,
exclude_fields_if_empty: [:text, :title]
)
{:ok, post} = Form.submit(form, params: %{"title" => "", "text" => "bar"})
assert post.text == "bar"
assert post.title == nil
end
test "blank nested form values unset - helps support dead view forms" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: PostWithDefault,
create_action: :create
]
],
exclude_fields_if_empty: [post: [:title, :description]]
)
|> Form.add_form(:post)
{:ok, comment} =
Form.submit(form,
params: %{"text" => "comment", "post" => %{"title" => "", "text" => "bar"}}
)
post = comment.post
assert post.text == "bar"
assert post.title == nil
end
test "phoenix forms are accepted as input in some cases" do
form = Form.for_create(PostWithDefault, :create, domain: Domain)
form = AshPhoenix.Form.validate(form, %{"text" => ""}, errors: form.submitted_once?)
form = form_for(form, "foo")
# This simply shouldn't raise
AshPhoenix.Form.params(form)
end
test "a phoenix form is returned in cases where a phoenix form is passed in" do
form = Form.for_create(PostWithDefault, :create, domain: Domain)
form = AshPhoenix.Form.validate(form, %{"text" => ""}, errors: form.submitted_once?)
form = form_for(form, "foo")
assert %Phoenix.HTML.Form{} = AshPhoenix.Form.validate(form, %{})
end
test "it supports forms with data and a `type: :append_and_remove`" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
form =
post
|> Form.for_update(:update_with_replace,
domain: Domain,
forms: [
comments: [
read_resource: Comment,
type: :list,
read_action: :read,
data: [comment]
]
]
)
assert [comment_form] = inputs_for(form_for(form, "blah"), :comments)
assert Phoenix.HTML.Form.input_value(comment_form, :text) == "comment"
form = Form.validate(form, %{"comments" => [%{"id" => comment.id}]})
comment_id = comment.id
assert %{"comments" => [%{"id" => ^comment_id}]} = Form.params(form)
end
test "ignoring a form filters it from the parameters" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
form =
post
|> Form.for_update(:update_with_replace,
domain: Domain,
forms: [
comments: [
read_resource: Comment,
type: :list,
read_action: :read,
data: [comment]
]
]
)
assert [comment_form] = inputs_for(form_for(form, "blah"), :comments)
assert Phoenix.HTML.Form.input_value(comment_form, :text) == "comment"
form = Form.validate(form, %{"comments" => [%{"id" => comment.id, "_ignore" => "true"}]})
assert %{"comments" => []} = Form.params(form)
form = Form.validate(form, %{"comments" => [%{"id" => comment.id, "_ignore" => "false"}]})
comment_id = comment.id
assert %{"comments" => [%{"id" => ^comment_id, "_ignore" => "false"}]} = Form.params(form)
form = Form.validate(form, %{"comments" => [%{"id" => comment.id, "_ignore" => "true"}]})
assert %{"comments" => []} = Form.params(form)
end
describe "the .changed? field is updated as data changes" do
test "it is false for a create form with no changes" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{})
refute form.changed?
end
test "it is false by default for update forms" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
form =
post
|> Form.for_update(:update)
|> Form.validate(%{})
refute form.changed?
end
test "it is true when a change is made" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
form =
post
|> Form.for_update(:update)
|> Form.validate(%{text: "post1"})
assert form.changed?
end
test "it goes back to false if the change is unmade" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
form =
post
|> Form.for_update(:update)
|> Form.validate(%{text: "post1"})
assert form.changed?
form =
form
|> Form.validate(%{text: "post"})
refute form.changed?
end
test "adding a form causes changed? to be true on the root form, but not the nested form" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post)
assert form.changed?
refute form.forms[:post].changed?
end
test "removing a form that was there prior marks the form as changed" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
# Check the persisted post.comments count after create
post = Post |> Ash.get!(post.id) |> Ash.load!(:comments)
assert Enum.count(post.comments) == 1
form =
post
|> Form.for_update(:update,
domain: Domain,
forms: [
comments: [
resource: Comment,
type: :list,
data: [comment],
create_action: :create,
update_action: :update
]
]
)
refute form.changed?
form = Form.remove_form(form, [:comments, 0])
assert form.changed?
end
test "generated forms have default values even with no server round-trips" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
# Check the persisted post.comments count after create
post = Post |> Ash.get!(post.id) |> Ash.load!(:comments)
assert Enum.count(post.comments) == 1
form =
post
|> Form.for_update(:update,
domain: Domain,
forms: [
comments: [
resource: Comment,
type: :list,
data: [comment],
create_action: :create,
update_action: :update
]
]
)
form = AshPhoenix.Form.add_form(form, :comments)
assert AshPhoenix.Form.params(form, hidden?: true) == %{
"_form_type" => "update",
"_touched" => "comments",
"comments" => [
%{
"_form_type" => "update",
"_touched" => "_form_type,id",
"id" => post.comments |> Enum.at(0) |> Map.get(:id)
},
%{"_form_type" => "create", "_touched" => "_form_type"}
],
"id" => post.id
}
end
test "removing a non-existant form should not change touched_forms" do
form =
Post
|> Form.for_create(:create, domain: Domain, forms: [auto?: true])
|> AshPhoenix.Form.remove_form([:author])
assert MapSet.member?(form.touched_forms, "author") == false
end
test "removing a form that was added does not mark the form as changed" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
# Check the persisted post.comments count after create
post = Post |> Ash.get!(post.id) |> Ash.load!(:comments)
assert Enum.count(post.comments) == 1
form =
post
|> Form.for_update(:update,
domain: Domain,
forms: [
comments: [
resource: Comment,
type: :list,
data: [comment],
create_action: :create,
update_action: :update
]
]
)
refute form.changed?
form = Form.add_form(form, [:comments])
assert form.changed?
form = Form.remove_form(form, [:comments, 1])
refute form.changed?
end
end
describe "errors" do
test "errors are not set on the form without validating" do
form =
Post
|> Form.for_create(:create)
|> form_for("action")
assert form.errors == []
end
test "errors are set on the form according to changeset errors on validate" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{})
|> form_for("action")
assert form.errors == [{:text, {"is required", []}}]
end
test "nested errors are set on the appropriate form after submit" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"text" => "text", "post" => %{}})
|> Form.submit(force?: true)
|> elem(1)
|> form_for("action")
assert form.errors == []
assert hd(inputs_for(form, :post)).errors == [{:text, {"is required", []}}]
end
test "relationship source data is retained, so that it can be properly removed" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
comment = Comment |> Ash.get!(comment.id)
comment
|> Ash.load!(:post)
|> Form.for_update(:update,
domain: Domain,
forms: [
auto?: true
]
)
|> Form.remove_form([:post])
|> Form.submit!(params: %{"text" => "text", "post" => %{"text" => "new_post"}})
assert [%{text: "new_post"}] = Ash.read!(Post)
end
test "nested errors are set on the appropriate form after submit, even if no submit actually happens" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.submit(params: %{"text" => "text", "post" => %{}})
|> elem(1)
|> form_for("action")
assert form.errors == []
assert hd(inputs_for(form, :post)).errors == [{:text, {"is required", []}}]
end
test "nested errors can be fetched with `Form.errors/2`" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"text" => "text", "post" => %{}})
|> Form.submit(force?: true)
|> elem(1)
|> form_for("action")
assert Form.errors(form.source, for_path: [:post]) == [{:text, "is required"}]
assert Form.errors(form.source, for_path: [:post], format: :raw) == [
{:text, {"is required", []}}
]
assert Form.errors(form.source, for_path: [:post], format: :plaintext) == [
"text: is required"
]
assert Form.errors(form.source, for_path: :all) == %{[:post] => [{:text, "is required"}]}
end
test "errors can be fetched with `Form.errors/2`" do
form =
Comment
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"post" => %{"text" => "text"}})
|> Form.submit(force?: true)
|> elem(1)
|> form_for("action")
assert Form.errors(form.source) == [{:text, "is required"}]
assert Form.errors(form.source, format: :raw) == [
{:text, {"is required", []}}
]
assert Form.errors(form.source, format: :plaintext) == [
"text: is required"
]
end
test "nested forms submit empty values when they have been touched, even if not included in future params" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"text" => "text"})
assert %{"text" => "text", "post" => nil} = Form.params(form)
end
test "nested forms submit empty list values when not present in input params" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"text" => "text"})
assert Form.value(form, :text) == "text"
assert %{
"text" => "text",
"post" => [],
"_form_type" => "create",
"_touched" => "_form_type,_touched,post,text"
} = Form.params(form, hidden?: true)
end
test "nested errors are set on the appropriate form after submit for many to many relationships" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
forms: [
post: [
type: :list,
for: :linked_posts,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{})
|> Form.validate(%{"text" => "text", "post" => [%{}]})
|> Form.submit(force?: true)
|> elem(1)
|> form_for("action")
assert form.errors == []
assert [nested_form] = inputs_for(form, :post)
assert nested_form.errors == [{:text, {"is required", []}}]
end
test "errors with a path are propagated down to the appropirate nested form" do
author = %Author{
email: "me@example.com"
}
form =
author
|> Form.for_update(:update_with_embedded_argument, domain: Domain, forms: [auto?: true])
|> Form.add_form(:embedded_argument, params: %{})
|> Form.validate(%{"embedded_argument" => %{"value" => "you@example.com"}})
|> form_for("action")
assert AshPhoenix.Form.errors(form, for_path: [:embedded_argument]) == [
value: "must match email"
]
assert AshPhoenix.Form.errors(form) == []
# Check that errors will appear on a nested form using the Phoenix Core Components inputs_for
# https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_component.ex#L2410
%Phoenix.HTML.FormField{field: field_name, form: parent_form} = form[:embedded_argument]
inputs_for_form =
parent_form.impl.to_form(
parent_form.source,
parent_form,
field_name,
parent_form.options
)
|> List.first()
# This is the expected format for a phoenix core component
assert inputs_for_form.errors == [{:value, {"must match email", []}}]
end
test "deeply nested errors don't multiply" do
author = %Author{
email: "me@example.com"
}
form =
author
|> Form.for_update(:update_with_embedded_argument, domain: Domain, forms: [auto?: true])
|> Form.add_form(:embedded_argument, params: %{})
|> Form.add_form([:embedded_argument, :nested_embeds], params: %{})
|> Form.validate(%{
"embedded_argument" => %{
"value" => "you@example.com",
nested_embeds: %{"0" => %{"limit" => "non-integer", "four_chars" => "not-4-chars"}}
}
})
|> form_for("action")
%Phoenix.HTML.FormField{field: field_name, form: parent_form} = form[:embedded_argument]
inputs_for_arg_form =
parent_form.impl.to_form(
parent_form.source,
parent_form,
field_name,
parent_form.options
)
|> List.first()
%Phoenix.HTML.FormField{field: field_name, form: parent_form} =
inputs_for_arg_form[:nested_embeds]
inputs_for_nested_form =
parent_form.impl.to_form(
parent_form.source,
parent_form,
field_name,
parent_form.options
)
|> List.first()
# Note: I'm not 100% which of the 3 errors messages are preferred. The opts are get_text bindings for error translation
# In the Phoenix core components the error translation is done `Gettext.dpgettext(MyApp.Gettext, domain, msgctxt, msgid, bindings)` with our tuple being `{msgid, bindings}`
assert Keyword.get_values(inputs_for_nested_form.errors, :limit) == [
{"is invalid", []}
]
assert Keyword.get_values(inputs_for_nested_form.errors, :four_chars) == [
{"must have length of exactly %{exact}",
[
exact: 4
]}
]
end
end
describe "data" do
test "it uses the provided data to create forms even without input" do
post1_id = Ash.UUID.generate()
post2_id = Ash.UUID.generate()
form =
Comment
|> Form.for_create(
:create,
forms: [
post: [
type: :list,
data: [%Post{id: post1_id}, %Post{id: post2_id}],
update_action: :update,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
]
]
)
|> form_for("foo")
assert Enum.count(inputs_for(form, :post)) == 2
end
test "a function can be used to derive the data from the data of the parent form" do
post1_id = Ash.UUID.generate()
post2_id = Ash.UUID.generate()
comment_id = Ash.UUID.generate()
form =
Comment
|> Form.for_create(
:create,
forms: [
post: [
type: :list,
data: [
%Post{id: post1_id, comments: [%Comment{id: comment_id}]},
%Post{id: post2_id, comments: []}
],
update_action: :update,
forms: [
comments: [
data: &(&1.comments || []),
type: :list,
resource: Comment,
create_action: :create,
update_action: :update
]
]
]
]
)
|> Form.validate(%{
"text" => "text",
"post" => %{
"0" => %{"id" => post1_id, "comments" => %{"0" => %{"id" => comment_id}}},
"1" => %{"id" => post2_id}
}
})
assert %{
"post" => [
%{"comments" => [%{"id" => ^comment_id}], "id" => ^post1_id},
%{"id" => ^post2_id}
],
"text" => "text"
} = Form.params(form)
end
end
describe "submit" do
test "it runs the action with the params" do
assert {:ok, %{text: "text"}} =
Post
|> Form.for_create(:create, domain: Domain)
|> Form.validate(%{text: "text"})
|> Form.submit()
end
test "it fallback to resource defined Domain if unset" do
assert {:ok, %{name: "name"}} =
Artist
|> Form.for_action(:create)
|> Form.validate(%{name: "name"})
|> Form.submit()
assert {:ok, [%{name: "name"}]} =
Artist
|> Form.for_action(:read)
|> Form.validate(%{name: "name changed"})
|> Form.submit()
artist =
Artist
|> Ash.Changeset.for_create(:create, %{name: "name"})
|> Ash.create!()
assert {:ok, %{name: "name changed"}} =
artist
|> Form.for_action(:update)
|> Form.validate(%{name: "name changed"})
|> Form.submit()
assert :ok =
artist
|> Form.for_action(:destroy)
|> Form.validate(%{name: "name changed"})
|> Form.submit()
end
end
describe "prepare_source" do
test "it runs on initial create" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
prepare_source: &Ash.Changeset.put_context(&1, :foo, :bar)
)
assert form.source.context.foo == :bar
end
test "it is preserved on validate create" do
form =
Post
|> Form.for_create(:create,
domain: Domain,
prepare_source: &Ash.Changeset.put_context(&1, :foo, :bar)
)
|> Form.validate(%{text: "text"})
assert form.source.context.foo == :bar
end
test "it is preserved through to submit" do
result =
Post
|> Form.for_create(:create,
domain: Domain,
prepare_source: fn changeset ->
Ash.Changeset.before_action(
changeset,
&Ash.Changeset.force_change_attribute(&1, :title, "special_title")
)
end
)
|> Form.validate(%{text: "text"})
|> Form.submit!()
assert result.title == "special_title"
end
end
describe "params" do
test "it includes nested forms, and honors their `for` configuration" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
],
other_post: [
type: :single,
for: :for_posts,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form("form[post]", params: %{"text" => "post_text"})
|> Form.add_form("form[other_post]", params: %{"text" => "post_text"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "post_text"})
|> Form.validate(%{
"text" => "text",
"post" => [%{"comments" => [%{"text" => "post_text"}], "text" => "post_text"}],
"other_post" => %{"text" => "post_text"}
})
assert %{
"text" => "text",
"post" => [%{"comments" => [%{"text" => "post_text"}], "text" => "post_text"}],
"for_posts" => %{"text" => "post_text"}
} = Form.params(form)
end
end
test "it properly retains form order" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
],
other_post: [
type: :single,
for: :for_posts,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form("form[post]", params: %{"text" => "post_text"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "0"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "1"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "2"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "3"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "4"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "5"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "6"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "7"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "8"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "9"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "10"})
|> Form.add_form("form[post][0][comments]", params: %{"text" => "11"})
assert %{
"post" => [
%{
"comments" => [
%{"text" => "0"},
%{"text" => "1"},
%{"text" => "2"},
%{"text" => "3"},
%{"text" => "4"},
%{"text" => "5"},
%{"text" => "6"},
%{"text" => "7"},
%{"text" => "8"},
%{"text" => "9"},
%{"text" => "10"},
%{"text" => "11"}
]
}
]
} = Form.params(form)
end
describe "`inputs_for` with no configuration" do
test "it should raise an error" do
form =
Post
|> Form.for_create(:create)
|> Form.validate(%{text: "text"})
|> form_for("action")
assert_raise AshPhoenix.Form.NoFormConfigured,
~r/There is a no attribute or relationship called `post` on the resource `AshPhoenix.Test.Post`/,
fn ->
AshPhoenix.Form.add_form(form, :post)
end
end
end
describe "inputs_for` relationships" do
test "it should name the fields correctly on `for_update`" do
post_id = Ash.UUID.generate()
comment_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: [%Comment{id: comment_id}]
}
}
form =
comment
|> Form.for_update(:update,
as: "comment",
forms: [
post: [
data: comment.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
data: & &1.comments,
type: :list,
resource: Comment,
update_action: :update,
create_action: :create
]
]
]
]
)
comments_form =
form
|> form_for("action")
|> inputs_for(:post)
|> hd()
|> inputs_for(:comments)
|> hd()
assert comments_form.name == "comment[post][comments][0]"
end
test "the `type: :single` option should create a form without integer paths" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{text: "post_text"})
|> Form.validate(%{"text" => "text", "post" => %{"text" => "post_text"}})
|> form_for("action")
assert %Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
related_form = hd(inputs_for(form, :post))
assert related_form.name == "form[post]"
assert FormData.input_value(related_form.source, related_form, :text) == "post_text"
end
test "it should show nothing in `inputs_for` by default" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.validate(%{"text" => "text"})
|> form_for("action")
assert inputs_for(form, :post) == []
end
test "when a value has been appended to the relationship, a form is created" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{text: "post_text"})
|> form_for("action")
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
related_form
] = inputs_for(form, :post)
assert FormData.input_value(related_form.source, related_form, :text) == "post_text"
end
test "a query path can be used when manipulating forms" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
]
]
)
|> Form.add_form("form[post]", params: %{text: "post_text"})
|> Form.add_form("form[post][0][comments]", params: %{text: "post_text"})
|> form_for("action")
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
post_form
] = inputs_for(form, :post)
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Comment}}
] = inputs_for(post_form, :comments)
end
test "list values get an index in their name and id" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{text: "post_text0"})
|> Form.add_form(:post, params: %{text: "post_text1"})
|> Form.add_form(:post, params: %{text: "post_text2"})
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_0,
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_1,
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_2
] = inputs_for(form_for(form, "action"), :post)
assert form_0.name == "form[post][0]"
assert form_0.id == "form_post_0"
assert form_1.name == "form[post][1]"
assert form_1.id == "form_post_1"
assert form_2.name == "form[post][2]"
assert form_2.id == "form_post_2"
end
test "when a value has been removed from the relationship, the form is removed" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{text: "post_text0"})
|> Form.add_form(:post, params: %{text: "post_text1"})
|> Form.add_form(:post, params: %{text: "post_text2"})
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}
] = inputs_for(form_for(form, "action"), :post)
form =
form
|> Form.remove_form([:post, 0])
|> Form.remove_form([:post, 1])
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
related_form
] = inputs_for(form_for(form, "action"), :post)
assert FormData.input_value(related_form.source, related_form, :text) == "post_text1"
end
test "when all values have been removed from a relationship, the empty list remains" do
form =
Comment
|> Form.for_create(:create,
forms: [
post: [
type: :list,
resource: Post,
create_action: :create
]
]
)
|> Form.add_form(:post, params: %{text: "post_text0"})
|> Form.add_form(:post, params: %{text: "post_text1"})
|> Form.add_form(:post, params: %{text: "post_text2"})
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}
] = inputs_for(form_for(form, "action"), :post)
form =
form
|> Form.remove_form([:post, 0])
|> Form.remove_form([:post, 0])
|> Form.remove_form([:post, 0])
|> Form.validate(%{})
assert [] = inputs_for(form_for(form, "action"), :post)
assert %{"post" => []} = Form.params(form)
end
test "when all values have been removed from an existing relationship, the empty list remains" do
post1_id = Ash.UUID.generate()
post2_id = Ash.UUID.generate()
comment = %Comment{text: "text", post: [%Post{id: post1_id}, %Post{id: post2_id}]}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: comment.post,
type: :list,
resource: Post,
create_action: :create,
update_action: :update
]
]
)
|> Form.add_form(:post, params: %{text: "post_text3"})
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}},
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}
] = inputs_for(form_for(form, "action"), :post)
form =
form
|> Form.remove_form([:post, 0])
|> Form.remove_form([:post, 0])
|> Form.remove_form([:post, 0])
|> Form.validate(%{})
assert %{"post" => []} = Form.params(form)
end
test "when all values have been removed from an existing `:single` relationship, the empty list remains" do
post_id = Ash.UUID.generate()
comment_2 = %Comment{text: "text"}
comment = %Comment{text: "text", post: %Post{id: post_id, comments: [comment_2]}}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: & &1.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
data: & &1.comments,
create_action: :create,
update_action: :update
]
]
]
]
)
assert [%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}] =
form
|> form_for("action")
|> inputs_for(:post)
form =
form
|> Form.remove_form([:post, :comments, 0])
# This is added by the hidden fields helper, so we add it here to simulate that.
|> Form.validate(%{"post" => %{"_touched" => "comments"}})
assert %{"post" => %{"comments" => []}} = Form.params(form)
end
test "remaining forms are reindexed after a form has been removed" do
post1_id = Ash.UUID.generate()
post2_id = Ash.UUID.generate()
post3_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: [%Post{id: post1_id}, %Post{id: post2_id}, %Post{id: post3_id}]
}
form =
comment
|> Form.for_update(:update,
forms: [
posts: [
data: comment.post,
type: :list,
resource: Post,
create_action: :create,
update_action: :update
]
]
)
|> Form.remove_form([:posts, 1])
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_0,
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_1
] = inputs_for(form_for(form, "action"), :posts)
assert form_0.name == "form[posts][0]"
assert form_0.id == "form_posts_0"
assert form_1.name == "form[posts][1]"
assert form_1.id == "form_posts_1"
end
test "sparse forms can also be removed by index" do
post1_id = Ash.UUID.generate()
post2_id = Ash.UUID.generate()
post3_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: [%Post{id: post1_id}, %Post{id: post2_id}, %Post{id: post3_id}]
}
form =
comment
|> Form.for_update(:update,
forms: [
posts: [
data: comment.post,
type: :list,
resource: Post,
create_action: :create,
update_action: :update,
sparse?: true
]
]
)
|> Form.remove_form([:posts, 1])
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_0,
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}} =
form_1
] = inputs_for(form_for(form, "action"), :posts)
assert form_0.name == "form[posts][0]"
assert form_0.id == "form_posts_0"
assert form_1.name == "form[posts][1]"
assert form_1.id == "form_posts_1"
form =
form
|> Form.remove_form([:posts, 1])
assert [
%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}
] = inputs_for(form_for(form, "action"), :posts)
end
test "when `:single`, `inputs_for` generates a list of one single item" do
post_id = Ash.UUID.generate()
comment = %Comment{text: "text", post: %Post{id: post_id, text: "Some text"}}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: comment.post,
type: :single,
resource: Post,
update_action: :update
]
]
)
assert [%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Post}}] =
inputs_for(form_for(form, "action"), :post)
end
test "it creates nested forms for single resources" do
post_id = Ash.UUID.generate()
comment_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: [%Comment{id: comment_id}]
}
}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: comment.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
data: & &1.comments,
type: :list,
resource: Comment,
update_action: :update,
create_action: :create
]
]
]
]
)
assert [%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Comment}}] =
form
|> form_for("action")
|> inputs_for(:post)
|> hd()
|> inputs_for(:comments)
end
test "it `add_form`s for nested single resources" do
post_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: []
}
}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: comment.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
]
]
)
|> Form.add_form([:post, :comments])
assert [%Phoenix.HTML.Form{source: %AshPhoenix.Form{resource: AshPhoenix.Test.Comment}}] =
form
|> form_for("action")
|> inputs_for(:post)
|> hd()
|> inputs_for(:comments)
end
test "it `remove_form`s for nested single resources" do
post_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: []
}
}
form =
comment
|> Form.for_update(:update,
forms: [
post: [
data: comment.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
type: :list,
resource: Comment,
create_action: :create
]
]
]
]
)
|> Form.add_form([:post, :comments])
|> Form.remove_form([:post, :comments, 0])
assert [] =
form
|> form_for("action")
|> inputs_for(:post)
|> hd()
|> inputs_for(:comments)
end
test "when `remove_form`ing an existing `:single` relationship, a nil value is included in the params - if the form has been touched" do
post =
Post
|> Ash.Changeset.new()
|> Ash.Changeset.set_argument(:author, %{email: "nigel@elixir-lang.org"})
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
form =
post
|> Form.for_update(:update, domain: Domain, forms: [auto?: true])
|> Form.remove_form([:author])
params =
form
|> Form.params()
assert %{"author" => nil} = params
end
test "when add_forming a required argument, the added form should be valid without needing to manually validate it" do
form =
Post
|> Form.for_create(:create_author_required, domain: Domain, forms: [auto?: true])
|> Form.validate(%{"text" => "foo"})
|> Form.add_form([:author], params: %{"email" => "james@foo.com"})
assert form.valid? == true
end
test "add form with nested params generate form with correct name" do
post_id = Ash.UUID.generate()
comment_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: [%Comment{id: comment_id}]
}
}
form =
comment
|> Form.for_update(:update, as: "comment", forms: [auto?: true])
|> Form.add_form([:post])
|> Form.add_form([:post, :comments], params: %{"post" => %{}})
post_form =
form
|> form_for("action")
|> inputs_for(:post)
|> hd()
|> inputs_for(:comments)
|> hd()
|> inputs_for(:post)
|> hd()
assert post_form.name == "comment[post][comments][0][post]"
end
test "update nested form name correctly when remove form in the middle" do
post_id = Ash.UUID.generate()
comment_id = Ash.UUID.generate()
comment = %Comment{
text: "text",
post: %Post{
id: post_id,
text: "Some text",
comments: [%Comment{id: comment_id}]
}
}
form =
comment
|> Form.for_update(:update, as: "comment", forms: [auto?: true])
|> Form.add_form([:post, :comments], params: %{"post" => %{}})
|> Form.add_form([:post, :comments], params: %{"post" => %{}})
|> Form.add_form([:post, :comments], params: %{"post" => %{}})
|> Form.remove_form([:post, :comments, 1])
assert form |> AshPhoenix.Form.get_form([:post, :comments, 0])
assert form |> AshPhoenix.Form.get_form([:post, :comments, 1])
assert form |> AshPhoenix.Form.get_form([:post, :comments, 2])
assert form |> AshPhoenix.Form.get_form([:post, :comments, 2, :post]) |> Map.get(:name) ==
"comment[post][comments][2][post]"
refute form |> AshPhoenix.Form.get_form([:post, :comments, 3])
end
end
describe "issue #259" do
test "updating should not duplicate nested resources" do
post =
Post
|> Ash.Changeset.for_create(:create, %{text: "post"})
|> Ash.create!()
comment =
Comment
|> Ash.Changeset.for_create(:create, %{text: "comment"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()
# Check the persisted post.comments count after create
post = Post |> Ash.get!(post.id) |> Ash.load!(:comments)
assert Enum.count(post.comments) == 1
# Grab the persisted comment
comment = Comment |> Ash.get!(comment.id) |> Ash.load!(post: [:comments])
form =
comment
|> Form.for_update(:update,
as: "comment",
domain: Domain,
forms: [
post: [
data: & &1.post,
type: :single,
resource: Post,
update_action: :update,
create_action: :create,
forms: [
comments: [
data: & &1.comments,
type: :list,
resource: Comment,
update_action: :update,
create_action: :create
]
]
]
]
)
updated_comment =
form
|> AshPhoenix.Form.submit!(
params: %{
"post" => %{
"id" => post.id,
"text" => "text",
"comments" => %{
"0" => %{
"id" => comment.id,
"text" => comment.text
}
}
}
}
)
assert Enum.count(updated_comment.post.comments) == 1
# now, check the persisted post
persisted_post = Post |> Ash.get!(post.id) |> Ash.load!(:comments)
assert Enum.count(persisted_post.comments) == 1
end
end
end