How to do "top-down" LiveView rendering tests of nested LiveComponents

Hi,

I cannot figure out how to test some LiveComponents simulating the actual events that would be sent by the user.

In my case, the LiveComponent is not visible when the parent LiveView is rendered. I guess this is where the problem lies but it is required. To test my Liveview, I do:

{:ok, view, html} = live(conn, "/myview")

Doing the action to show the LiveComponent works:

render_click(view, "show", %{"param1" => "value1", "param2" => "value2"})

but I can only test the initial state of the LiveComponent because render_click only renders the html.

For that mater, I can’t use find_child to get the LiveComponent view because it is always nil:

find_child(view, "component_id") # returns nil

So I cannot send other render_* events to the LiveComponent.

It other LiveComponents I created, I used send to change the state in the parent view. This did work fine but in this case the rendering of LiveComponent is not handled in the same way and it would feel wrong to add an handle_info only for testing.

The last thing I looked into is render_component. Again, render_component seems to only render a definite state of the LiveComponent and that is not what I am looking for. Sure, testing that all different component states render what is expected is fine but there is no guarantee that the user actions on the UI set the component in the right state. Moreover, such tests would be hard to maintain: I want to test what is rendered when the user makes some actions, no mater what the implementation is (ie. the assigns state).

There are also other solutions like rendering the LiveComponent and hidding it with css but I am not confortable doing such design decisions only to solve testing problems.

I may not be familiar with all the tools available for testing and maybe I am missing something, that’s why I am asking for help here… Thanks!

3 Likes

I have a similar problem where I want to test a nested live_view that does not mount in router.(with mount :not_mounted_in_router)

I guess those are the caveats of the bleeding edge (sigh)

What is your exact issue and what approach are you taking?

Recent LV updates have improved a lot the test tooling.

Here’s a snippet to test LV with components that aren’t available at initial rendering:

{:ok, view, html} = live(conn, "path/to/lv")

# Component is not there
refute html =~ "some component text"

# click something that displays the component
assert view
           |> element("#button-id")
           |> render_click() =~ "some component text"

# interact with the component inside the parent LV
view
|> form("form[phx-submit=save]")
|> render_submit(%{
  "resource" => %{
    "description" => "some fake params"
  }
})

# test what happens after save
assert_patch(view, "some/other/path)
5 Likes

Hey @shamanime !

Here is what I have:

A “normal” get view like this:

routex.ex

get "/someview", SomeController, :index

In some_view.html.eex template

<div>@inner_content</div>
<div>
  <%= live_render(@conn, SidebarViewLive, session: %{}) %>
</div>

sidebar_view_live.ex

defmodule SidebarViewLive do
  use MyApp, :live_view
  def mount(:not_mounted_at_router, params, socket), do: stuff
end

So 2 questions:

  1. How do I test SidebarViewLive?
  2. How would I test events in any LiveComponent inside that live view?

Thanks for taking the time man!

1: Instead of using live to “mount” your LV in testing you can use live_isolated/3.

2: The snippet I wrote in the other post should work. After you mount your menu LV you can interact with it using the element and render_ helpers to reproduce the steps taken for the component to display and to interact with it. This will work as an integration test: you won’t update the LV assigns directly, instead you’ll mimic what the user does and assert that your LV/Component is updating correctly.

So basically after your router renders and mounts the LV you’ll end with this structure:

<html from layout>
  <html from some_view.html.eex>
    <html from your side_bar LV>
      <html from your component inside the LV that may not be here>
      </html from your component inside the LV that may not be here>
    </html from your side_bar LV>
  </html from some_view.html.eex>
</html from layout>

When you use live_isolated you’ll get only this chunk:

<html from your side_bar LV>
  <html from your component inside the LV that may not be here>
  </html from your component inside the LV that may not be here>
</html from your side_bar LV>

And then you can interact with it by passing CSS selectors to element and using the render_ functions to trigger events in your LV and components.

4 Likes

@shamanime What about multi-step actions?

Lets say I want to test something like this:

     html = view
            |> element("#menu_opener")
            |> render_click()
            |> element("#filter-input")
            |> render_change(%{term: "o"})
     assert html =~ "oo"

The problem is I have the live component hidden, and only shown after the user clicks the menu opener.

1 Like

The render_ helpers returns HTML so they can’t be piped to element, you need to do it step by step like this:

# this will display your component
view
  |> element("#menu_opener")
  |> render_click()

# you interact with it with the same `view` you mounted earlier
assert
  view
  |> element("#filter-input")
  |> render_change(%{term: "o"}) =~ "oo"
3 Likes

Thanks @shamanime for your input. I haven’t had the occasion to write tests in LV 0.13 yet but it looks the original problem remains.

As @phasnox mentionned the requirement here is to test a nested LiveComponent.

Your last example pinpoints my original problem. The problem is that #filter-input does not exist in the view because it is only rendered when #menu_opener is clicked. @phasnox last example shows exactly what we are trying to test but that can’t be done because, as you mention it, render_ returns HTML and not a view that could be used to do additionnal render_click calls.

There is no way to get a view struct that would be the result of the render_click on #menu_opener. Here view only contains original code of the LiveView.

Since live/2 only works for LiveViews, as does live_isolated/3, they cannot be used to test LiveComponents. And there seems to be no way to test UI events on LiveComponents outside of a LiveView.

At this point it looks that it is not possible to test LiveComponents by simulating the user actions on LiveComponents that are not available when the LiveView is created.

Hello!

There are no issues testing live components. My snippet shows how to test them.
I work with an app that has this exact issue you’re describing and I am able to test the component.

Have you tried adding tests as I mentioned?

You need to get them to show by interacting with your LiveView and from there you interact with your component.

Let me expand it so you are sure I understand what you’re saying:

# mount the LiveView, this is from a Route but you can also use live_isolated.
{:ok, view, _html} = live(conn, /path/to/main/lv)

# make sure the **component** is not displayed
# at this point your LV is mounted but the component is not present
refute view |> element("#filter-input") |> has_element?

# click a button in your LV that will display the component
view
  |> element("#menu_opener")
  |> render_click()

# make sure your **component** is displayed after the click
assert view |> element("#filter-input") |> has_element?

# interact with it with your **component**
assert
  view
  |> element("#filter-input")
  |> render_change(%{term: "o"}) =~ "oo"
1 Like

To make this more clear I’ve setup a repo here: https://github.com/shamanime/phx_live_component_test

It is a fresh Phoenix app created with --live with one LiveView that conditionally displays/renders a component.

I’ve used the same snipped I posted here to test the component: https://github.com/shamanime/phx_live_component_test/commit/a3b600bd3bf8183ea1571c00e23e083e1c0715eb

6 Likes

Thanks for the detailed answer @shamanime. To be honest, I did not try the example you gave because it was equivalent to what I was doing, without success, before posting this two months ago.

I don’t have the exact code that caused the initial problem anymore and I upgraded my LV version several times since then. Anyway, it would make no sense trying to reproduce the problem now when your solution does work with current LiveView version.

Thanks for taking the time to answer.

1 Like

@shamanime thanks! Just tried it and worked like a charm!!

I forgot that view is an stateful genserver, that is why this works right?

view
  |> element("#menu_opener")
  |> render_click()

...

assert
  view
  |> element("#filter-input")
  |> render_change(%{term: "o"}) =~ "oo"

Anyways thank you!

1 Like