Phoenix.LiveViewTestを理解する

こんにちは！
プログラミング未経験文系出身、Elixirの国に迷い込んだ？！見習いアルケミストのaliceと申します。
今回はPhoenix.LiveViewTestについて学んだことをまとめます。

# Phoenix.LiveViewTestとは？
ElixirのWebアプリケーションフレームワークであるPhoenixのライブラリである、LiveViewのライフサイクルを利用した自動テストの仕組み。結合テストの自動化が可能になります。

## 実行環境
Windows 11 Home(バージョン 21H2, OS ビルド 22000.1219)
WSL2 Ubuntu22.04
Elixir 1.14.1
Erlang 25.0.4
Phoenix installer v1.7.0-rc.0

# Phoenix.LiveViewTestを試してみた！
## Phoenixプロジェクトを作成する
いつも通りPhoenixプロジェクトを作ります。
```sh
$mix phx.new dec19 --database sqlite3
$cd dec19
$mix ecto.create
```
### mix test.watchを導入する
毎回mix testを実行するのは面倒なので、テストを自動実行するライブラリであるmix test.watchを導入します。
https://hex.pm/packages/mix_test_watch にアクセスし、mix.exs用のmix.test.watchをコピーしてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677280/5b8a464b-cc69-948a-b33e-7b553142d904.png)

mix.exsのdepsに貼り付けます。
```diff_elixir:mix.exs
defmodule Dec19.MixProject do
  use Mix.Project

  def project do
    [
      app: :dec19,
      version: "0.1.0",
      elixir: "~> 1.14",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {Dec19.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.7.0-rc.0", override: true},
      {:phoenix_ecto, "~> 4.4"},
      {:ecto_sql, "~> 3.6"},
      {:ecto_sqlite3, ">= 0.0.0"},
      {:phoenix_html, "~> 3.0"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_view, "~> 0.18.3"},
      {:heroicons, "~> 0.5"},
      {:floki, ">= 0.30.0", only: :test},
      {:phoenix_live_dashboard, "~> 0.7.2"},
      {:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
      {:swoosh, "~> 1.3"},
      {:finch, "~> 0.13"},
      {:telemetry_metrics, "~> 0.6"},
      {:telemetry_poller, "~> 1.0"},
      {:gettext, "~> 0.20"},
      {:jason, "~> 1.2"},
      {:plug_cowboy, "~> 2.5"},
+     {:mix_test_watch, "~> 1.1"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to install project dependencies and perform other setup tasks, run:
  #
  #     $ mix setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [
      setup: ["deps.get", "ecto.setup"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
      "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
    ]
  end
end
```

ライブラリをとってきます。
```sh
$ mix deps.get
```

追加したライブラリの動作確認をします。
```sh
$ mix test.watch
```

## PhoenixプロジェクトをLiveView化する
LiveViewを導入します。
```sh
$ mix phx.gen.live Accounts User users name:string
```

routerにpathを追加します。
```diff_elixir:lib/dec19_web/router.ex
defmodule Dec19Web.Router do
  use Dec19Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {Dec19Web.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", Dec19Web do
    pipe_through :browser

+   live "/users", UserLive.Index, :index
+   live "/users/new", UserLive.Index, :new
+   live "/users/:id/edit", UserLive.Index, :edit
+
+   live "/users/:id", UserLive.Show, :show
+   live "/users/:id/show/edit", UserLive.Show, :edit

    get "/", PageController, :home
  end

  # Other scopes may use custom stacks.
  # scope "/api", Dec19Web do
  #   pipe_through :api
  # end

  # Enable LiveDashboard and Swoosh mailbox preview in development
  if Application.compile_env(:dec19, :dev_routes) do
    # If you want to use the LiveDashboard in production, you should put
    # it behind authentication and allow only admins to access it.
    # If your application does not have an admins-only section yet,
    # you can use Plug.BasicAuth to set up some basic authentication
    # as long as you are also using SSL (which you should anyway).
    import Phoenix.LiveDashboard.Router

    scope "/dev" do
      pipe_through :browser

      live_dashboard "/dashboard", metrics: Dec19Web.Telemetry
      forward "/mailbox", Plug.Swoosh.MailboxPreview
    end
  end
end
```

マイグレーションします。
```
$ mix ecto.migrate
```

## LiveViewTestの動きを検証してみる
まずはmix test.watchを起動
```sh
$ mix test.watch
Running tests...
.............
Finished in 0.3 seconds (0.1s async, 0.2s sync)
18 tests, 0 failures
```

エンドポイント/usersのページに文字列"hogehoge"を加えて、それが存在することを確認してみましょう。
※以下ソースコード中のシギル~pはphoenix1.7以降の新機能です。詳しくは@the_haigo さんの[こちらの記事](https://qiita.com/the_haigo/items/bc848d80ff8fdf6ffa79)をご覧ください。
```diff_elixir:lib/dec19_web/live/user_live/index.html.heex
  <.header>
  Listing Users
  <:actions>
    <.link patch={~p"/users/new"}>
      <.button>New User</.button>
    </.link>
  </:actions>
  </.header>

+ <h1>hogehoge</h1>
  <.table id="users" rows={@users} row_click={&JS.navigate(~p"/users/#{&1}")}>
  <:col :let={user} label="Name"><%= user.name %></:col>
  <:action :let={user}>
    <div class="sr-only">
      <.link navigate={~p"/users/#{user}"}>Show</.link>
    </div>
    <.link patch={~p"/users/#{user}/edit"}>Edit</.link>
  </:action>
  <:action :let={user}>
    <.link phx-click={JS.push("delete", value: %{id: user.id})} data-confirm="Are you sure?">
      Delete
    </.link>
  </:action>
  </.table>

  <.modal
  :if={@live_action in [:new, :edit]}
  id="user-modal"
  show
  on_cancel={JS.navigate(~p"/users")}
  >
  <.live_component
    module={Dec19Web.UserLive.FormComponent}
    id={@user.id || :new}
    title={@page_title}
    action={@live_action}
    user={@user}
    navigate={~p"/users"}
  />
  </.modal>
```
・変数index_liveは公式ドキュメントのいうところのViewとよばれるもの。
・[=~演算子はテキストベースの一致演算子](https://hexdocs.pm/elixir/1.14/Kernel.html#=~/2)。そのまま使用すると部分一致検索である点に注意。
```diff_elixir:test/dec19_web/live/user_live_test.exs
  defmodule Dec19Web.UserLiveTest do
    use Dec19Web.ConnCase

    import Phoenix.LiveViewTest
    import Dec19.AccountsFixtures

    @create_attrs %{name: "some name"}
    @update_attrs %{name: "some updated name"}
    @invalid_attrs %{name: nil}

    defp create_user(_) do
      user = user_fixture()
      %{user: user}
    end

    describe "Index" do
      setup [:create_user]

      test "lists all users", %{conn: conn, user: user} do
+       {:ok, index_live, html} = live(conn, ~p"/users")

        assert html =~ "Listing Users"
        assert html =~ user.name
+       assert index_live |> has_element?("h1", "hogehoge")
      end

      test "saves new user", %{conn: conn} do
        {:ok, index_live, _html} = live(conn, ~p"/users")

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

        assert_patch(index_live, ~p"/users/new")

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

        {:ok, _, html} =
          index_live
          |> form("#user-form", user: @create_attrs)
          |> render_submit()
          |> follow_redirect(conn, ~p"/users")

        assert html =~ "User created successfully"
        assert html =~ "some name"
      end

      test "updates user in listing", %{conn: conn, user: user} do
        {:ok, index_live, _html} = live(conn, ~p"/users")

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

        assert_patch(index_live, ~p"/users/#{user}/edit")

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

        {:ok, _, html} =
          index_live
          |> form("#user-form", user: @update_attrs)
          |> render_submit()
          |> follow_redirect(conn, ~p"/users")

        assert html =~ "User updated successfully"
        assert html =~ "some updated name"
      end

      test "deletes user in listing", %{conn: conn, user: user} do
        {:ok, index_live, _html} = live(conn, ~p"/users")

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

    describe "Show" do
      setup [:create_user]

      test "displays user", %{conn: conn, user: user} do
        {:ok, _show_live, html} = live(conn, ~p"/users/#{user}")

        assert html =~ "Show User"
        assert html =~ user.name
      end

      test "updates user within modal", %{conn: conn, user: user} do
        {:ok, show_live, _html} = live(conn, ~p"/users/#{user}")

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

        assert_patch(show_live, ~p"/users/#{user}/show/edit")

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

        {:ok, _, html} =
          show_live
          |> form("#user-form", user: @update_attrs)
          |> render_submit()
          |> follow_redirect(conn, ~p"/users/#{user}")

        assert html =~ "User updated successfully"
        assert html =~ "some updated name"
      end
    end
  end
```
テストが1件増え、かつ成功していることが確認できました。
```sh
$ mix test.watch
Running tests...
..........
Finished in 0.3 seconds (0.1s async, 0.2s sync)
19 tests, 0 failures
```

### (余談)mix test / mix test.watchを、特定のモジュール・テストだけ実行したい場合
特定のモジュールの場合→オプションに<テストモジュール(相対パス)>を渡す。
```sh
$ mix test.watch test/dec19_web/live/user_live_test.exs
Running tests...
......
Finished in 0.3 seconds (0.00s async, 0.3s sync)
6 tests, 0 failures
```

特定のテストの場合→オプションに <テストモジュール(相対パス)>:<開始行> を渡す。
```sh
$ mix test.watch mix test.watch test/dec19_web/live/user_live_test.exs:27
Running tests...
......
Finished in 0.3 seconds (0.00s async, 0.3s sync)
6 tests, 0 failures
```

### (余談)IO.inspect()のデバッグ結果を全表示したいとき
IO.inspect()の引数に limit: :infinity, printable_limit: :infinity を渡す。
普通にIO.inspect()した場合
```elixir:test/dec19_web/live/user_live_test.exs
test "saves new user", %{conn: conn} do
      {:ok, index_live, _html} = live(conn, ~p"/users")

      assert index_live
             |> element("a", "New User1")
             |> render_click()
             |> IO.inspect() =~
               "新規作成"
```

結果行が途中で...と切れて最後まで見えない。
```sh
Running tests...
Excluding tags: [:test]
Including tags: [line: "27"]

"<header class=\"px-4 sm:px-6 lg:px-8\"><div class=\"flex items-center justify-between border-b border-zinc-100 py-3\"><div class=\"flex items-center gap-4\"><a href=\"/\"><svg viewbox=\"0 0 71 48\" class=\"h-6\" aria-hidden=\"true\"><path d=\"m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z\" fill=\"#FD4F00\"></path></svg></a><p class=\"rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand\">\n        v1.7\n      </p></div><div class=\"flex items-center gap-4\"><a href=\"https://twitter.com/elixirphoenix\" class=\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\">\n        @elixirphoenix\n      </a><a href=\"https://github.com/phoenixframework/phoenix\" class=\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\">\n        GitHub\n      </a><a href=\"https://hexdocs.pm/phoenix/overview.html\" class=\"rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70\">\n        Get Started <span aria-hidden=\"true\">→</s" <> ...
.
Finished in 0.3 seconds (0.00s async, 0.3s sync)
6 tests, 0 failures, 5 excluded
```

IO.inspect()の引数に limit: :infinity, printable_limit: :infinity を渡す場合
```diff_elixir:test/dec19_web/live/user_live_test.exs
test "saves new user", %{conn: conn} do
      {:ok, index_live, _html} = live(conn, ~p"/users")

      assert index_live
             |> element("a", "New User1")
             |> render_click()
+            |> IO.inspect(limit: :infinity, printable_limit: :infinity) =~
               "新規作成"
```

結果行が全部見える（代わりに出力が長くなる）
```sh
alice@okadanopc:~/code/practice/dec19$ mix test.watch test/dec19_web/live/user_live_test.exs:27

Running tests...
Excluding tags: [:test]
Including tags: [line: "27"]

"<header class=\"px-4 sm:px-6 lg:px-8\"><div class=\"flex items-center justify-between border-b border-zinc-100 py-3\"><div class=\"flex items-center gap-4\"><a href=\"/\"><svg viewbox=\"0 0 71 48\" class=\"h-6\" aria-hidden=\"true\"><path d=\"m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z\" fill=\"#FD4F00\"></path></svg></a><p class=\"rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand\">\n        v1.7\n      </p></div><div class=\"flex items-center gap-4\"><a href=\"https://twitter.com/elixirphoenix\" class=\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\">\n        @elixirphoenix\n      </a><a href=\"https://github.com/phoenixframework/phoenix\" class=\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\">\n        GitHub\n      </a><a href=\"https://hexdocs.pm/phoenix/overview.html\" class=\"rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70\">\n        Get Started <span aria-hidden=\"true\">→</span></a></div></div></header><main class=\"px-4 py-20 sm:px-6 lg:px-8\"><div class=\"mx-auto max-w-2xl\"><div id=\"disconnected\" phx-click=\"[[&quot;push&quot;,{&quot;event&quot;:&quot;lv:clear-flash&quot;,&quot;value&quot;:{&quot;key&quot;:&quot;error&quot;}}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#flash&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}]]\" role=\"alert\" class=\"fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1 bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900\" phx-connected=\"[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#disconnected&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}]]\" phx-disconnected=\"[[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#disconnected&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;]]}]]\"><p class=\"flex items-center gap-1.5 text-[0.8125rem] font-semibold leading-6\"><svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" class=\"h-4 w-4\" fill=\"currentColor\" viewbox=\"0 0 20 20\"><path fill-rule=\"evenodd\" d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z\" clip-rule=\"evenodd\"></path></svg>\n    We can&#39;t find the internet\n  </p><p class=\"mt-2 text-[0.8125rem] leading-5\">\n      Attempting to reconnect <svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" class=\"ml-1 w-3 h-3 inline animate-spin\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" viewbox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99\"></path></svg></p></div><header class=\"flex items-center justify-between gap-6\"><div><h1 class=\"text-lg font-semibold leading-8 text-zinc-800\">\n      \n  Listing Users\n  \n    </h1></div><div class=\"flex-none\"><a href=\"/users/new\" data-phx-link=\"patch\" data-phx-link-state=\"push\"><button class=\"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80 \">\n  New User1\n</button></a></div></header><h1>hogehoge</h1><div id=\"users\" class=\"overflow-y-auto px-4 sm:overflow-visible sm:px-0\"><table class=\"mt-11 w-[40rem] sm:w-full\"><thead class=\"text-left text-[0.8125rem] leading-6 text-zinc-500\"><tr><th class=\"p-0 pb-4 pr-6 font-normal\">Name</th><th class=\"relative p-0 pb-4\"><span class=\"sr-only\">Actions</span></th></tr></thead><tbody class=\"relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700\"><tr id=\"users-1\" class=\"relative group hover:bg-zinc-50\"><td phx-click=\"[[&quot;navigate&quot;,{&quot;href&quot;:&quot;/users/1&quot;,&quot;replace&quot;:false}]]\" class=\"p-0 hover:cursor-pointer\"><div><span class=\"absolute h-full w-4 top-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl\"></span><span class=\"absolute h-full w-4 top-0 -right-4 group-hover:bg-zinc-50 sm:rounded-r-xl\"></span></div><div class=\"block py-4 pr-6\"><span class=\"relative font-semibold text-zinc-900\">\n              some name\n            </span></div></td><td class=\"p-0 w-14\"><div class=\"relative whitespace-nowrap py-4 text-right text-sm font-medium\"><span class=\"relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700\"><div class=\"sr-only\"><a href=\"/users/1\" data-phx-link=\"redirect\" data-phx-link-state=\"push\">Show</a></div><a href=\"/users/1/edit\" data-phx-link=\"patch\" data-phx-link-state=\"push\">Edit</a></span><span class=\"relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700\"><a href=\"#\" data-confirm=\"Are you sure?\" phx-click=\"[[&quot;push&quot;,{&quot;event&quot;:&quot;delete&quot;,&quot;value&quot;:{&quot;id&quot;:1}}]]\">\n      Delete\n    </a></span></div></td></tr></tbody></table></div><div id=\"user-modal\" phx-mounted=\"[[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;],[&quot;opacity-100&quot;]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;]]}],[&quot;add_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;focus_first&quot;,{&quot;to&quot;:&quot;#user-modal-content&quot;}]]\" phx-remove=\"[[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[&quot;block&quot;],[&quot;block&quot;],[&quot;hidden&quot;]]}],[&quot;remove_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;pop_focus&quot;,{}]]\" class=\"relative z-50 hidden\"><div id=\"user-modal-bg\" class=\"fixed inset-0 bg-zinc-50/90 transition-opacity\" aria-hidden=\"true\"></div><div class=\"fixed inset-0 overflow-y-auto\" aria-labelledby=\"user-modal-title\" aria-describedby=\"user-modal-description\" role=\"dialog\" aria-modal=\"true\" tabindex=\"0\"><div class=\"flex min-h-full items-center justify-center\"><div class=\"w-full max-w-3xl p-4 sm:p-6 lg:py-8\"><div id=\"user-modal-container\" phx-hook=\"Phoenix.FocusWrap\" class=\"hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition\" phx-click-away=\"[[&quot;navigate&quot;,{&quot;href&quot;:&quot;/users&quot;,&quot;replace&quot;:false}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[&quot;block&quot;],[&quot;block&quot;],[&quot;hidden&quot;]]}],[&quot;remove_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;pop_focus&quot;,{}]]\" phx-key=\"escape\" phx-mounted=\"[[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;],[&quot;opacity-100&quot;]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;]]}],[&quot;add_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;focus_first&quot;,{&quot;to&quot;:&quot;#user-modal-content&quot;}]]\" phx-window-keydown=\"[[&quot;navigate&quot;,{&quot;href&quot;:&quot;/users&quot;,&quot;replace&quot;:false}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[&quot;block&quot;],[&quot;block&quot;],[&quot;hidden&quot;]]}],[&quot;remove_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;pop_focus&quot;,{}]]\"><span id=\"user-modal-container-start\" tabindex=\"0\" aria-hidden=\"true\"></span><div class=\"absolute top-6 right-5\"><button phx-click=\"[[&quot;navigate&quot;,{&quot;href&quot;:&quot;/users&quot;,&quot;replace&quot;:false}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;],[&quot;opacity-0&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-in&quot;,&quot;duration-200&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;]]}],[&quot;hide&quot;,{&quot;time&quot;:200,&quot;to&quot;:&quot;#user-modal&quot;,&quot;transition&quot;:[[&quot;block&quot;],[&quot;block&quot;],[&quot;hidden&quot;]]}],[&quot;remove_class&quot;,{&quot;names&quot;:[&quot;overflow-hidden&quot;],&quot;time&quot;:200,&quot;to&quot;:&quot;body&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;pop_focus&quot;,{}]]\" type=\"button\" class=\"-m-3 flex-none p-3 opacity-20 hover:opacity-40\" aria-label=\"close\"><svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" class=\"h-5 w-5 stroke-current\" fill=\"currentColor\" viewbox=\"0 0 24 24\"><path fill-rule=\"evenodd\" d=\"M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z\" clip-rule=\"evenodd\"></path></svg></button></div><div id=\"user-modal-content\"><div data-phx-component=\"1\"><header class=\"\"><div><h1 class=\"text-lg font-semibold leading-8 text-zinc-800\">\n      \n    New User\n    新規作成\n    \n    </h1><p class=\"mt-2 text-sm leading-6 text-zinc-600\">\n      Use this form to manage user records in your database.\n    </p></div><div class=\"flex-none\"></div></header><form method=\"post\" errors=\"\" id=\"user-form\" phx-change=\"validate\" phx-submit=\"save\" phx-target=\"1\"><div class=\"space-y-8 bg-white mt-10\"><div phx-feedback-for=\"user[name]\"><label for=\"user-form_name\" class=\"block text-sm font-semibold leading-6 text-zinc-800\">\n  name\n</label><input type=\"text\" name=\"user[name]\" id=\"user-form_name\" class=\"border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px] text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5\"/></div><div class=\"mt-2 flex items-center justify-between gap-6\"><button class=\"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80 \" phx-disable-with=\"Saving...\">\n  Save User\n</button></div></div></form></div></div><span id=\"user-modal-container-end\" tabindex=\"0\" aria-hidden=\"true\"></span></div></div></div></div></div></div></main>"
.
Finished in 0.2 seconds (0.00s async, 0.2s sync)
6 tests, 0 failures, 5 excluded
```


# ～Elixirの国のご案内～
※Elixirって何ぞや？と思ったらこちらもどぞ。未来がぎゅっと詰まった、Elixirは今年で生まれて10周年です:laughing::sparkles::sparkles:

https://qiita.com/piacerex/items/7bf8f9f4f5f576e597f5



[We Are The Alchemists, my friends!:bouquet:](https://qiita.com/torifukukaiou/items/6bcd5bbb80df9e594e9e "Awesome-san's Awesome article")[^1]
Elixirコミュニティは本当に優しくて温かい人たちばかり！
私が挫折せずにいられるのもこの恵まれた環境のおかげです。


まずは気軽に話しかけてみてください。[^10]


https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian




[^1]: @torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。


[^10]: @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中！