The simplest way to test a LiveView both stateful and function component in isolation while keeping the interactivity.
Version
0.7.0drops support for some older versions of Elixir, OTP, Phoenix and Phoenix LiveView. This was done because the current CI matrix generated 24 different builds and just adding OTP 26 would mean duplicating that. Also, removing support for LiveView 0.18.16 drop some code.
Note: Current version supports OTP 27 and above, Elixir 1.17 and above, Phoenix 1.7 and above, and LiveView 1.0 and above. For the exact support matrix, check the elixir-ci workflow matrix.
def deps do
[
# For support for LiveView 1.1.0 and Phoenix 1.8:
{:live_isolated_component, "~> 0.10.0", only: [:dev, :test]}
# For support for LiveView 1.0.0:
{:live_isolated_component, "~> 0.9.0", only: [:dev, :test]}
# For support for LiveView 0.20:
{:live_isolated_component, "~> 0.8.0", only: [:dev, :test]}
# If you are using OTP 25 or above, Elixir 1.14, Phoenix 1.7, LiveView 0.19:
{:live_isolated_component, "~> 0.7.0", only: [:dev, :test]}
# If you are in LV 0.18 or above
{:live_isolated_component, "~> 0.6.5", only: [:dev, :test]}
# If you are in LV 0.17
{:live_isolated_component, "~> 0.5.2", only: [:dev, :test]}
]
endDocumentation can be found at hexdocs.
Importing LiveIsolatedComponent will import one function, live_assign, and a few macros. You can use live_isolated_component like you would use live_isolated, just pass the component you want to test as the first argument and use the options as you see fit. If you want to change the passed assigns from the test, use live_assign with the view instead of the socket.
Simple rendering:
{:ok, view, _html} = live_isolated_component(SimpleButton)
assert has_element?(view, ".count", "Clicked 0 times")
view
|> element("button")
|> render_click()
assert has_element?(view, ".count", "Clicked 1 times")Testing assigns:
{:ok, view, _html} = live_isolated_component(Greeting, %{name: "Sergio"})
assert has_element?(view, ".name", "Sergio")
live_assign(view, :name, "Fran")
# or
# live_assign(view, name: "Fran")
# or
# live_assign(view, %{name: "Fran"})
assert has_element?(view, ".name", "Fran")Testing handle_event:
{:ok, view, _html} = live_isolated_component(SimpleButton,
assigns: %{on_click: :i_was_clicked}
)
view
|> element("button")
|> render_click()
assert_handle_event view, :i_was_clickedTesting handle_info:
{:ok, view, _html} = live_isolated_component(ComplexButton,
assigns: %{on_click: :i_was_clicked}
)
view
|> element("button")
|> render_click()
assert_handle_info view, :i_was_clickedhandle_event callback:
{:ok, view, _html} = live_isolated_component(SimpleButton,
assigns: %{on_click: :i_was_clicked},
handle_event: fn :i_was_clicked, _params, socket ->
# Do something
{:noreply, socket}
end
)handle_info callback:
{:ok, view, _html} = live_isolated_component(SimpleButton,
assigns: %{on_click: :i_was_clicked},
handle_info: fn :i_was_clicked, _params, socket ->
# Do something
{:noreply, socket}
end
)The slots options can be:
- Just a slot. In that case, it'd be taken as the default slot.
- A map or keywords. In this case, the keys are the name of the slots, the values can either be a slot or an array of slots. In case of keywords, the values will be collected for the same slot name.
We define slots by using the slot macro. This macro accepts a keyword list and a block.
The block needs to return a template (you can use sigil_H). The keywords will be considered
attributes of the slot except for the following let:
letwill bind the argument to the value. You can use destructuring here.
Like in a real slot, the assigns the slot have access to is that of the parent LiveView.
Just a default slot:
{:ok, view, html} = live_isolated_component(MyComponent,
slots: slot(assigns: assigns) do
~H[Hello from default slot]
end
)Just a default slot (map version):
{:ok, view, html} = live_isolated_component(MyComponent,
slots: %{
inner_block: slot(assigns: assigns) do
~H[Hello from default slot]
end
}
)Named slot (only one slot defined):
{:ok, view, html} = live_isolated_component(MyTableComponent,
slots: %{
col: slot(assigns: assigns, header: "Column Header") do
~H[Hello from the column slot]
end
}
)Named slot (multiple slots defined):
{:ok, view, html} = live_isolated_component(MyTableComponent,
slots: %{
col: [
slot(assigns: assigns, let: item, header: "Language") do
~H[<%= item.language %>]
end,
slot(assigns: assigns, let: %{greeting: greeting}, header: "Greeting") do
~H[<%= greeting %>]
end
]
}
)