Skip to content

Serabe/live_isolated_component

Repository files navigation

LiveIsolatedComponent

Elixir CI

The simplest way to test a LiveView both stateful and function component in isolation while keeping the interactivity.

Installation

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.

Basic usage

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.

Example

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
  )

Slots

The slots options can be:

  1. Just a slot. In that case, it'd be taken as the default slot.
  2. 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.

Defining a slot

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.

Slot Examples

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
    ]
  }
)