yak(behavior-tests-web-core-components-uyju): behavior tests: web core components
This commit is contained in:
@@ -0,0 +1,226 @@
|
|||||||
|
defmodule FirehoseWeb.CoreComponentsTest do
|
||||||
|
# Pure render tests: no conn, no database, so plain ExUnit.Case (the same
|
||||||
|
# choice layouts_test.exs makes) instead of the ConnCase sandbox.
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias FirehoseWeb.CoreComponents
|
||||||
|
|
||||||
|
@checkbox_assigns %{
|
||||||
|
type: "checkbox",
|
||||||
|
name: "prefs[news]",
|
||||||
|
id: "prefs_news",
|
||||||
|
label: "News",
|
||||||
|
value: "true",
|
||||||
|
rest: %{},
|
||||||
|
errors: [],
|
||||||
|
field: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@select_assigns %{
|
||||||
|
type: "select",
|
||||||
|
name: "role",
|
||||||
|
id: "role",
|
||||||
|
label: "Role",
|
||||||
|
prompt: "Pick one",
|
||||||
|
options: [{"Admin", "admin"}, {"Editor", "editor"}],
|
||||||
|
value: "admin",
|
||||||
|
multiple: false,
|
||||||
|
rest: %{},
|
||||||
|
errors: [],
|
||||||
|
field: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@textarea_assigns %{
|
||||||
|
type: "textarea",
|
||||||
|
name: "body",
|
||||||
|
id: "body",
|
||||||
|
label: "Body",
|
||||||
|
value: "hello world",
|
||||||
|
rest: %{},
|
||||||
|
errors: [],
|
||||||
|
field: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "input/1 checkbox" do
|
||||||
|
test "a checked box renders checked when the value is true" do
|
||||||
|
html = render_checkbox(%{})
|
||||||
|
|
||||||
|
assert attr(html, "input[type='checkbox']", "name") == "prefs[news]"
|
||||||
|
assert attr(html, "input[type='checkbox']", "value") == "true"
|
||||||
|
assert attr(html, "input[type='checkbox']", "checked") == ""
|
||||||
|
assert text(html) =~ "News"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "an unchecked box still posts false for the field" do
|
||||||
|
html = render_checkbox(%{value: "false"})
|
||||||
|
|
||||||
|
assert attr(html, "input[type='checkbox']", "checked") == nil
|
||||||
|
|
||||||
|
# Markup is the behaviour here: the hidden field carrying the same name is
|
||||||
|
# what makes an unchecked box post "false" instead of posting nothing.
|
||||||
|
assert attr(html, "input[type='hidden']", "name") == "prefs[news]"
|
||||||
|
assert attr(html, "input[type='hidden']", "value") == "false"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows the error message when the field has errors" do
|
||||||
|
html = render_checkbox(%{errors: ["can't be blank"]})
|
||||||
|
|
||||||
|
assert text(html) =~ "can't be blank"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "input/1 select" do
|
||||||
|
test "renders the prompt, every option, and preselects the current value" do
|
||||||
|
html = render_select(%{})
|
||||||
|
|
||||||
|
assert text(html) =~ "Pick one"
|
||||||
|
assert text(html) =~ "Admin"
|
||||||
|
assert text(html) =~ "Editor"
|
||||||
|
# The outcome, not the serialisation: the option carrying the current
|
||||||
|
# value is selected, the other one is not.
|
||||||
|
assert attr(html, "option[value='admin']", "selected") == ""
|
||||||
|
assert attr(html, "option[value='editor']", "selected") == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "marks the select erroneous and shows the message when given errors" do
|
||||||
|
html = render_select(%{errors: ["is invalid"]})
|
||||||
|
|
||||||
|
assert text(html) =~ "is invalid"
|
||||||
|
assert "select-error" in classes(html, "select")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "input/1 textarea" do
|
||||||
|
test "keeps the value inside the textarea" do
|
||||||
|
html = render_textarea(%{value: "hello world"})
|
||||||
|
|
||||||
|
# normalize_value/2 prefixes the content with a newline, so compare the
|
||||||
|
# trimmed text: what matters is the value, not the surrounding whitespace.
|
||||||
|
assert String.trim(text(html, "textarea")) == "hello world"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders an empty textarea for a nil value instead of the text nil" do
|
||||||
|
html = render_textarea(%{value: nil})
|
||||||
|
|
||||||
|
assert String.trim(text(html, "textarea")) == ""
|
||||||
|
refute html =~ "nil"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "keeps a leading newline in the value" do
|
||||||
|
# HTML drops one leading newline of a <textarea> before the browser sees
|
||||||
|
# it, so normalize_value/2 prefixes the content with one: a value that
|
||||||
|
# starts with a newline survives, and one that does not gain one.
|
||||||
|
html = render_textarea(%{value: "\nsecond line"})
|
||||||
|
|
||||||
|
assert text(html, "textarea") == "\nsecond line"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "marks the textarea erroneous and shows the message when given errors" do
|
||||||
|
html = render_textarea(%{errors: ["is too short"]})
|
||||||
|
|
||||||
|
assert text(html) =~ "is too short"
|
||||||
|
assert "textarea-error" in classes(html, "textarea")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "button/1" do
|
||||||
|
test "renders a button element when given no destination" do
|
||||||
|
html = render_button(%{})
|
||||||
|
|
||||||
|
assert count(html, "button") == 1
|
||||||
|
assert count(html, "a") == 0
|
||||||
|
assert String.trim(text(html)) == "Save"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders a link to the destination when given an href, keeping the label" do
|
||||||
|
html = render_button(%{rest: %{href: "/somewhere"}})
|
||||||
|
|
||||||
|
assert attr(html, "a", "href") == "/somewhere"
|
||||||
|
assert String.trim(text(html)) == "Save"
|
||||||
|
assert count(html, "button") == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders a live-navigation link when given navigate" do
|
||||||
|
html = render_button(%{rest: %{navigate: "/elsewhere"}})
|
||||||
|
|
||||||
|
# data-phx-link is the only thing distinguishing a live-navigation link
|
||||||
|
# from a full-page one, so it is the behaviour to pin here.
|
||||||
|
assert attr(html, "a", "data-phx-link") == "redirect"
|
||||||
|
assert attr(html, "a", "href") == "/elsewhere"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_checkbox(overrides) do
|
||||||
|
render_component(&CoreComponents.input/1, Map.merge(@checkbox_assigns, overrides))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_select(overrides) do
|
||||||
|
render_component(&CoreComponents.input/1, Map.merge(@select_assigns, overrides))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_textarea(overrides) do
|
||||||
|
render_component(&CoreComponents.input/1, Map.merge(@textarea_assigns, overrides))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_button(overrides) do
|
||||||
|
assigns =
|
||||||
|
Map.merge(
|
||||||
|
%{
|
||||||
|
variant: nil,
|
||||||
|
class: "",
|
||||||
|
rest: %{},
|
||||||
|
# A slot, as HEEx builds it: a list of entries whose inner_block takes
|
||||||
|
# (argument, changed).
|
||||||
|
inner_block: [%{inner_block: fn _, _ -> "Save" end}]
|
||||||
|
},
|
||||||
|
overrides
|
||||||
|
)
|
||||||
|
|
||||||
|
render_component(&CoreComponents.button/1, assigns)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Every attribute assertion goes through here, so the LazyHTML mechanism lives
|
||||||
|
# in one place: one value per matched element, nil when the element (or the
|
||||||
|
# attribute) is absent, "" for a valueless attribute such as `checked`.
|
||||||
|
defp attr(html, selector, name) do
|
||||||
|
html
|
||||||
|
|> nodes(selector)
|
||||||
|
|> LazyHTML.attribute(name)
|
||||||
|
|> List.first()
|
||||||
|
end
|
||||||
|
|
||||||
|
# The class tokens of every matched element, for the "this field is marked
|
||||||
|
# erroneous" assertions.
|
||||||
|
defp classes(html, selector) do
|
||||||
|
html
|
||||||
|
|> nodes(selector)
|
||||||
|
|> LazyHTML.attribute("class")
|
||||||
|
|> Enum.flat_map(&String.split/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp nodes(html, selector) do
|
||||||
|
html
|
||||||
|
|> LazyHTML.from_fragment()
|
||||||
|
|> LazyHTML.query(selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp count(html, selector) do
|
||||||
|
html
|
||||||
|
|> nodes(selector)
|
||||||
|
|> Enum.count()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp text(html) do
|
||||||
|
html
|
||||||
|
|> LazyHTML.from_fragment()
|
||||||
|
|> LazyHTML.text()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp text(html, selector) do
|
||||||
|
html
|
||||||
|
|> nodes(selector)
|
||||||
|
|> LazyHTML.text()
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user