Skip to content

Commit dd79690

Browse files
authored
Fix uncheck for checkboxes with array name (#306)
## Problem I've seeing a bug in which uncheck doesn't work as expected with checkboxes with array names like: <input type="checkbox" name="items[]" value="one" /> ## Issue The issue was that `FormData.merge`combined the previous value with the newly computed value for array-named checkboxes. In the uncheck case, that meant unchecked values could remain present because they were still in the old state. ## Fix This commit introduces `FormData.override`, which replaces the old value with a newly computed one + logic to calculate this new state for checkboxes with array-like names. It also adds tests reproducing the issue.
1 parent 99ff643 commit dd79690

11 files changed

Lines changed: 215 additions & 26 deletions

File tree

lib/phoenix_test/active_form.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ defmodule PhoenixTest.ActiveForm do
1515
struct!(%__MODULE__{}, opts)
1616
end
1717

18-
def add_form_data(%__MODULE__{} = active_form, new_form_data) do
19-
Map.update!(active_form, :form_data, &FormData.add_data(&1, new_form_data))
18+
def put_form_data(%__MODULE__{} = active_form, name, value) do
19+
Map.update!(active_form, :form_data, &FormData.put_data(&1, name, value))
2020
end
2121

2222
def add_upload(%__MODULE__{} = active_form, new_upload) do

lib/phoenix_test/field_helpers.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule PhoenixTest.FieldHelpers do
2+
@moduledoc false
3+
4+
alias PhoenixTest.ActiveForm
5+
alias PhoenixTest.FormData
6+
alias PhoenixTest.Html
7+
8+
# Computes the value to store in active_form for a field interaction.
9+
# For array-named fields (name ending in "[]"), values are accumulated/removed
10+
# from the existing list rather than replaced wholesale. This mirrors browser
11+
# behavior where each checkbox toggle adds/removes its value from the array.
12+
def next_field_value(session, form, field) do
13+
current_form_data = current_form_data(session, form)
14+
15+
case {session.current_operation.name, field} do
16+
{:check, %{name: name, value: value}} ->
17+
if multiple_values_name?(name) do
18+
current_form_data
19+
|> FormData.get_data(name)
20+
|> List.wrap()
21+
|> Kernel.++([value])
22+
|> Enum.uniq()
23+
else
24+
value
25+
end
26+
27+
{:uncheck, %{name: name, parsed: parsed} = current_field} ->
28+
if multiple_values_name?(name) do
29+
checked_value = Html.attribute(parsed, "value") || "on"
30+
31+
current_form_data
32+
|> FormData.get_data(name)
33+
|> List.wrap()
34+
|> Enum.reject(&(&1 == checked_value))
35+
else
36+
current_field.value
37+
end
38+
39+
{_, %{name: name, value: values}} ->
40+
if multiple_values_name?(name) and is_list(values) do
41+
current_form_data
42+
|> FormData.get_data(name)
43+
|> List.wrap()
44+
|> Kernel.++(values)
45+
|> Enum.uniq()
46+
else
47+
values
48+
end
49+
50+
_ ->
51+
field.value
52+
end
53+
end
54+
55+
def current_form_data(session, form) do
56+
if session.active_form.selector == form.selector do
57+
FormData.override(form.form_data, session.active_form.form_data)
58+
else
59+
form.form_data
60+
end
61+
end
62+
63+
def active_form_for(active_form, form) do
64+
if active_form.selector == form.selector do
65+
active_form
66+
else
67+
ActiveForm.new(id: form.id, selector: form.selector)
68+
end
69+
end
70+
71+
def multiple_values_name?(name) when is_binary(name) do
72+
String.ends_with?(name, "[]")
73+
end
74+
75+
def multiple_values_name?(_name), do: false
76+
end

lib/phoenix_test/form_data.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ defmodule PhoenixTest.FormData do
6363
%__MODULE__{data: data}
6464
end
6565

66+
def override(%__MODULE__{data: data1}, %__MODULE__{data: data2}) do
67+
%__MODULE__{data: Map.merge(data1, data2)}
68+
end
69+
70+
def get_data(%__MODULE__{data: data}, name) do
71+
Map.get(data, name)
72+
end
73+
74+
def put_data(%__MODULE__{} = form_data, name, value) when is_nil(name) or is_nil(value), do: form_data
75+
76+
def put_data(%__MODULE__{} = form_data, name, value) do
77+
%__MODULE__{form_data | data: Map.put(form_data.data, name, value)}
78+
end
79+
6680
defp allows_multiple_values?(field_name), do: String.ends_with?(field_name, "[]")
6781

6882
def filter(%__MODULE__{data: data}, fun) do

lib/phoenix_test/live.ex

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule PhoenixTest.Live do
22
@moduledoc false
33
import Phoenix.LiveViewTest
4+
import PhoenixTest.FieldHelpers
45
import PhoenixTest.SessionHelpers, only: [scope_selector: 2]
56

67
alias PhoenixTest.ActiveForm
@@ -135,7 +136,7 @@ defmodule PhoenixTest.Live do
135136

136137
form_data =
137138
if active_form.selector == form.selector do
138-
FormData.merge(form.form_data, active_form.form_data)
139+
FormData.override(form.form_data, active_form.form_data)
139140
else
140141
form.form_data
141142
end
@@ -163,7 +164,7 @@ defmodule PhoenixTest.Live do
163164

164165
existing_form_data =
165166
if session.active_form.selector == form.selector do
166-
FormData.merge(form.form_data, session.active_form.form_data)
167+
FormData.override(form.form_data, session.active_form.form_data)
167168
else
168169
form.form_data
169170
end
@@ -438,16 +439,13 @@ defmodule PhoenixTest.Live do
438439
Field.validate_name!(field)
439440

440441
form = Field.parent_form!(field, html)
442+
field_value = next_field_value(session, form, field)
441443

442444
session =
443445
Map.update!(session, :active_form, fn active_form ->
444-
if active_form.selector == form.selector do
445-
ActiveForm.add_form_data(active_form, field)
446-
else
447-
[id: form.id, selector: form.selector]
448-
|> ActiveForm.new()
449-
|> ActiveForm.add_form_data(field)
450-
end
446+
active_form
447+
|> active_form_for(form)
448+
|> ActiveForm.put_form_data(field.name, field_value)
451449
end)
452450

453451
maybe_trigger_phx_change(session, form, field)
@@ -495,7 +493,7 @@ defmodule PhoenixTest.Live do
495493

496494
defp merged_form_data(session, %Form{} = form) do
497495
form.form_data
498-
|> FormData.merge(session.active_form.form_data)
496+
|> FormData.override(session.active_form.form_data)
499497
|> FormData.filter(fn %{name: name} -> name in Form.form_element_names(form) end)
500498
end
501499

@@ -521,7 +519,7 @@ defmodule PhoenixTest.Live do
521519
form = Form.find!(session.current_operation.html, selector)
522520

523521
form_data = remove_data_for_fields_that_have_been_removed(form_data, form)
524-
form_data = FormData.merge(form.form_data, form_data)
522+
form_data = FormData.override(form.form_data, form_data)
525523

526524
additional_data =
527525
if form.submit_button do
@@ -625,7 +623,9 @@ defmodule PhoenixTest.Live do
625623
{:found, form} ->
626624
active_form = session.active_form
627625
active_form? = form.selector == active_form.selector
628-
form_data = FormData.merge(form.form_data, if(active_form?, do: active_form.form_data, else: FormData.new()))
626+
627+
form_data =
628+
FormData.override(form.form_data, if(active_form?, do: active_form.form_data, else: FormData.new()))
629629

630630
%{session.conn | resp_body: html}
631631
|> PhoenixTest.Static.build()

lib/phoenix_test/static.ex

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule PhoenixTest.Static do
22
@moduledoc false
33

44
import Phoenix.ConnTest
5+
import PhoenixTest.FieldHelpers
56

67
alias ExUnit.AssertionError
78
alias PhoenixTest.ActiveForm
@@ -246,7 +247,7 @@ defmodule PhoenixTest.Static do
246247
Form.put_button_data(form, form.submit_button)
247248
end)
248249

