Allow arbitrary suggestion modules in Live ComboBox component (#3154)

* Move ComboBox under Live.Components namespace

* Make suggestions module injectable through component API

* Reorganize tests

* Test ComboBox in isolation

* Allow external suggestion limit option

* Funnels editor: bugfix propagating suggestions over limit

* Update docs & typespecs
This commit is contained in:
hq1 2023-07-19 10:23:14 +02:00 committed by GitHub
parent 34fbc3d5bc
commit bf84c043ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 438 additions and 305 deletions

View File

@ -1,4 +1,4 @@
defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
defmodule PlausibleWeb.Live.Components.ComboBox do
@moduledoc """
Phoenix LiveComponent for a combobox UI element with search and selection
functionality.
@ -13,18 +13,28 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
by default but can be customized. When a user types into the input
field, the component searches the available options and provides
suggestions based on the input.
Any module exposing suggest/2 function can be supplied via `suggest_mod`
attribute - see the provided `ComboBox.StaticSearch`.
"""
use Phoenix.LiveComponent
alias Phoenix.LiveView.JS
@max_options_displayed 15
@default_suggestions_limit 15
def update(assigns, socket) do
assigns =
if assigns[:suggestions] do
Map.put(assigns, :suggestions, Enum.take(assigns.suggestions, suggestions_limit(assigns)))
else
assigns
end
socket =
socket
|> assign(assigns)
|> assign_new(:suggestions, fn ->
Enum.take(assigns.options, @max_options_displayed)
Enum.take(assigns.options, suggestions_limit(assigns))
end)
{:ok, socket}
@ -36,6 +46,8 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
attr(:submit_name, :string, required: true)
attr(:display_value, :string, default: "")
attr(:submit_value, :string, default: "")
attr(:suggest_mod, :atom, required: true)
attr(:suggestions_limit, :integer)
def render(assigns) do
~H"""
@ -79,7 +91,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
</div>
</div>
<.dropdown ref={@id} options={@options} suggestions={@suggestions} target={@myself} />
<.dropdown ref={@id} suggest_mod={@suggest_mod} suggestions={@suggestions} target={@myself} />
</div>
"""
end
@ -108,8 +120,8 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
end
attr(:ref, :string, required: true)
attr(:options, :list, default: [])
attr(:suggestions, :list, default: [])
attr(:suggest_mod, :atom, required: true)
attr(:target, :any)
def dropdown(assigns) do
@ -154,7 +166,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
attr(:idx, :integer, required: true)
def option(assigns) do
assigns = assign(assigns, :max_options_displayed, @max_options_displayed)
assigns = assign(assigns, :suggestions_limit, suggestions_limit(assigns))
~H"""
<li
@ -175,7 +187,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
</span>
</a>
</li>
<li :if={@idx == @max_options_displayed - 1} class="text-xs text-gray-500 relative py-2 px-3">
<li :if={@idx == @suggestions_limit - 1} class="text-xs text-gray-500 relative py-2 px-3">
Max results reached. Refine your search by typing in goal name.
</li>
"""
@ -197,42 +209,26 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
{:noreply, socket}
end
def handle_event("search", %{"_target" => [target]} = params, socket) do
def handle_event(
"search",
%{"_target" => [target]} = params,
%{assigns: %{suggest_mod: suggest_mod, options: options}} = socket
) do
input = params[target]
input_len = input |> String.trim() |> String.length()
if input_len > 0 do
suggestions = suggest(input, socket.assigns.options)
suggestions =
input
|> suggest_mod.suggest(options)
|> Enum.take(suggestions_limit(socket.assigns))
{:noreply, assign(socket, %{suggestions: suggestions})}
else
{:noreply, socket}
end
end
def suggest(input, options) do
input_len = String.length(input)
options
|> Enum.reject(fn {_, value} ->
input_len > String.length(to_string(value))
end)
|> Enum.sort_by(
fn {_, value} ->
if to_string(value) == input do
3
else
value = to_string(value)
input = String.downcase(input)
value = String.downcase(value)
weight = if String.contains?(value, input), do: 1, else: 0
weight + String.jaro_distance(value, input)
end
end,
:desc
)
|> Enum.take(@max_options_displayed)
end
defp do_select(socket, submit_value, display_value) do
id = socket.assigns.id
@ -254,4 +250,8 @@ defmodule PlausibleWeb.Live.FunnelSettings.ComboBox do
socket
end
defp suggestions_limit(assigns) do
Map.get(assigns, :suggestions_limit, @default_suggestions_limit)
end
end

View File

@ -0,0 +1,35 @@
defmodule PlausibleWeb.Live.Components.ComboBox.StaticSearch do
@moduledoc """
Default suggestion engine for the `ComboBox` component.
Assumes, the user have already queried the database and the data set is
small enough to be kept in state and filtered based on external input.
Favours exact matches. Skips entries shorter than input.
Allows fuzzy matching based on Jaro Distance.
"""
@spec suggest(String.t(), [{any(), any()}]) :: [{any(), any()}]
def suggest(input, options) do
input_len = String.length(input)
options
|> Enum.reject(fn {_, value} ->
input_len > String.length(to_string(value))
end)
|> Enum.sort_by(
fn {_, value} ->
if to_string(value) == input do
3
else
value = to_string(value)
input = String.downcase(input)
value = String.downcase(value)
weight = if String.contains?(value, input), do: 1, else: 0
weight + String.jaro_distance(value, input)
end
end,
:desc
)
end
end

View File

@ -68,7 +68,8 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
<div class="w-2/5 flex-1">
<.live_component
submit_name="funnel[steps][][goal_id]"
module={PlausibleWeb.Live.FunnelSettings.ComboBox}
module={PlausibleWeb.Live.Components.ComboBox}
suggest_mod={PlausibleWeb.Live.Components.ComboBox.StaticSearch}
id={"step-#{step_idx}"}
options={reject_alrady_selected("step-#{step_idx}", @goals, @selections_made)}
/>
@ -401,7 +402,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
result = Enum.reject(goals, fn {goal_id, _} -> goal_id in selection_ids end)
send_update(PlausibleWeb.Live.FunnelSettings.ComboBox, id: combo_box, suggestions: result)
send_update(PlausibleWeb.Live.Components.ComboBox, id: combo_box, suggestions: result)
result
end
end

View File

@ -0,0 +1,38 @@
defmodule PlausibleWeb.Live.Components.ComboBox.StaticSearchTest do
use ExUnit.Case, async: true
alias PlausibleWeb.Live.Components.ComboBox.StaticSearch
describe "autosuggest algorithm" do
test "favours exact match" do
options = fake_options(["yellow", "hello", "cruel hello world"])
assert [{_, "hello"}, {_, "cruel hello world"}, {_, "yellow"}] =
StaticSearch.suggest("hello", options)
end
test "skips entries shorter than input" do
options = fake_options(["yellow", "hello", "cruel hello world"])
assert [{_, "cruel hello world"}] = StaticSearch.suggest("cruel hello", options)
end
test "favours similiarity" do
options = fake_options(["melon", "hello", "yellow"])
assert [{_, "hello"}, {_, "yellow"}, {_, "melon"}] = StaticSearch.suggest("hell", options)
end
test "allows fuzzy matching" do
options = fake_options(["/url/0xC0FFEE", "/url/0xDEADBEEF", "/url/other"])
assert [{_, "/url/0xC0FFEE"}, {_, "/url/0xDEADBEEF"}, {_, "/url/other"}] =
StaticSearch.suggest("0x FF", options)
end
end
defp fake_options(option_names) do
option_names
|> Enum.shuffle()
|> Enum.with_index(fn element, index -> {index, element} end)
end
end

View File

@ -0,0 +1,199 @@
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(0)"
assert text_of_attr(li2, "@mouseenter") == "setFocus(1)"
assert text_of_attr(li1, "x-bind:class") =~ "focus === 0"
assert text_of_attr(li2, "x-bind:class") =~ "focus === 1"
end
test "Alpine.js: component refers to window.suggestionsDropdown" do
assert new_options(2)
|> render_sample_component()
|> find("div#input-picker-main-test-component")
|> text_of_attr("x-data") =~ "window.suggestionsDropdown('test-component')"
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") == "focusPrev"
assert text_of_attr(main, "x-on:keydown.arrow-down") == "focusNext"
assert text_of_attr(main, "x-on:keydown.enter") == "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
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_mod={__MODULE__.SampleSuggest}
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-0") == "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-0 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
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_mod: ComboBox.StaticSearch
],
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 - 1}/
end
defp type_into_combo(lv, id, text) do
lv
|> element("input##{id}")
|> render_change(%{
"_target" => ["display-#{id}"],
"display-#{id}" => "#{text}"
})
end
end

