forked from phoenixframework/phoenix_live_view
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds a playground for testing select behaviors. It can be opened by starting the e2e server (npm run e2e:server) and then opening http://localhost:4004/select.
- Loading branch information
Showing
21 changed files
with
250 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
defmodule Phoenix.LiveViewTest.E2E.SelectLive do | ||
use Phoenix.LiveView | ||
|
||
@impl Phoenix.LiveView | ||
def mount(_params, _session, socket) do | ||
{:ok, | ||
assign(socket, | ||
tick_timer: nil, | ||
select2_timer: nil, | ||
select2_countdown: 5, | ||
select4_timer: nil, | ||
select4_countdown: 5, | ||
tick: 0, | ||
form: to_form(%{"select3" => "2"}, as: :select_form), | ||
select1_opts: ["these options", "are fixed"], | ||
select2_opts: Enum.to_list(1..10), | ||
select3_opts: 1..10, | ||
select4_opts: 1..10, | ||
select4_value: "1" | ||
)} | ||
end | ||
|
||
@impl Phoenix.LiveView | ||
def handle_info(:tick, socket) do | ||
{:noreply, update(socket, :tick, &(&1 + 1))} | ||
end | ||
|
||
def handle_info(:update_select2_opts, socket) do | ||
{:noreply, | ||
update(socket, :select2_opts, fn existing_opts -> | ||
existing_opts ++ [Enum.max(existing_opts) + 1] | ||
end)} | ||
end | ||
|
||
def handle_info(:select2_countdown, socket) do | ||
if socket.assigns.select2_countdown == 0 do | ||
send(self(), :update_select2_opts) | ||
{:noreply, assign(socket, select2_timer: nil)} | ||
else | ||
Process.send_after(self(), :select2_countdown, 1000) | ||
{:noreply, update(socket, :select2_countdown, &(&1 - 1))} | ||
end | ||
end | ||
|
||
def handle_info(:select4_countdown, socket) do | ||
if socket.assigns.select4_countdown == 0 do | ||
send(self(), :change_select4_value) | ||
{:noreply, assign(socket, select4_timer: nil)} | ||
else | ||
Process.send_after(self(), :select4_countdown, 1000) | ||
{:noreply, update(socket, :select4_countdown, &(&1 - 1))} | ||
end | ||
end | ||
|
||
def handle_info(:change_select4_value, socket) do | ||
{:noreply, assign(socket, :select4_value, Enum.random(1..10) |> to_string())} | ||
end | ||
|
||
@types %{ | ||
select1: :string, | ||
select2: :integer, | ||
select3: :integer, | ||
select4: :integer | ||
} | ||
|
||
def changeset(params) do | ||
Ecto.Changeset.cast({%{}, @types}, params, [:select1, :select2, :select3, :select4]) | ||
|> Ecto.Changeset.validate_number(:select3, greater_than: 5) | ||
end | ||
|
||
@impl Phoenix.LiveView | ||
def handle_event("validate", %{"select_form" => params}, socket) do | ||
changeset = changeset(params) | ||
|
||
{:noreply, assign(socket, form: to_form(changeset, as: :select_form, action: :validate))} | ||
end | ||
|
||
def handle_event("toggle-tick", _, socket) do | ||
case socket.assigns.tick_timer do | ||
nil -> | ||
timer_ref = :timer.send_interval(1000, :tick) | ||
{:noreply, assign(socket, :tick_timer, timer_ref)} | ||
|
||
ref -> | ||
Process.cancel_timer(ref) | ||
{:noreply, assign(socket, :tick_timer, nil)} | ||
end | ||
end | ||
|
||
def handle_event("schedule-select2-update", _, socket) do | ||
timer_ref = Process.send_after(self(), :select2_countdown, 1000) | ||
{:noreply, assign(socket, select2_countdown: 5, select2_timer: timer_ref)} | ||
end | ||
|
||
def handle_event("schedule-select4-update", _, socket) do | ||
timer_ref = Process.send_after(self(), :select4_countdown, 1000) | ||
{:noreply, assign(socket, select4_countdown: 5, select4_timer: timer_ref)} | ||
end | ||
|
||
@impl Phoenix.LiveView | ||
def render(assigns) do | ||
~H""" | ||
<style> | ||
* { font-size: unset; } | ||
body { | ||
padding: 20px; | ||
max-width: 500px; | ||
font-family: sans-serif; | ||
} | ||
.has-error { | ||
border: 5px solid red; | ||
} | ||
select { | ||
border: 1px solid black; | ||
} | ||
</style> | ||
<h1>Select Playgroud</h1> | ||
<p> | ||
This page contains multiple select inputs to test various behaviors. | ||
Sadly, we cannot test all of them automatically, as there is no way to assert the state of an open select's native UI. | ||
</p> | ||
Tick: <%= @tick %> | ||
<div style="display: flex; flex-direction: column; gap: 8px"> | ||
<button phx-click="toggle-tick"><%= if @tick_timer, do: "Disable", else: "Enable" %> ticking</button> | ||
<button :if={!@select2_timer} phx-click="schedule-select2-update">Schedule select2 update</button> | ||
<span :if={@select2_timer}>Select 2 will update in <%= @select2_countdown %>s</span> | ||
<button :if={!@select4_timer} phx-click="schedule-select4-update">Schedule select4 update</button> | ||
<span :if={@select4_timer}>Select 4 will update in <%= @select4_countdown %>s</span> | ||
</div> | ||
<.form for={@form} phx-change="validate"> | ||
<h2>Select 1</h2> | ||
<p> | ||
The select should not close when the page is patched while it is open. | ||
You can simulate patching by enabling ticking above. | ||
</p> | ||
<.input type="select" field={@form[:select1]} label="Select 1" options={@select1_opts} /> | ||
<hr/> | ||
<h2>Select 2</h2> | ||
<p> | ||
The second select's options will be updated after a 5s timeout (button on top). | ||
This can be used to test the behavior of the select when its options change while it is open. | ||
</p> | ||
<.input type="select" field={@form[:select2]} label="Select 2" options={@select2_opts} /> | ||
<hr/> | ||
<h2>Select 3</h2> | ||
<p> | ||
Error classes are correctly applied to the third select. | ||
It should have a red border for all values from 1 to 5. The border should disappear when selecting 6 or higher. | ||
</p> | ||
<.input type="select" field={@form[:select3]} label="Select 3" options={@select3_opts} /> | ||
<hr/> | ||
<h2>Select 4</h2> | ||
<p> | ||
The selected value of this field changes after a 5s timeout (button on top). | ||
This can be used to test the behavior of the select when its value changes while it is open. | ||
We expect the value to be ignored if the select is open, as value changes to focused inputs are ignored. | ||
</p> | ||
<.input type="select" field={@form[:select4]} value={@select4_value} label="Select 4" options={@select4_opts} /> | ||
<hr/> | ||
</.form> | ||
""" | ||
end | ||
|
||
### | ||
# Input components copied and adjusted from generated core_components | ||
|
||
attr :id, :any, default: nil | ||
attr :name, :any | ||
attr :label, :string, default: nil | ||
attr :value, :any | ||
|
||
attr :type, :string, | ||
default: "text", | ||
values: ~w(checkbox color date datetime-local email file month number password | ||
range search select tel text textarea time url week) | ||
|
||
attr :field, Phoenix.HTML.FormField | ||
|
||
attr :errors, :list, default: [] | ||
attr :checked, :boolean | ||
attr :prompt, :string, default: nil | ||
attr :options, :list | ||
attr :multiple, :boolean, default: false | ||
|
||
attr :rest, :global, | ||
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength | ||
multiple pattern placeholder readonly required rows size step) | ||
|
||
slot(:inner_block) | ||
|
||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do | ||
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] | ||
|
||
assigns | ||
|> assign(field: nil, id: assigns.id || field.id) | ||
|> assign(:errors, errors) | ||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) | ||
|> assign_new(:value, fn -> field.value end) | ||
|> input() | ||
end | ||
|
||
def input(%{type: "select"} = assigns) do | ||
~H""" | ||
<div> | ||
<select | ||
id={@id} | ||
name={@name} | ||
class={if @errors != [], do: "has-error"} | ||
multiple={@multiple} | ||
{@rest} | ||
> | ||
<option :if={@prompt} value=""><%= @prompt %></option> | ||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %> | ||
</select> | ||
</div> | ||
""" | ||
end | ||
end |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
const { test, expect } = require("../test-fixtures"); | ||
const { syncLV } = require("../utils"); | ||
|
||
// this tests issue #2659 | ||
// https://github.com/phoenixframework/phoenix_live_view/pull/2659 | ||
test("select shows error when invalid option is selected", async ({ page }) => { | ||
await page.goto("/select"); | ||
await syncLV(page); | ||
|
||
const select3 = page.locator("#select_form_select3"); | ||
await expect(select3).toHaveValue("2"); | ||
await expect(select3).not.toHaveClass("has-error"); | ||
|
||
// 5 or below should be invalid | ||
await select3.selectOption("3"); | ||
await syncLV(page); | ||
await expect(select3).toHaveClass("has-error"); | ||
|
||
// 6 or above should be valid | ||
await select3.selectOption("6"); | ||
await syncLV(page); | ||
await expect(select3).not.toHaveClass("has-error"); | ||
}); |