-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Devise a way to find form inputs by their associated labels #44
Comments
@mattwynne I'm curious what you think of the approach proposed in #46 |
I made a similar module to phoenix_test some time ago and I implemented finding form inputs like this: defp find_by_label(form, text) do
case Floki.find(form, "label:fl-icontains('#{text}')") do
[label] ->
case Floki.attribute(label, "for") do
[id] ->
case Floki.find(form, "input[id='#{id}']") do
[input] -> {:ok, input}
[] -> raise "No input found for label: #{inspect(label)}"
end
[] ->
raise "No 'for' attribute found for label: #{inspect(label)}"
end
[] ->
:error
_many ->
raise "Multiple labels found matching: #{inspect(text)}"
end
end Here's the full thing just in case:defmodule Browser do
## PUBLIC API
defmacro visit(conn, path) do
quote do
Browser.visit(@endpoint, unquote(conn), unquote(path))
end
end
def visit(endpoint, conn, path) do
conn = put(conn, :endpoint, endpoint)
request(conn, :get, path)
end
def request(conn, method, path) do
endpoint = get(conn, :endpoint) || conn.private[:phoenix_endpoint]
conn
|> Phoenix.ConnTest.dispatch(endpoint, method, path)
|> follow_redirects()
|> put(:endpoint, endpoint)
end
@doc """
Fill in form input (text, email, passwsord) or textarea
"""
def fill_in(conn, selector, opts) do
value = Keyword.fetch!(opts, :with)
conn = ensure_doc(conn)
doc = get(conn, :doc)
found =
doc
|> find_forms()
|> Enum.find_value(fn {form, index} ->
case find_by_label(form, selector) do
{:ok, input} ->
case Floki.attribute(input, "name") do
[name] -> {index, name}
end
:error ->
nil
end
end)
if !found do
raise "No input found for: #{inspect(selector)}"
end
{index, name} = found
put_form_value(conn, index, name, value)
end
def click(conn, selector) do
elements = elements(conn, :click, selector)
case elements do
[{:form, form, index} | _] -> submit_form(conn, form, index)
[{:link, path} | _] -> request(conn, :get, path)
[{:link, path, method} | _] -> request(conn, String.to_existing_atom(method), path)
[] -> raise "No link or submit button found for: #{inspect(selector)}"
end
end
def elements(%Plug.Conn{} = conn, scope, selector) do
conn = ensure_doc(conn)
doc = get(conn, :doc)
elements(doc, scope, selector)
end
def elements(response, scope, selector) when is_binary(response) do
doc = Floki.parse_document!(response)
elements(doc, scope, selector)
end
def elements(doc, :click, selector) do
find_form_submit_buttons(doc, selector) ++ find_links(doc, selector)
end
def open_browser(conn) do
path = Path.join([System.tmp_dir!(), "#{Phoenix.LiveView.Utils.random_id()}.html"])
File.write!(path, conn.resp_body)
System.cmd("open", [path])
conn
end
defmacro assert_html_response(conn, status) do
quote do
assert html_response(unquote(conn), unquote(status))
# unquote(conn)
end
end
## PRIVATE
defp ensure_doc(conn) do
case get(conn, :doc) do
nil -> put(conn, :doc, Floki.parse_document!(conn.resp_body))
_ -> conn
end
end
defp get(conn, key, default \\ nil) do
conn.private[__MODULE__][key] || default
end
defp put(conn, key, value) do
data = Map.put(conn.private[__MODULE__] || %{}, key, value)
Plug.Conn.put_private(conn, __MODULE__, data)
end
defp clear(conn) do
Plug.Conn.put_private(conn, __MODULE__, nil)
end
def put_form_value(conn, index, name, value) do
forms = get(conn, :forms, %{})
form = forms[index] || %{}
form = Map.put(form, name, value)
forms = Map.put(forms, index, form)
put(conn, :forms, forms)
end
defp find_forms(doc) do
doc
|> Floki.find("form")
|> Enum.with_index()
end
defp submit_form(conn, form, index) do
[action] = Floki.attribute(form, "action")
method =
case Floki.attribute(form, "method") do
[method] -> method
[] -> "post"
end
default_values =
form
|> Floki.find("input, textarea")
|> Enum.reduce(%{}, fn field, values ->
case Floki.attribute(field, "name") do
[name] ->
case Floki.attribute(field, "value") do
[value] ->
Map.put(values, name, value)
[] ->
Map.put(values, name, "")
end
[] ->
values
end
end)
user_values = get(conn, :forms)[index] || %{}
params =
default_values
|> Map.merge(user_values)
|> Enum.map(fn {k, v} -> "#{k}=#{URI.encode_www_form(v)}" end)
|> Enum.join("&")
|> Plug.Conn.Query.decode()
follow_redirects(
Phoenix.ConnTest.dispatch(
Phoenix.ConnTest.recycle(clear(conn)),
conn.private[:phoenix_endpoint],
method,
action,
params
)
)
end
defp follow_redirects(conn) do
if conn.status in [301, 302] do
n = get(conn, :redirects, 0)
if n > 5 do
raise "Too many redirects"
end
conn = put(conn, :redirects, n + 1)
follow_redirects(
Phoenix.ConnTest.dispatch(
Phoenix.ConnTest.recycle(conn),
conn.private[:phoenix_endpoint],
:get,
Phoenix.ConnTest.redirected_to(conn),
nil
)
)
else
conn
end
end
defp find_by_label(form, text) do
case Floki.find(form, "label:fl-icontains('#{text}')") do
[label] ->
case Floki.attribute(label, "for") do
[id] ->
case Floki.find(form, "input[id='#{id}']") do
[input] -> {:ok, input}
[] -> raise "No input found for label: #{inspect(label)}"
end
[] ->
raise "No 'for' attribute found for label: #{inspect(label)}"
end
[] ->
:error
_many ->
raise "Multiple labels found matching: #{inspect(text)}"
end
end
# defp find_form_submit_button(doc, text) do
# doc
# |> find_forms()
# |> Enum.find_value(fn {form, index} ->
# case Floki.find(form, "button[type=submit]:fl-icontains('#{text}')") do
# [_button] -> {:form, form, index}
# [] -> nil
# _many -> raise "Multiple buttons found matching: #{inspect(text)}"
# end
# end)
# end
defp find_form_submit_buttons(doc, text) do
for {form, index} <- find_forms(doc),
_button <- Floki.find(form, "button[type=submit]:fl-icontains('#{text}')") do
{:form, form, index}
end
end
defp find_links(doc, text) do
for link <- Floki.find(doc, "a:fl-icontains('#{text}')") do
href = List.first(Floki.attribute(link, "href"))
case Floki.attribute(link, "data-method") do
[method] -> {:link, href, method}
[] -> {:link, href}
end
end
end
# defp find_link(doc, text) do
# case Floki.find(doc, "a:fl-icontains('#{text}')") do
# [link] -> {:link, List.first(Floki.attribute(link, "href"))}
# [] -> nil
# _many -> raise "Multiple links found matching: #{inspect(text)}"
# end
# end
end and the client side looks like this: conn
|> visit("/sign-up")
|> fill_in("Name", with: "John Doe")
|> fill_in("Email", with: "[email protected]")
|> fill_in("Password", with: "passwordpassword")
|> click("Create account") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For example, we'd like to be able to do something like this:
That
field
function needs to be able to walk the DOM from the label to the associated field, which isn't possible with CSS selectors.I wonder if we could expose an extension point in
assert_has
that gives the user the DOM so they can walk around it themselves, or maybe support XPath too?The text was updated successfully, but these errors were encountered: