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
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追加
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に変更
<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をつけないとアップロードを検知してくれないので注意が必要です
保存時にファイルをコピーしてファイルパスを返す処理を追加
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が走るので別で定義
defmodule BlogWeb.Endpoint do
...
plug Plug.Static,
at: "/",
from: {:blog, "priv"},
gzip: false,
only: ~w(uploads)
...
end
フォルダがないとコピーできないと怒られるのでmkdir
mkdir priv/uploads
indexで表示
<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にする
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とは別処理でやらないとうまく行かずつまりました
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'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'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'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't be blank"
index_live |> element("#post-form") |> open_browser() # 追加
もう少し広い範囲て見たい場合は element("main")
にすると全体が表示されます
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