Skip to content

Commit 65b95d0

Browse files
committed
add e2e test for keyed comprehension rendering
1 parent cbf7dc2 commit 65b95d0

File tree

4 files changed

+304
-4
lines changed

4 files changed

+304
-4
lines changed

lib/phoenix_live_view/diff.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ defmodule Phoenix.LiveView.Diff do
6363
{[], components}
6464
else
6565
Enum.map_reduce(0..(keyed[@keyed_count] - 1), components, fn index, components ->
66-
diff = Map.fetch!(keyed, index)
66+
diff = Map.fetch!(keyed, index)
6767

68-
to_iodata(Map.put(diff, @static, static), components, template, mapper)
68+
to_iodata(Map.put(diff, @static, static), components, template, mapper)
6969
end)
7070
end
7171
end
@@ -648,8 +648,7 @@ defmodule Phoenix.LiveView.Diff do
648648
{{diff, 0, new_prints, pending, components, template}, MapSet.new()},
649649
fn
650650
{key, vars, render},
651-
{{_diff, index, _new_prints, _pending, _components, _template} = acc,
652-
seen_keys} ->
651+
{{_diff, index, _new_prints, _pending, _components, _template} = acc, seen_keys} ->
653652
{key, seen_keys} =
654653
cond do
655654
not has_key? ->
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
defmodule Phoenix.LiveViewTest.E2E.KeyedComprehensionLive do
2+
use Phoenix.LiveView
3+
4+
@count 10
5+
6+
def render(assigns) do
7+
~H"""
8+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
9+
<div class="p-8">
10+
<div class="border-b border-gray-200 mb-6">
11+
<nav role="tablist" class="tabs tabs-border">
12+
<.link
13+
role="tab"
14+
class={"tab #{if @active_tab == "all_keyed", do: "tab-active"}"}
15+
patch="/keyed-comprehension?tab=all_keyed"
16+
>
17+
All keyed
18+
</.link>
19+
<.link
20+
role="tab"
21+
class={"tab #{if @active_tab == "rows_keyed", do: "tab-active"}"}
22+
patch="/keyed-comprehension?tab=rows_keyed"
23+
>
24+
Rows keyed
25+
</.link>
26+
<.link
27+
role="tab"
28+
class={"tab #{if @active_tab == "no_keyed", do: "tab-active"}"}
29+
patch="/keyed-comprehension?tab=no_keyed"
30+
>
31+
No keyed
32+
</.link>
33+
</nav>
34+
</div>
35+
36+
<button class="btn" phx-click="randomize">randomize</button>
37+
<button class="btn" phx-click="change_0">change first</button>
38+
<button class="btn" phx-click="change_other">change other</button>
39+
40+
<form>
41+
<input phx-change="change_size" name="size" value={@size} />
42+
</form>
43+
44+
<div :for={i <- 1..2} :key={i}>
45+
<.table_with_all_keyed
46+
:if={@active_tab == "all_keyed"}
47+
rows={@items}
48+
id={fn row -> row.id end}
49+
>
50+
<:col :let={%{entry: entry}} id="1" name="Foo">
51+
<.my_component my_count={@count} the_name={entry.foo.bar} /> {i}
52+
</:col>
53+
<:col id="2" name="Count">{@count}</:col>
54+
</.table_with_all_keyed>
55+
56+
<.table_with_rows_keyed
57+
:if={@active_tab == "rows_keyed"}
58+
rows={@items}
59+
id={fn row -> row.id end}
60+
>
61+
<:col :let={%{entry: entry}} id="1" name="Foo">
62+
<.my_component my_count={@count} the_name={entry.foo.bar} /> {i}
63+
</:col>
64+
<:col id="2" name="Count">{@count}</:col>
65+
</.table_with_rows_keyed>
66+
67+
<.table_with_no_keyed :if={@active_tab == "no_keyed"} rows={@items} id={fn row -> row.id end}>
68+
<:col :let={%{entry: entry}} id="1" name="Foo">
69+
<.my_component my_count={@count} the_name={entry.foo.bar} /> {i}
70+
</:col>
71+
<:col id="2" name="Count">{@count}</:col>
72+
</.table_with_no_keyed>
73+
</div>
74+
</div>
75+
"""
76+
end
77+
78+
defp my_component(assigns) do
79+
~H"""
80+
<span>
81+
Count: {@my_count} Name: {@the_name}
82+
</span>
83+
"""
84+
end
85+
86+
attr :rows, :list, required: true
87+
slot :col
88+
89+
defp table_with_all_keyed(assigns) do
90+
~H"""
91+
<div class="mt-8 flow-root">
92+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
93+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
94+
<table class="min-w-full divide-y divide-gray-300">
95+
<thead>
96+
<tr>
97+
<th
98+
:for={slot <- @col}
99+
:key={slot.id}
100+
scope="col"
101+
class="py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0"
102+
>
103+
{slot.name}
104+
</th>
105+
</tr>
106+
</thead>
107+
<tbody class="divide-y divide-gray-200">
108+
<tr :for={row <- @rows} :key={@id.(row)}>
109+
<td
110+
:for={slot <- @col}
111+
:key={"#{@id.(row)}_#{slot.id}"}
112+
class="py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0"
113+
>
114+
{render_slot(slot, row)}
115+
</td>
116+
</tr>
117+
</tbody>
118+
</table>
119+
</div>
120+
</div>
121+
</div>
122+
"""
123+
end
124+
125+
attr :rows, :list, required: true
126+
slot :col
127+
128+
defp table_with_rows_keyed(assigns) do
129+
~H"""
130+
<div class="mt-8 flow-root">
131+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
132+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
133+
<table class="min-w-full divide-y divide-gray-300">
134+
<thead>
135+
<tr>
136+
<th
137+
:for={slot <- @col}
138+
scope="col"
139+
class="py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0"
140+
>
141+
{slot.name}
142+
</th>
143+
</tr>
144+
</thead>
145+
<tbody class="divide-y divide-gray-200">
146+
<tr :for={row <- @rows} :key={@id.(row)}>
147+
<td
148+
:for={slot <- @col}
149+
class="py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0"
150+
>
151+
{render_slot(slot, row)}
152+
</td>
153+
</tr>
154+
</tbody>
155+
</table>
156+
</div>
157+
</div>
158+
</div>
159+
"""
160+
end
161+
162+
attr :rows, :list, required: true
163+
slot :col
164+
165+
defp table_with_no_keyed(assigns) do
166+
~H"""
167+
<div class="mt-8 flow-root">
168+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
169+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
170+
<table class="min-w-full divide-y divide-gray-300">
171+
<thead>
172+
<tr>
173+
<th
174+
:for={slot <- @col}
175+
scope="col"
176+
class="py-3.5 first:pr-3 first:pl-4 px-3 text-left text-sm font-semibold text-gray-900 first:sm:pl-0"
177+
>
178+
{slot.name}
179+
</th>
180+
</tr>
181+
</thead>
182+
<tbody class="divide-y divide-gray-200">
183+
<tr :for={row <- @rows}>
184+
<td
185+
:for={slot <- @col}
186+
class="py-4 first:pr-3 first:pl-4 px-3 text-sm first:font-medium whitespace-nowrap first:text-gray-900 text-gray-500 first:sm:pl-0"
187+
>
188+
{render_slot(slot, row)}
189+
</td>
190+
</tr>
191+
</tbody>
192+
</table>
193+
</div>
194+
</div>
195+
</div>
196+
"""
197+
end
198+
199+
def mount(_params, _session, socket) do
200+
:timer.send_interval(1000, :report_memory)
201+
{:ok, assign(socket, count: 0, items: random_items(@count), size: @count, tailwind: true)}
202+
end
203+
204+
def handle_params(params, _session, socket) do
205+
{:noreply, assign_tab(socket, params)}
206+
end
207+
208+
defp assign_tab(socket, %{"tab" => tab}) when tab in ["all_keyed", "rows_keyed", "no_keyed"] do
209+
assign(socket, :active_tab, tab)
210+
end
211+
212+
defp assign_tab(socket, _), do: assign(socket, :active_tab, "all_keyed")
213+
214+
def handle_event("randomize", _params, socket) do
215+
{:noreply,
216+
socket |> assign(:items, random_items(socket.assigns.size)) |> update(:count, &(&1 + 1))}
217+
end
218+
219+
def handle_event("change_size", %{"size" => size}, socket) do
220+
size =
221+
case size do
222+
"" -> 0
223+
_ -> String.to_integer(size)
224+
end
225+
226+
{:noreply,
227+
socket
228+
|> assign(:items, random_items(size))
229+
|> assign(:size, size)
230+
|> update(:count, &(&1 + 1))}
231+
end
232+
233+
def handle_event("change_0", _params, socket) do
234+
{:noreply,
235+
socket
236+
|> assign(:items, [
237+
%{id: 2000, entry: %{other: "hey", foo: %{bar: "#{System.unique_integer()}"}}}
238+
| Enum.slice(socket.assigns.items, 1..(socket.assigns.size + 1))
239+
])}
240+
end
241+
242+
def handle_event("change_other", _params, socket) do
243+
{:noreply,
244+
socket
245+
|> assign(
246+
:items,
247+
Enum.map(socket.assigns.items, fn item ->
248+
%{item | entry: %{item.entry | other: "hey #{System.unique_integer()}"}}
249+
end)
250+
)}
251+
end
252+
253+
def handle_info(:report_memory, socket) do
254+
:erlang.garbage_collect()
255+
IO.puts("Heap size: #{Process.info(self())[:total_heap_size]}")
256+
257+
{:noreply, socket}
258+
end
259+
260+
def random_items(size) do
261+
1..(size * 2)
262+
|> Enum.take_random(size)
263+
|> Enum.map(&%{id: &1, entry: %{other: "hey", foo: %{bar: "New#{&1 + 1}"}}})
264+
end
265+
end

