LiveViewのlive_file_inputをテストする

LiveViewのファイルアップロードのテストの書き方がGuideに乗ってなかったので忘備録

# テスト用のアプリ作成

```
mix phx.new blog
cd blog
mix ecto.create
```

```
mix phx.gen.live Blogs Post posts title:string body:string eyecatch:string
mix ecto.migrate
```

```lib/blog_web/router.ex
defmodule BlogWeb.Router do
  ...
  scope "/", BlogWeb do
    pipe_through :browser

    get "/", PageController, :index
    live "/posts", PostLive.Index, :index
    live "/posts/new", PostLive.Index, :new
    live "/posts/:id/edit", PostLive.Index, :edit

    live "/posts/:id", PostLive.Show, :show
    live "/posts/:id/show/edit", PostLive.Show, :edit    
  end
end
```

## allow_upload追加

```lib/blog_web/live/post_live/form_component.ex
defmodule BlogWeb.PostLive.FormComponent do
  use BlogWeb, :live_component

  alias Blog.Blogs

  @impl true
  def update(%{post: post} = assigns, socket) do
    changeset = Blogs.change_post(post)

    {
      :ok,
      socket
      |> assign(assigns)
      |> assign(:changeset, changeset)
      |> allow_upload(:image, accept: :any)
    }
  end
  ...
end
```

## formのeyecatchをlive_file_inputに変更

```leex:lib/blog_web/live/post_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="post-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>
  
    <%= label f, :body %>
    <%= text_input f, :body %>
    <%= error_tag f, :body %>
  
    <%= label f, :eyecatch %>
    <%= live_file_input @uploads.image %>
    <%= for entry <- @uploads.image.entries do %>
      <figure>
        <%= live_img_preview entry %>
        <figcaption><%= entry.client_name %></figcaption>
      </figure>
    <% end %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```
今回は phx.gen.liveで生成したformなので`phx-change="validate"`が付いていますが
独自に実装してphx-changeをつけないとアップロードを検知してくれないので注意が必要です


## 保存時にファイルをコピーしてファイルパスを返す処理を追加

```lib/blog_web/live/post_live/form_component.ex
defmodule BlogWeb.PostLive.FormComponent do
  ...
  def handle_event("save", %{"post" => post_params}, socket) do
    uploaded_file =
      consume_uploaded_entries(socket, :image, fn %{path: path}, _entry ->
        dest = Path.join([:code.priv_dir(:blog), "uploads", Path.basename(path)])
        File.cp!(path, dest)
        Routes.static_path(socket, "/uploads/#{Path.basename(dest)}")
      end)
      |> List.first
    post_params = Map.put(post_params, :eyecatch, uploade_file)
    save_post(socket, socket.assigns.action, post_params)
  end
  ...
end
```

## endpointに/uploadsで追加、assetsとかと一緒にするとlive_reloadが走るので別で定義

```lib/blog_web/endpoint.ex
defmodule BlogWeb.Endpoint do
  ...
  plug Plug.Static,
    at: "/",
    from: {:blog, "priv"},
    gzip: false,
    only: ~w(uploads)
  ...
end
```
## フォルダがないとコピーできないと怒られるのでmkdir

```
mkdir priv/uploads
```

## indexで表示

```leex:lib/blog_web/live/post_live/index.html.heex
<h1>Listing Posts</h1>

...
<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th>Eyecatch</th>

      <th></th>
    </tr>
  </thead>
  <tbody id="posts">
    <%= for post <- @posts do %>
      <tr id={"post-#{post.id}"}>
        <%= if is_nil(post.eyecatch) do %>
          <td>no image</td>
        <% else %>
          <td><img src={Routes.static_path(@socket, post.eyecatch)} width="80"></td>
        <% end %>
        <td><%= post.title %></td>
        <td><%= post.body %></td>

        <td>
          <span><%= live_redirect "Show", to: Routes.post_show_path(@socket, :show, post) %></span>
          <span><%= live_patch "Edit", to: Routes.post_index_path(@socket, :edit, post) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: post.id, data: [confirm: "Are you sure?"] %></span>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<span><%= live_patch "New Post", to: Routes.post_index_path(@socket, :new) %></span>
```

