16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ElixirAdvent Calendar 2021

Day 11

LiveViewのlive_file_inputをテストする

Last updated at Posted at 2022-01-13

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に変更

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で表示

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

複数ファイルの場合

第3引数の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

もう少し広い範囲て見たい場合は element("main") にすると全体が表示されます
スクリーンショット 2022-01-25 15.41.35.png

validationの発火方法 ※2022/04/18追記

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

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

16
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?