test/e2e/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
155155
live "/js", E2E.JsLive
156156
live "/select", E2E.SelectLive
157157
live "/components", E2E.ComponentsLive
158+
live "/keyed-comprehension", E2E.KeyedComprehensionLive
158159
end
159160

160161
scope "/portal", Phoenix.LiveViewTest do
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { test, expect } from "../test-fixtures";
2+
import { syncLV, evalLV } from "../utils";
3+
4+
for (let tab of ["all_keyed", "rows_keyed", "no_keyed"]) {
5+
test(`renders correctly - ${tab}`, async ({ page }) => {
6+
await page.goto(`/keyed-comprehension?tab=${tab}`);
7+
await syncLV(page);
8+
9+
for (let i = 0; i < 10; i++) {
10+
await page.getByRole("button", { name: "randomize" }).click();
11+
await syncLV(page);
12+
}
13+
14+
const order = await evalLV(page, `socket.assigns.items`);
15+
16+
const theText = async (page, i, index) =>
17+
(
18+
await page
19+
.locator("table")
20+
.nth(i)
21+
.locator("tbody tr")
22+
.nth(index)
23+
.textContent()
24+
).replace(/\s+/g, " ");
25+
26+
await Promise.all(
27+
order.map(async (item, index) => {
28+
const text0 = await theText(page, 0, index);
29+
const text1 = await theText(page, 1, index);
30+
expect(text0).toEqual(` Count: 10 Name: ${item.entry.foo.bar} 1 10 `);
31+
expect(text1).toEqual(` Count: 10 Name: ${item.entry.foo.bar} 2 10 `);
32+
}),
33+
);
34+
});
35+
}

0 commit comments

Comments
 (0)