これでファイルをアップロードするアプリができました
続いてテストを書いていきます

# live_file_inputのテストを追加する

## 下準備

```
mkdir test/support/upload_files
cp priv/static/images/phoenix.png test/support/upload_files/phoenix.png
```

## ファイルパスじゃないとエラーになるので、fixturesのeyecatchをpriv/static/images/phoenix.pngにする

```test/support/fixtures/blogs_fixtures.ex
defmodule Blog.BlogsFixtures do
  def post_fixture(attrs \\ %{}) do
    {:ok, post} =
      attrs
      |> Enum.into(%{
        body: "some body",
        eyecatch: "/images/phoenix.png",
        title: "some title"
      })
      |> Blog.Blogs.create_post()

    post
  end
end

```

## file_input
file_uploadのテストはfile_inputとrender_uploadを使います
file_inputの引数は以下になります

- 1 LiveView
- 2 form id
- 3 allow_uploadで指定したatom
- 4 uploadするファイルのリスト
  - uploadするファイルは ファイル名、mime、バイナリがあれば大丈夫そうです

render_uploadでアップロード処理を実行してファイル名が表示されるかをチェックします

これはformとは別処理でやらないとうまく行かずつまりました

```test/blog_web/live/post_live_test.exs
defmodule BlogWeb.PostLiveTest do
  use BlogWeb.ConnCase

  import Phoenix.LiveViewTest
  import Blog.BlogsFixtures
  
  # eyecatchの入力欄はないので消す
  @create_attrs %{body: "some body", title: "some title"}
  @update_attrs %{body: "some updated body", title: "some updated title"}
  @invalid_attrs %{body: nil,  title: nil}

  defp create_post(_) do
    post = post_fixture()
    %{post: post}
  end

  describe "Index" do
    setup [:create_post]

    test "lists all posts", %{conn: conn, post: post} do
      {:ok, _index_live, html} = live(conn, Routes.post_index_path(conn, :index))

      assert html =~ "Listing Posts"
      assert html =~ post.body
    end

    test "saves new post", %{conn: conn} do
      {:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))

      assert index_live |> element("a", "New Post") |> render_click() =~
               "New Post"

      assert_patch(index_live, Routes.post_index_path(conn, :new))

      assert index_live
             |> form("#post-form", post: @invalid_attrs)
             |> render_change() =~ "can&#39;t be blank"
      # ここ追加
      assert index_live
             |> file_input("#post-form", :image, [
               %{
                 name: "phoenix.png",
                 type: "image/png",
                 content: File.read!("test/support/upload_files/phoenix.png")
               }
             ])
             |> render_upload("phoenix.png") =~ "phoenix.png"

      {:ok, _, html} =
        index_live
        |> form("#post-form", post: @create_attrs)
        |> render_submit()
        |> follow_redirect(conn, Routes.post_index_path(conn, :index))

      assert html =~ "Post created successfully"
      assert html =~ "some body"
    end

    test "updates post in listing", %{conn: conn, post: post} do
      {:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))

      assert index_live |> element("#post-#{post.id} a", "Edit") |> render_click() =~
               "Edit Post"

      assert_patch(index_live, Routes.post_index_path(conn, :edit, post))

      assert index_live
             |> form("#post-form", post: @invalid_attrs)
             |> render_change() =~ "can&#39;t be blank"
      # ここ追加
      assert index_live
             |> file_input("#post-form", :image, [
               %{
                 name: "phoenix.png",
                 type: "image/png",
                 content: File.read!("test/support/upload_files/phoenix.png")
               }
             ])
             |> render_upload("phoenix.png") =~ "phoenix.png"

      {:ok, _, html} =
        index_live
        |> form("#post-form", post: @update_attrs)
        |> render_submit()
        |> follow_redirect(conn, Routes.post_index_path(conn, :index))

      assert html =~ "Post updated successfully"
      assert html =~ "some updated body"
    end

    test "deletes post in listing", %{conn: conn, post: post} do
      {:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))

      assert index_live |> element("#post-#{post.id} a", "Delete") |> render_click()
      refute has_element?(index_live, "#post-#{post.id}")
    end
  end

  describe "Show" do
    setup [:create_post]

    test "displays post", %{conn: conn, post: post} do
      {:ok, _show_live, html} = live(conn, Routes.post_show_path(conn, :show, post))

      assert html =~ "Show Post"
      assert html =~ post.body
    end

    test "updates post within modal", %{conn: conn, post: post} do
      {:ok, show_live, _html} = live(conn, Routes.post_show_path(conn, :show, post))

      assert show_live |> element("a", "Edit") |> render_click() =~
               "Edit Post"

      assert_patch(show_live, Routes.post_show_path(conn, :edit, post))

      assert show_live
             |> form("#post-form", post: @invalid_attrs)
             |> render_change() =~ "can&#39;t be blank"
      # ここ追加
      assert show_live
             |> file_input("#post-form", :image, [
               %{
                 name: "phoenix.png",
                 type: "image/png",
                 content: File.read!("test/support/upload_files/phoenix.png")
               }
             ])
             |> render_upload("phoenix.png") =~ "phoenix.png"

      {:ok, _, html} =
        show_live
        |> form("#post-form", post: @update_attrs)
        |> render_submit()
        |> follow_redirect(conn, Routes.post_show_path(conn, :show, post))

      assert html =~ "Post updated successfully"
      assert html =~ "some updated body"
    end
  end
end
```

