analytics/test/plausible_web/live/components/combo_box_test.exs
hq1 0822bc61df
Props Settings UI to match Goals Settings (#3322)
* Add hint to creatable ComboBoxes without suggestions available

* Load external resources once in funnel settings

* Load external resources once in goal settings

* Make Custom Props Settings UI match Goal Settings

* Remove unnecessary goals query

This should be done only once in the live view

* Remove funnels feature flag

* fixup

* Make the modal scrollable

* By default, focus first suggestion for creatables

* Add sample props to seeds

* Load all suggestions asynchronously, unless `Mix.env == :test`

* ComboBox: Fix inconsistent suggestions

We require "Create ..." element to be only focused
when there are no suggestions available.
This causes some issues, depending on the state,
the least focusable index might be either 0 ("Create...")
or 1. This patch addresses all the quirks with focus.

* Fix ComboBox max results message

So that AlpineJS doesn't think it's a focusable
option.

* Keep the state up to date when changing props

* Update seeds with sensible prop names

* Make escape work for closing combobox suggestions

Co-authored-by: Uku Taht <Uku.taht@gmail.com>

* Revert "Make escape work for closing combobox suggestions"

This reverts commit 306866d2a1.

@ukutaht unfortunately this makes it impossible to select
an suggestion.

* Revert "Revert "Make escape work for closing combobox suggestions""

This reverts commit 4844857812.

* Make ESC great again

* Improve readability

---------

Co-authored-by: Uku Taht <Uku.taht@gmail.com>
2023-09-13 14:55:29 +02:00

375 lines
12 KiB
Elixir

defmodule PlausibleWeb.Live.Components.ComboBoxTest do
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
alias PlausibleWeb.Live.Components.ComboBox
@ul "ul#dropdown-test-component[x-show=isOpen][x-ref=suggestions]"
describe "static rendering" do
test "renders suggestions" do
assert doc = render_sample_component(new_options(10))
assert element_exists?(
doc,
~s/input#test-component[name="display-test-component"][phx-change="search"]/
)
assert element_exists?(doc, @ul)
for i <- 1..10 do
assert element_exists?(doc, suggestion_li(i))
end
end
test "renders up to 15 suggestions by default" do
assert doc = render_sample_component(new_options(20))
assert element_exists?(doc, suggestion_li(14))
assert element_exists?(doc, suggestion_li(15))
refute element_exists?(doc, suggestion_li(16))
refute element_exists?(doc, suggestion_li(17))
assert Floki.text(doc) =~ "Max results reached"
end
test "renders up to n suggestions if provided" do
assert doc = render_sample_component(new_options(20), suggestions_limit: 10)
assert element_exists?(doc, suggestion_li(9))
assert element_exists?(doc, suggestion_li(10))
refute element_exists?(doc, suggestion_li(11))
refute element_exists?(doc, suggestion_li(12))
end
test "Alpine.js: renders attrs focusing suggestion elements" do
assert doc = render_sample_component(new_options(10))
li1 = doc |> find(suggestion_li(1)) |> List.first()
li2 = doc |> find(suggestion_li(2)) |> List.first()
assert text_of_attr(li1, "@mouseenter") == "setFocus(1)"
assert text_of_attr(li2, "@mouseenter") == "setFocus(2)"
assert text_of_attr(li1, "x-bind:class") =~ "focus === 1"
assert text_of_attr(li2, "x-bind:class") =~ "focus === 2"
end
test "Alpine.js: component sets up keyboard navigation" do
main =
new_options(2)
|> render_sample_component()
|> find("div#input-picker-main-test-component")
assert text_of_attr(main, "x-on:keydown.arrow-up.prevent") == "focusPrev"
assert text_of_attr(main, "x-on:keydown.arrow-down.prevent") == "focusNext"
assert text_of_attr(main, "x-on:keydown.enter.prevent") == "select"
end
test "Alpine.js: component sets up close on click-away" do
assert new_options(2)
|> render_sample_component()
|> find("div#input-picker-main-test-component div div")
|> text_of_attr("@click.away") == "close"
end
test "Alpine.js: component sets up open on focusing the display input" do
assert new_options(2)
|> render_sample_component()
|> find("input#test-component")
|> text_of_attr("x-on:focus") == "open"
end
test "Alpine.js: dropdown is annotated and shows when isOpen is true" do
dropdown =
new_options(2)
|> render_sample_component()
|> find("#dropdown-test-component")
assert text_of_attr(dropdown, "x-show") == "isOpen"
assert text_of_attr(dropdown, "x-ref") == "suggestions"
end
test "Dropdown shows a notice when no suggestions exist" do
doc = render_sample_component([])
assert text_of_element(doc, "#dropdown-test-component") ==
"No matches found. Try searching for something different."
end
test "dropdown suggests user input when creatable" do
doc =
render_sample_component([{"USD", "US Dollar"}, {"EUR", "Euro"}],
creatable: true,
display_value: "Brazilian Real"
)
assert text_of_element(doc, "#dropdown-test-component-option-0") ==
~s(Create "Brazilian Real")
assert text_of_element(doc, "#dropdown-test-component-option-1") == "US Dollar"
assert text_of_element(doc, "#dropdown-test-component-option-2") == "Euro"
refute text_of_element(doc, "#dropdown-test-component") ==
"No matches found. Try searching for something different."
end
test "when no options available, hints the user to create one by typing" do
doc =
render_sample_component([],
creatable: true
)
assert text_of_element(doc, "#dropdown-test-component div") == "Create an item by typing."
end
test "makes the html input required when required option is passed" do
input_query = "input[type=text][required]"
assert render_sample_component([], required: true) |> element_exists?(input_query)
refute render_sample_component([]) |> element_exists?(input_query)
end
test "adds class to html element when class option is passed" do
assert render_sample_component([], class: "animate-spin")
|> element_exists?("#input-picker-main-test-component.animate-spin")
end
end
describe "integration" do
defmodule SampleView do
use Phoenix.LiveView
defmodule SampleSuggest do
def suggest("Echo me", options) do
[{length(options), "Echo me"}]
end
def suggest("all", options) do
options
end
end
def render(assigns) do
~H"""
<.live_component
submit_name="some_submit_name"
module={PlausibleWeb.Live.Components.ComboBox}
suggest_fun={&SampleSuggest.suggest/2}
id="test-component"
options={for i <- 1..20, do: {i, "Option #{i}"}}
suggestions_limit={7}
/>
"""
end
end
test "uses the suggestions module", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleView, session: %{})
doc = type_into_combo(lv, "test-component", "Echo me")
assert text_of_element(doc, "#dropdown-test-component-option-1") == "Echo me"
end
test "stores selected value", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleView, session: %{})
type_into_combo(lv, "test-component", "Echo me")
doc =
lv
|> element("li#dropdown-test-component-option-1 a")
|> render_click()
assert element_exists?(doc, "input[type=hidden][name=some_submit_name][value=20]")
end
test "limits the suggestions", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleView, session: %{})
doc = type_into_combo(lv, "test-component", "all")
assert element_exists?(doc, suggestion_li(6))
assert element_exists?(doc, suggestion_li(7))
refute element_exists?(doc, suggestion_li(8))
refute element_exists?(doc, suggestion_li(9))
end
test "clearing search input resets to all options", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleView, session: %{})
type_into_combo(lv, "test-component", "Echo me")
doc = type_into_combo(lv, "test-component", "")
for i <- 1..7, do: assert(element_exists?(doc, suggestion_li(i)))
end
end
describe "creatable integration" do
defmodule CreatableView do
use Phoenix.LiveView
def render(assigns) do
~H"""
<.live_component
submit_name="some_submit_name"
module={PlausibleWeb.Live.Components.ComboBox}
suggest_fun={&ComboBox.StaticSearch.suggest/2}
id="test-creatable-component"
options={for i <- 1..20, do: {i, "Option #{i}"}}
creatable
/>
"""
end
end
test "stores selected value from suggestion", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
type_into_combo(lv, "test-creatable-component", "option 20")
doc =
lv
|> element("li#dropdown-test-creatable-component-option-1 a")
|> render_click()
assert element_exists?(doc, "input[type=hidden][name=some_submit_name][value=20]")
end
test "suggests creating custom value", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
assert lv
|> type_into_combo("test-creatable-component", "my new option")
|> text_of_element("li#dropdown-test-creatable-component-option-0 a") ==
~s(Create "my new option")
end
test "does not suggest creating value when input exact matches a suggestion", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
assert lv
|> type_into_combo("test-creatable-component", "Option 1")
|> text_of_element("li#dropdown-test-creatable-component-option-1 a") == ~s(Option 1)
end
test "stores new value by clicking on the dropdown custom option", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
type_into_combo(lv, "test-creatable-component", "my new option")
doc =
lv
|> element("li#dropdown-test-creatable-component-option-0 a")
|> render_click()
assert element_exists?(
doc,
"input[type=hidden][name=some_submit_name][value=\"my new option\"]"
)
end
test "stores new value while typing", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
assert lv
|> type_into_combo("test-creatable-component", "my new option")
|> element_exists?(
"input[type=hidden][name=some_submit_name][value=\"my new option\"]"
)
end
end
describe "async suggestions" do
defmodule SampleViewAsync do
use Phoenix.LiveView
defmodule SampleSuggest do
def suggest("", []) do
:timer.sleep(500)
[{1, "One"}, {2, "Two"}, {3, "Three"}]
end
def suggest("Echo me", _options) do
:timer.sleep(500)
[{1, "Echo me"}]
end
end
def render(assigns) do
~H"""
<.live_component
submit_name="some_submit_name"
module={PlausibleWeb.Live.Components.ComboBox}
suggest_fun={&SampleSuggest.suggest/2}
id="test-component"
async={true}
suggestions_limit={7}
/>
"""
end
end
test "options are empty at immediate render" do
doc =
render_component(
ComboBox,
submit_name: "test-submit-name",
id: "test-component",
suggest_fun: &ComboBox.StaticSearch.suggest/2,
async: true
)
refute element_exists?(doc, "#dropdown-test-component-option-1")
end
test "pre-fills the suggestions asynchronously", %{conn: conn} do
{:ok, lv, doc} = live_isolated(conn, SampleViewAsync, session: %{})
refute element_exists?(doc, "#dropdown-test-component-option-1")
:timer.sleep(1000)
doc = render(lv)
assert text_of_element(doc, "#dropdown-test-component-option-1") == "One"
assert text_of_element(doc, "#dropdown-test-component-option-2") == "Two"
assert text_of_element(doc, "#dropdown-test-component-option-3") == "Three"
end
test "uses the suggestions function asynchronously", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleViewAsync, session: %{})
doc = type_into_combo(lv, "test-component", "Echo me")
refute element_exists?(doc, "#dropdown-test-component-option-1")
:timer.sleep(1000)
doc = render(lv)
assert element_exists?(doc, "#dropdown-test-component-option-1")
assert text_of_element(doc, "#dropdown-test-component-option-1") == "Echo me"
end
end
defp render_sample_component(options, extra_opts \\ []) do
render_component(
ComboBox,
Keyword.merge(
[
options: options,
submit_name: "test-submit-name",
id: "test-component",
suggest_fun: &ComboBox.StaticSearch.suggest/2
],
extra_opts
)
)
end
defp new_options(n) do
Enum.map(1..n, &{&1, "TestOption #{&1}"})
end
defp suggestion_li(idx) do
~s/#{@ul} li#dropdown-test-component-option-#{idx}/
end
defp type_into_combo(lv, id, text) do
lv
|> element("input##{id}")
|> render_change(%{
"_target" => ["display-#{id}"],
"display-#{id}" => "#{text}"
})
end
end