defmodule PlausibleWeb.Live.Components.ComboBoxTest do use PlausibleWeb.ConnCase, async: true import Phoenix.LiveViewTest 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 preselected default value (tuple)" do options = new_options(10) assert doc = render_sample_component(options, selected: List.last(options)) assert element_exists?(doc, ~s|input[type="hidden"][name="test-submit-name"][value="10"]|) assert element_exists?( doc, ~s|input[type="text"][name="display-test-component"][value="TestOption 10"]| ) end test "renders preselected default value (string)" do options = [{"a", "a"}, {"b", "b"}, {"c", "c"}] assert doc = render_sample_component(options, selected: "b") assert element_exists?(doc, "input[type=hidden][name=test-submit-name][value=b]") assert element_exists?( doc, ~s|input[type=text][name=display-test-component][value="b"]| ) 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 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)) li2 = doc |> find(suggestion_li(2)) 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, ~s|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, ~s|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 test "selectionInProgress doesn't apply when no server roundtrip is needed", %{conn: conn} do {:ok, lv, html} = live_isolated(conn, CreatableView, session: %{}) regular_option = find(html, "li#dropdown-test-creatable-component-option-1 a") assert text_of_attr(regular_option, "x-on:click") == "selectionInProgress = true" creatable_option = lv |> type_into_combo("test-creatable-component", "my new option") |> find("input[type=hidden][name=some_submit_name][value=\"my new option\"]") assert text_of_attr(creatable_option, "x-on:click") == nil end end describe "async suggestions" do defmodule SampleViewAsync do use Phoenix.LiveView defmodule SampleSuggest do def suggest("", []) do :timer.sleep(600) [{1, "One"}, {2, "Two"}, {3, "Three"}] end def suggest("Echo me", _options) do :timer.sleep(510) [{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 @tag :slow 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") assert eventually( fn -> doc = render(lv) { text_of_element(doc, "#dropdown-test-component-option-1") == "One" && text_of_element(doc, "#dropdown-test-component-option-2") == "Two" && text_of_element(doc, "#dropdown-test-component-option-3") == "Three", true } end, 50, 50 ) end @tag :slow 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") assert eventually( fn -> doc = render(lv) { element_exists?(doc, "#dropdown-test-component-option-1") && text_of_element(doc, "#dropdown-test-component-option-1") == "Echo me", true } end, 50, 50 ) 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