## 複数ファイルの場合
第３引数のListにアップロードするファイル情報を追加
file_inputが複数の場合はfile_inputの数だけ file_inputとrender_uploadを実行してください

## 未実装部分
editが前の値を考慮せずに更新してしまうのでそこは別処理が必要になりますのご注意ください

# 補足
テスト時に使える便利な関数に open_browserというものが合って
elementで指定したDOMを静的HTMLを生成してブラウザで開いてくれるというすぐれものです

```
      assert index_live
             |> form("#post-form", post: @invalid_attrs)
             |> render_change() =~ "can&#39;t be blank"

      index_live |> element("#post-form") |> open_browser() # 追加
```
![スクリーンショット 2022-01-25 15.42.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3c7d019c-dc06-1b89-f79a-dfa3915038ae.png)


もう少し広い範囲て見たい場合は `element("main")` にすると全体が表示されます
![スクリーンショット 2022-01-25 15.41.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ff6f2e10-594e-3e58-84d7-ff8657a4f801.png)

# validationの発火方法 ※2022/04/18追記
ファイルをアップロードした際にvalidationイベントで処理をしてアップロードをキャンセルさせたい場合、
file_inputだとvalidationイベントを発火してくれないので
file_input後formに変更を加えないように空のMap(%{})を第３引数に入れて、`render_change()`を実行するとfile_inputが入った状態でのvalidationを発火した結果を表示してくれます

```elixir
assert index_live
       |> form("#post-form", %{})
       |> render_change() =~ "バリデーションエラーメッセージ"
```

# 最後に
いかがでしょうか？
しかもseleniumとか使っていないので爆速です
buildinだからライブラリのインストールも設定もいらない
LiveViewはテストもいいぞぉ

# 参考サイト
https://hexdocs.pm/phoenix_live_view/0.17.5/uploads.html#content
https://hexdocs.pm/phoenix_live_view/0.17.5/Phoenix.LiveViewTest.html#file_input/4
https://hexdocs.pm/phoenix_live_view/0.17.5/Phoenix.LiveViewTest.html#render_upload/3
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#open_browser/2