249-
to_submit = FormPayload.new(FormData.merge(form.form_data, form_data))
250+
to_submit = FormPayload.new(FormData.override(form.form_data, form_data))
250251

251252
session
252253
|> Map.put(:active_form, ActiveForm.new())
@@ -278,15 +279,12 @@ defmodule PhoenixTest.Static do
278279
defp fill_in_field_data(session, field) do
279280
Field.validate_name!(field)
280281
form = Field.parent_form!(field, session.current_operation.html)
282+
field_value = next_field_value(session, form, field)
281283

282284
Map.update!(session, :active_form, fn active_form ->
283-
if active_form.selector == form.selector do
284-
ActiveForm.add_form_data(active_form, field)
285-
else
286-
[id: form.id, selector: form.selector]
287-
|> ActiveForm.new()
288-
|> ActiveForm.add_form_data(field)
289-
end
285+
active_form
286+
|> active_form_for(form)
287+
|> ActiveForm.put_form_data(field.name, field_value)
290288
end)
291289
end
292290

@@ -315,7 +313,7 @@ defmodule PhoenixTest.Static do
315313

316314
defp build_payload(form, active_form \\ ActiveForm.new()) do
317315
form.form_data
318-
|> FormData.merge(active_form.form_data)
316+
|> FormData.override(active_form.form_data)
319317
|> FormPayload.new()
320318
|> FormPayload.add_form_data(active_form.uploads)
321319
end

test/phoenix_test/active_form_test.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ defmodule PhoenixTest.ActiveFormTest do
44
alias PhoenixTest.ActiveForm
55
alias PhoenixTest.FormData
66

7-
describe "add_form_data" do
8-
test "adds form data passed" do
7+
describe "put_form_data" do
8+
test "puts form data with given name and value" do
99
active_form =
1010
[id: "user-form", selector: "#user-form"]
1111
|> ActiveForm.new()
12-
|> ActiveForm.add_form_data({"user[name]", "Frodo"})
12+
|> ActiveForm.put_form_data("user[name]", "Frodo")
1313