View File

@ -0,0 +1,130 @@
defmodule PlausibleWeb.Live.FunnelSettings.FormTest do
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
describe "integration - live rendering" do
setup [:create_user, :log_in, :create_site]
test "search reacts to the input, the user types in", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
doc = type_into_combo(lv, 1, "hello")
assert text_of_element(doc, "#dropdown-step-1-option-0") == "Hello World"
doc = type_into_combo(lv, 1, "plausible")
assert text_of_element(doc, "#dropdown-step-1-option-0") == "Plausible"
end
test "selecting an option prefills input values", %{conn: conn, site: site} do
{:ok, [_, _, g3]} = setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
doc = type_into_combo(lv, 1, "another")
refute element_exists?(doc, ~s/input[type="hidden"][value="#{g3.id}"]/)
refute element_exists?(doc, ~s/input[type="text"][value="Another World"]/)
lv
|> element("li#dropdown-step-1-option-0 a")
|> render_click()
assert lv
|> element("#submit-step-1")
|> render()
|> element_exists?(~s/input[type="hidden"][value="#{g3.id}"]/)
assert lv
|> element("#step-1")
|> render()
|> element_exists?(~s/input[type="text"][value="Another World"]/)
end
test "selecting one option reduces suggestions in the other", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
type_into_combo(lv, 1, "another")
lv
|> element("li#dropdown-step-1-option-0 a")
|> render_click()
doc = type_into_combo(lv, 2, "another")
refute text_of_element(doc, "ul#dropdown-step-1 li") =~ "Another World"
assert text_of_element(doc, "ul#dropdown-step-2 li") =~ "Hello World"
assert text_of_element(doc, "ul#dropdown-step-2 li") =~ "Plausible"
refute text_of_element(doc, "ul#dropdown-step-2 li") =~ "Another World"
end
test "suggestions are limited on change", %{conn: conn, site: site} do
setup_goals(site, for(i <- 1..20, do: "Goal #{i}"))
lv = get_liveview(conn, site)
doc =
lv
|> element("li#dropdown-step-1-option-0 a")
|> render_click()
assert element_exists?(doc, ~s/#li#dropdown-step-1-option-14/)
refute element_exists?(doc, ~s/#li#dropdown-step-1-option-15/)
end
test "removing one option alters suggestions for other", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
lv |> element(~s/a[phx-click="add-step"]/) |> render_click()
type_into_combo(lv, 2, "hello")
lv
|> element("li#dropdown-step-2-option-0 a")
|> render_click()
doc = type_into_combo(lv, 1, "hello")
refute text_of_element(doc, "ul#dropdown-step-1 li") =~ "Hello World"
lv |> element(~s/#remove-step-2/) |> render_click()
doc = type_into_combo(lv, 1, "hello")
assert text_of_element(doc, "ul#dropdown-step-1 li") =~ "Hello World"
end
end
defp get_liveview(conn, site) do
conn = assign(conn, :live_module, PlausibleWeb.Live.FunnelSettings)
{:ok, lv, _html} = live(conn, "/#{site.domain}/settings/funnels")
lv |> element(~s/button[phx-click="add-funnel"]/) |> render_click()
assert form_view = find_live_child(lv, "funnels-form")
form_view |> element("form") |> render_change(%{funnel: %{name: "My test funnel"}})
form_view
end
defp setup_goals(site, goal_names) when is_list(goal_names) do
goals =
Enum.map(goal_names, fn goal_name ->
{:ok, g} = Plausible.Goals.create(site, %{"event_name" => goal_name})
g
end)
{:ok, goals}
end
defp type_into_combo(lv, idx, text) do
lv
|> element("input#step-#{idx}")
|> render_change(%{
"_target" => ["display-step-#{idx}"],
"display-step-#{idx}" => "#{text}"
})
end
end

View File

@ -1,270 +0,0 @@
defmodule PlausibleWeb.Live.FunnelSettings.ComboBoxTest do
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
alias PlausibleWeb.Live.FunnelSettings.ComboBox
@ul "ul#dropdown-test-component[x-show=isOpen][x-ref=suggestions]"
defp suggestion_li(idx) do
~s/#{@ul} li#dropdown-test-component-option-#{idx - 1}/
end
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 "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(0)"
assert text_of_attr(li2, "@mouseenter") == "setFocus(1)"
assert text_of_attr(li1, "x-bind:class") =~ "focus === 0"
assert text_of_attr(li2, "x-bind:class") =~ "focus === 1"
end
test "Alpine.js: component refers to window.suggestionsDropdown" do
assert new_options(2)
|> render_sample_component()
|> find("div#input-picker-main-test-component")
|> text_of_attr("x-data") =~ "window.suggestionsDropdown('test-component')"
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") == "focusPrev"
assert text_of_attr(main, "x-on:keydown.arrow-down") == "focusNext"
assert text_of_attr(main, "x-on:keydown.enter") == "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
end
describe "autosuggest algorithm" do
test "favours exact match" do
options = fake_options(["yellow", "hello", "cruel hello world"])
assert [{_, "hello"}, {_, "cruel hello world"}, {_, "yellow"}] =
ComboBox.suggest("hello", options)
end
test "skips entries shorter than input" do
options = fake_options(["yellow", "hello", "cruel hello world"])
assert [{_, "cruel hello world"}] = ComboBox.suggest("cruel hello", options)
end
test "favours similiarity" do
options = fake_options(["melon", "hello", "yellow"])
assert [{_, "hello"}, {_, "yellow"}, {_, "melon"}] = ComboBox.suggest("hell", options)
end
test "allows fuzzy matching" do
options = fake_options(["/url/0xC0FFEE", "/url/0xDEADBEEF", "/url/other"])
assert [{_, "/url/0xC0FFEE"}, {_, "/url/0xDEADBEEF"}, {_, "/url/other"}] =
ComboBox.suggest("0x FF", options)
end
test "suggests up to 15 entries" do
options =
1..20
|> Enum.map(&"Option #{&1}")
|> fake_options()
suggestions = ComboBox.suggest("Option", options)
assert Enum.count(suggestions) == 15
end
end
describe "integration - live rendering" do
setup [:create_user, :log_in, :create_site]
test "search reacts to the input, the user types in", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
doc = type_into_combo(lv, 1, "hello")
assert text_of_element(doc, "#dropdown-step-1-option-0") == "Hello World"
doc = type_into_combo(lv, 1, "plausible")
assert text_of_element(doc, "#dropdown-step-1-option-0") == "Plausible"
end
test "selecting an option prefills input values", %{conn: conn, site: site} do
{:ok, [_, _, g3]} = setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
doc = type_into_combo(lv, 1, "another")
refute element_exists?(doc, ~s/input[type="hidden"][value="#{g3.id}"]/)
refute element_exists?(doc, ~s/input[type="text"][value="Another World"]/)
lv
|> element("li#dropdown-step-1-option-0 a")
|> render_click()
assert lv
|> element("#submit-step-1")
|> render()
|> element_exists?(~s/input[type="hidden"][value="#{g3.id}"]/)
assert lv
|> element("#step-1")
|> render()
|> element_exists?(~s/input[type="text"][value="Another World"]/)
end
test "selecting one option reduces suggestions in the other", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
type_into_combo(lv, 1, "another")
lv
|> element("li#dropdown-step-1-option-0 a")
|> render_click()
doc = type_into_combo(lv, 2, "another")
refute text_of_element(doc, "ul#dropdown-step-1 li") =~ "Another World"
assert text_of_element(doc, "ul#dropdown-step-2 li") =~ "Hello World"
assert text_of_element(doc, "ul#dropdown-step-2 li") =~ "Plausible"
refute text_of_element(doc, "ul#dropdown-step-2 li") =~ "Another World"
end
test "removing one option alters suggestions for other", %{conn: conn, site: site} do
setup_goals(site, ["Hello World", "Plausible", "Another World"])
lv = get_liveview(conn, site)
lv |> element(~s/a[phx-click="add-step"]/) |> render_click()
type_into_combo(lv, 2, "hello")
lv
|> element("li#dropdown-step-2-option-0 a")
|> render_click()
doc = type_into_combo(lv, 1, "hello")
refute text_of_element(doc, "ul#dropdown-step-1 li") =~ "Hello World"
lv |> element(~s/#remove-step-2/) |> render_click()
doc = type_into_combo(lv, 1, "hello")
assert text_of_element(doc, "ul#dropdown-step-1 li") =~ "Hello World"
end
end
defp render_sample_component(options) do
render_component(ComboBox,
options: options,
submit_name: "test-submit-name",
id: "test-component"
)
end
defp new_options(n) do
Enum.map(1..n, &{&1, "TestOption #{&1}"})
end
defp get_liveview(conn, site) do
conn = assign(conn, :live_module, PlausibleWeb.Live.FunnelSettings)
{:ok, lv, _html} = live(conn, "/#{site.domain}/settings/funnels")
lv |> element(~s/button[phx-click="add-funnel"]/) |> render_click()
assert form_view = find_live_child(lv, "funnels-form")
form_view |> element("form") |> render_change(%{funnel: %{name: "My test funnel"}})
form_view
end
defp setup_goals(site, goal_names) when is_list(goal_names) do
goals =
Enum.map(goal_names, fn goal_name ->
{:ok, g} = Plausible.Goals.create(site, %{"event_name" => goal_name})
g
end)
{:ok, goals}
end
defp fake_options(option_names) do
option_names
|> Enum.shuffle()
|> Enum.with_index(fn element, index -> {index, element} end)
end
defp type_into_combo(lv, idx, text) do
lv
|> element("input#step-#{idx}")
|> render_change(%{
"_target" => ["display-step-#{idx}"],
"display-step-#{idx}" => "#{text}"
})
end
end