The simplest way to test a LiveView both stateful and function component in isolation while keeping the interactivity.
Version
0.7.0
drops 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 25 and above, Elixir 1.14 and above, Phoenix 1.7, and LiveView 0.19 and above. When live_isolated_component reaches 1.0, it'll only actively support latest OTP and previouw, latest Elixir and previous, and LiveView above 1.0. That means that as soon as a version that is not supported actively starts to warns or error in CI, its support will be dropped. Thank you for your understanding!
def deps do
[
# 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]}
]
end
Documentation 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_clicked
Testing 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_clicked
handle_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
:
let
will 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
]
}
)