LoginSignup
16
1

More than 1 year has passed since last update.

こんにちは!
プログラミング未経験文系出身、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プロジェクトを作ります。

$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

mix.exsのdepsに貼り付けます。

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

ライブラリをとってきます。

$ mix deps.get

追加したライブラリの動作確認をします。

$ mix test.watch

PhoenixプロジェクトをLiveView化する

LiveViewを導入します。

$ mix phx.gen.live Accounts User users name:string

routerにpathを追加します。

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を起動

$ 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 さんのこちらの記事をご覧ください。

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とよばれるもの。
=~演算子はテキストベースの一致演算子。そのまま使用すると部分一致検索である点に注意。

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件増え、かつ成功していることが確認できました。

$ 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を、特定のモジュール・テストだけ実行したい場合

特定のモジュールの場合→オプションに<テストモジュール(相対パス)>を渡す。

$ 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

特定のテストの場合→オプションに <テストモジュール(相対パス)>:<開始行> を渡す。

$ 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()した場合

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() =~
               "新規作成"

結果行が途中で...と切れて最後まで見えない。

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 を渡す場合

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) =~
               "新規作成"

結果行が全部見える(代わりに出力が長くなる)

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:

We Are The Alchemists, my friends!:bouquet:1
Elixirコミュニティは本当に優しくて温かい人たちばかり!
私が挫折せずにいられるのもこの恵まれた環境のおかげです。

まずは気軽に話しかけてみてください。2

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

  2. @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中!

16
1
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
1