From 4717fbfc1674849b7768672049dced497c379228 Mon Sep 17 00:00:00 2001 From: Harald Ringvold Date: Thu, 12 Dec 2024 16:56:04 +0100 Subject: [PATCH] Add tests for quick_tags and fix failing tags tests --- lib/live_select/component.ex | 6 +- lib/live_select/component.html.heex | 2 +- test/live_select_quick_tags_test.exs | 534 +++++++++++++++++++++++++++ 3 files changed, 537 insertions(+), 5 deletions(-) create mode 100644 test/live_select_quick_tags_test.exs diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index b3f1a38..6a5eb4c 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -435,13 +435,13 @@ defmodule LiveSelect.Component do extra_params ) when active_option >= 0 do - option = Enum.at(socket.assigns.options, active_option) + option = Enum.at(options, active_option) if already_selected?(option, selection) do pos = get_selection_index(option, selection) unselect(socket, pos) else - select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params) + select(socket, Enum.at(options, active_option), extra_params) end end @@ -710,7 +710,6 @@ defmodule LiveSelect.Component do defp next_selectable(%{ options: options, active_option: active_option, - selection: selection, mode: :quick_tags }) do options @@ -739,7 +738,6 @@ defmodule LiveSelect.Component do defp prev_selectable(%{ options: options, active_option: active_option, - selection: selection, mode: :quick_tags }) do options diff --git a/lib/live_select/component.html.heex b/lib/live_select/component.html.heex index a55681a..116edf3 100644 --- a/lib/live_select/component.html.heex +++ b/lib/live_select/component.html.heex @@ -124,7 +124,7 @@ ) ) } - data-idx={idx} + data-idx={if @mode == :quick_tags or not already_selected?(option, @selection), do: idx} > <%= if @option == [] do %> <%= option.label %> diff --git a/test/live_select_quick_tags_test.exs b/test/live_select_quick_tags_test.exs new file mode 100644 index 0000000..1e7b71e --- /dev/null +++ b/test/live_select_quick_tags_test.exs @@ -0,0 +1,534 @@ +defmodule LiveSelectQuickTagsTest do + @moduledoc false + + use LiveSelectWeb.ConnCase, async: true + + import LiveSelect.TestHelpers + + setup %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=quick_tags") + + %{live: live} + end + + test "can select multiple options", %{live: live} do + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 2, method: :key) + + type(live, "ABC") + + select_nth_option(live, 4, method: :click) + + assert_selected_multiple(live, ~w(B D)) + end + + test "already selected options are selectable in the dropdown using keyboard", %{live: live} do + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 2) + + type(live, "ABC") + navigate(live, 2, :down) + keydown(live, "Enter") + + assert_selected_multiple(live, ~w()) + + type(live, "ABC") + navigate(live, 10, :down) + navigate(live, 10, :up) + keydown(live, "Enter") + end + + test "already selected options are selectable in the dropdown using mouseclick", %{ + live: live + } do + select_and_open_dropdown(live, 2) + + assert_selected_multiple(live, ~w(B)) + + select_nth_option(live, 2, method: :click) + + assert_selected_multiple(live, ~w()) + end + + test "hitting enter with only one option selects it", %{live: live} do + stub_options(~w(A)) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple(live, ~w(A)) + end + + test "hitting enter with more than one option does not select", %{live: live} do + stub_options(~w(A B)) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, []) + end + + test "hitting enter with only one option does not select it if already selected", %{live: live} do + stub_options(~w(A)) + + type(live, "ABC") + + select_nth_option(live, 1) + + assert_selected_multiple(live, ~w(A)) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, ~w(A)) + end + + describe "when user_defined_options = true" do + setup %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags&user_defined_options=true&update_min_len=3") + %{live: live} + end + + test "hitting enter adds entered text to selection", %{live: live} do + stub_options(["A", "B"]) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple(live, ["ABC"]) + end + + test "hitting enter does not add text to selection if element with same label is already selected", + %{live: live} do + stub_options(["ABC", "DEF"]) + + type(live, "ABC") + + select_nth_option(live, 1, method: :key) + + assert_selected_multiple(live, ["ABC"]) + + type(live, "ABC") + + assert_options(live, ["ABC", "DEF"]) + + keydown(live, "Enter") + + assert_selected_multiple_static(live, ["ABC"]) + end + + test "hitting enter adds text to selection even if there is only one available option", %{ + live: live + } do + stub_options(["A"]) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple(live, ["ABC"]) + end + + test "text added to selection should be trimmed", %{live: live} do + stub_options([]) + + type(live, " ABC ") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, ["ABC"]) + end + + test "text with only whitespace is ignored and not added to selection", %{live: live} do + stub_options(["ABC"]) + + type(live, " ") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, []) + end + + test "text shorter than update_min_len is ignored and not added to selection", %{live: live} do + stub_options([{"ABC", 1}, {"DEF", 2}]) + + type(live, "AB") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, []) + end + + test "hitting enter while options are awaiting update does not select", %{live: live} do + stub_options(~w(A B C), delay_forever: true) + + type(live, "ABC") + + keydown(live, "Enter") + + assert_selected_multiple_static(live, []) + end + + test "one can still select options from the dropdown", %{live: live} do + stub_options(~w(A B C)) + + type(live, "ABC") + + select_nth_option(live, 1, method: :key) + + type(live, "ABC") + + select_nth_option(live, 2, method: :click) + + assert_selected_multiple(live, ~w(A B)) + end + end + + describe "when max_selectable option is set" do + setup %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags&max_selectable=2") + + %{live: live} + end + + test "prevents selection of more than max_selectable options", %{live: live} do + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 2, method: :key) + + type(live, "ABC") + + select_nth_option(live, 4, method: :click) + + assert_selected_multiple(live, ~w(B D)) + + type(live, "ABC") + + select_nth_option(live, 3, method: :click) + + assert_selected_multiple_static(live, ~w(B D)) + end + end + + test "can remove selected options by clicking on tag", %{live: live} do + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 2) + + type(live, "ABC") + + select_nth_option(live, 3) + + type(live, "ABC") + + select_nth_option(live, 1) + + assert_selected_multiple(live, ~w(B C A)) + + unselect_nth_option(live, 2) + + assert_selected_multiple(live, ~w(B A)) + end + + test "can set an option as sticky so it can't be removed", %{live: live} do + options = + [ + %{tag_label: "R", value: "Rome", sticky: true}, + %{tag_label: "NY", value: "New York"} + ] + |> Enum.sort() + + sticky_pos = + Enum.find_index(options, & &1[:sticky]) + 1 + + stub_options(options) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2) + + refute_option_removable(live, sticky_pos) + + assert_option_removable(live, 3 - sticky_pos) + end + + test "can specify alternative labels for tags using maps", %{live: live} do + options = + [%{tag_label: "R", value: "Rome"}, %{tag_label: "NY", value: "New York"}] |> Enum.sort() + + stub_options(options) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2) + + assert_selected_multiple(live, options |> Enum.map(&Map.put(&1, :label, &1.value))) + end + + test "can specify alternative labels for tags using keywords", %{live: live} do + options = + [[tag_label: "R", value: "Rome"], [tag_label: "NY", value: "New York"]] |> Enum.sort() + + stub_options(options) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2) + + selection = + options + |> Enum.map(&Map.new/1) + |> Enum.map(&Map.put(&1, :label, &1[:value])) + + assert_selected_multiple(live, selection) + end + + test "can be disabled", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?disabled=true&mode=tags") + + assert element(live, selectors()[:text_input]) + |> render() + |> Floki.parse_fragment!() + |> Floki.attribute("disabled") == ["disabled"] + end + + test "can clear the selection", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2, method: :click) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, value: nil) + + assert_selected_multiple(live, []) + end + + test "can force the selection", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2, method: :click) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, value: ~w(B C)) + + assert_selected_multiple(live, ~w(B C)) + end + + test "can force the selection and options", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2, method: :click) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, value: [3, 5], options: [{"C", 3}, {"D", 4}, {"E", 5}]) + + assert_selected_multiple(live, [%{label: "C", value: 3}, %{label: "E", value: 5}]) + end + + test "can render custom clear button", %{conn: conn} do + {:ok, live, _html} = live(conn, "/live_component_test") + + type(live, "Ber", + component: "#my_form_city_search_custom_clear_tags_live_select_component", + parent: "#form_component" + ) + + select_nth_option(live, 1, + component: "#my_form_city_search_custom_clear_tags_live_select_component" + ) + + assert element( + live, + "#my_form_city_search_custom_clear_tags_live_select_component button[data-idx=0]", + "custom clear button" + ) + |> has_element? + end + + defp select_and_open_dropdown(live, pos) do + if pos < 1 || pos > 4, do: raise("pos must be between 1 and 4") + + stub_options(~w(A B C D)) + + type(live, "ABC") + + select_nth_option(live, 2) + + type(live, "ABC") + + :ok + end + + describe "when focus and blur events are set" do + setup %{conn: conn} do + {:ok, live, _html} = + live(conn, "/?phx-focus=focus-event-for-parent&phx-blur=blur-event-for-parent&mode=tags") + + %{live: live} + end + + test "focusing on the input field sends a focus event to the parent", %{live: live} do + element(live, selectors()[:text_input]) + |> render_focus() + + assert_push_event(live, "parent_event", %{ + id: "my_form_city_search_live_select_component", + event: "focus-event-for-parent", + payload: %{id: "my_form_city_search_live_select_component"} + }) + end + + test "blurring the input field sends a blur event to the parent", %{live: live} do + element(live, selectors()[:text_input]) + |> render_blur() + + assert_push_event(live, "select", %{ + id: "my_form_city_search_live_select_component", + parent_event: "blur-event-for-parent" + }) + end + + test "selecting option with enter doesn't send blur event to parent", %{conn: conn} do + stub_options([{"A", 1}, {"B", 2}, {"C", 3}]) + + {:ok, live, _html} = live(conn, "/?phx-blur=blur-event-for-parent&mode=tags") + + type(live, "ABC") + + assert_options(live, ["A", "B", "C"]) + + select_nth_option(live, 2, method: :key) + + refute_push_event(live, "select", %{ + id: "my_form_city_search_live_select_component", + parent_event: "blur-event-for-parent" + }) + end + + test "selecting option with click doesn't send blur event to parent", %{conn: conn} do + stub_options([{"A", 1}, {"B", 2}, {"C", 3}]) + + {:ok, live, _html} = live(conn, "/?phx-blur=blur-event-for-parent&mode=tags") + + type(live, "ABC") + + assert_options(live, ["A", "B", "C"]) + + select_nth_option(live, 2, method: :click) + + refute_push_event(live, "select", %{ + id: "my_form_city_search_live_select_component", + parent_event: "blur-event-for-parent" + }) + end + end + + test "selection can be updated from the form", %{conn: conn} do + stub_options(%{ + "A" => 1, + "B" => 2, + "C" => 3 + }) + + {:ok, live, _html} = live(conn, "/?mode=tags") + + type(live, "ABC") + + select_nth_option(live, 1) + + type(live, "ABC") + + select_nth_option(live, 2, method: :click) + + stub_options(%{"D" => 4, "E" => 5}) + + type(live, "DEE") + + select_nth_option(live, 1) + + render_change(live, "change", %{"my_form" => %{"city_search" => [1, 2, 4]}}) + + assert_selected_multiple(live, [ + %{value: 1, label: "A"}, + %{value: 2, label: "B"}, + %{value: 4, label: "D"} + ]) + end + + test "selection recovery", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + values = [ + value1 = %{"name" => "A", "pos" => [10.0, 20.0]}, + value2 = %{"name" => "B", "pos" => [30.0, 40.0]}, + value3 = %{"name" => "C", "pos" => [50.0, 60.0]} + ] + + render_change(live, "change", %{"my_form" => %{"city_search" => Jason.encode!(values)}}) + + render_hook(element(live, selectors()[:container]), "selection_recovery", [ + %{label: "A", value: value1}, + %{label: "B", value: value2}, + %{label: "C", value: value3} + ]) + + assert_selected_multiple_static(live, [ + %{label: "A", value: value1}, + %{label: "B", value: value2}, + %{label: "C", value: value3} + ]) + end +end