1414
assert FormData.has_data?(active_form.form_data, "user[name]", "Frodo")
1515
end

test/phoenix_test/form_data_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,22 @@ defmodule PhoenixTest.FormDataTest do
176176
end
177177
end
178178

179+
describe "override" do
180+
test "replaces list values when same key is present" do
181+
base =
182+
FormData.new()
183+
|> FormData.add_data("items[]", "")
184+
|> FormData.add_data("items[]", "one")
185+
186+
override =
187+
FormData.put_data(FormData.new(), "items[]", [""])
188+
189+
form_data = FormData.override(base, override)
190+
191+
assert FormData.to_list(form_data) == [{"items[]", ""}]
192+
end
193+
end
194+
179195
describe "filter" do
180196
test "filters form data based on function provivded" do
181197
form_data =

test/phoenix_test/live_test.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,17 @@ defmodule PhoenixTest.LiveTest do
663663
|> assert_has("#form-data", text: "checkbox_group: [1, 2]")
664664
end
665665

666+
test "adds checked values for array named checkboxes without replacing existing values", %{conn: conn} do
667+
conn
668+
|> visit("/live/index")
669+
|> within("#array-checkbox-form", fn session ->
670+
check(session, "Three")
671+
end)
672+
|> assert_has("#form-data", text: "one")
673+
|> assert_has("#form-data", text: "two")
674+
|> assert_has("#form-data", text: "three")
675+
end
676+
666677
test "check triggers phx-change on the input if it is defined", %{conn: conn} do
667678
conn
668679
|> visit("/live/index")
@@ -784,6 +795,28 @@ defmodule PhoenixTest.LiveTest do
784795
|> assert_has("#form-data", text: "like-elixir: no")
785796
end
786797

798+
test "removes checked values from array named checkboxes on change", %{conn: conn} do
799+
conn
800+
|> visit("/live/index")
801+
|> within("#array-checkbox-form", fn session ->
802+
uncheck(session, "One")
803+
end)
804+
|> refute_has("#form-data", text: "one")
805+
|> assert_has("#form-data", text: "two")
806+
end
807+
808+
test "removes checked values from array named checkboxes on submit", %{conn: conn} do
809+
conn
810+
|> visit("/live/index")
811+
|> within("#array-checkbox-form", fn session ->
812+
session
813+
|> uncheck("One")
814+
|> submit()
815+
end)
816+
|> refute_has("#form-data", text: "one")
817+
|> assert_has("#form-data", text: "two")
818+
end
819+
787820
test "raises error if checkbox doesn't have phx-click or belong to form", %{conn: conn} do
788821
session = visit(conn, "/live/index")
789822

test/phoenix_test/static_test.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,19 @@ defmodule PhoenixTest.StaticTest do
545545
|> assert_has("#form-data", text: "admin: on")
546546
end
547547

548+
test "adds checked values for array named checkboxes without replacing existing values", %{conn: conn} do
549+
conn
550+
|> visit("/page/index")
551+
|> within("#array-checkbox-form", fn session ->
552+
check(session, "Three")
553+
end)
554+
|> submit()
555+
|> assert_has("#form-data", text: "items: [")
556+
|> assert_has("#form-data", text: "one")
557+
|> assert_has("#form-data", text: "two")
558+
|> assert_has("#form-data", text: "three")
559+
end
560+
548561
test "handle checkbox name with '?'", %{conn: conn} do
549562
conn
550563
|> visit("/page/index")
@@ -616,6 +629,17 @@ defmodule PhoenixTest.StaticTest do
616629
|> refute_has("#form-data", text: "like-elixir: yes")
617630
|> assert_has("#form-data", text: "like-elixir: no")
618631
end
632+
633+
test "removes checked values from array named checkboxes", %{conn: conn} do
634+
conn
635+
|> visit("/page/index")
636+
|> within("#array-checkbox-form", fn session ->
637+
uncheck(session, "One")
638+
end)
639+
|> submit()
640+
|> refute_has("#form-data", text: "one")
641+
|> assert_has("#form-data", text: "two")
642+
end
619643
end
620644

621645
describe "choose/3" do

test/support/web_app/index_live.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,21 @@ defmodule PhoenixTest.WebApp.IndexLive do
370370
<button type="submit">Save</button>
371371
</form>
372372
373+
<form id="array-checkbox-form" phx-change="change-form" phx-submit="change-form">
374+
<input type="hidden" name="items[]" value="" />
375+
376+
<label for="array-item-one">One</label>
377+
<input id="array-item-one" type="checkbox" name="items[]" value="one" checked />
378+
379+
<label for="array-item-two">Two</label>
380+
<input id="array-item-two" type="checkbox" name="items[]" value="two" checked />
381+
382+
<label for="array-item-three">Three</label>
383+
<input id="array-item-three" type="checkbox" name="items[]" value="three" />
384+
385+
<button type="submit">Save Array Checkbox Form</button>
386+
</form>
387+
373388
<form id="same-labels" phx-submit="save-form" phx-change="change-form">
374389
<fieldset name="like-elixir">
375390
<legend>Do you like Elixir:</legend>

0 commit comments

Comments
 (0)