Repository: phoenixframework/phoenix
Pull request #4363 (2021-07-06 02:08:56)

error in input tag usage


Patch:
diff --git a/guides/controllers.md b/guides/controllers.md
index d2e792c28..0d9164f10 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -311,7 +311,7 @@ end
 
 We would then need to provide an `index.xml.eex` template which created valid XML, and we would be done.
 
-For a list of valid content mime-types, please see the [mime.types](https://github.com/elixir-plug/mime/blob/master/priv/mime.types) documentation from the mime type library.
+For a list of valid content mime-types, please see the [mime.types](https://github.com/elixir-plug/mime/blob/master/lib/mime.ex) documentation from the mime type library.
 
 ### Setting the HTTP Status
 
diff --git a/guides/realtime/channels.md b/guides/realtime/channels.md
index 769e0033d..eb58c5ff3 100644
--- a/guides/realtime/channels.md
+++ b/guides/realtime/channels.md
@@ -270,7 +270,7 @@ In `lib/hello_web/templates/page/index.html.eex`, we'll replace the existing cod
 
 ```html
 <div id="messages" role="log" aria-live="polite"></div>
-<input id="chat-input" type="text"></input>
+<input id="chat-input" type="text">
 ```
 
 Now let's add a couple of event listeners to `assets/js/socket.js`:
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 28a40d279..af1640619 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -21,8 +21,13 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
   live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @mailer do %>
 
 # Configures the mailer.
-# Check https://hexdocs.pm/swoosh for different adapters.
-config :<%= @app_name %>, <%= @web_namespace %>.Mailer, adapter: Swoosh.Adapters.SMTP
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local
 
 # Swoosh API client is needed for adapters other than SMTP.
 config :swoosh, :api_client, false<% end %>
diff --git a/installer/templates/phx_single/config/dev.exs b/installer/templates/phx_single/config/dev.exs
index 03568daff..269007684 100644
--- a/installer/templates/phx_single/config/dev.exs
+++ b/installer/templates/phx_single/config/dev.exs
@@ -57,13 +57,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
       ~r"lib/<%= @lib_web_name %>/(live|views)/.*(ex)$",
       ~r"lib/<%= @lib_web_name %>/templates/.*(eex)$"
     ]
-  ]<% end %><%= if @mailer do %>
-
-# By default, emails will not be sent in localhost.
-# Instead they are available at "/dev/mailbox".
-# Check the application router for details.
-config :<%= @app_name %>, <%= @web_namespace %>.Mailer,
-  adapter: Swoosh.Adapters.Local<% end %>
+  ]<% end %>
 
 # Do not include metadata nor timestamps in development logs
 config :logger, :console, format: "[$level] $message\n"
diff --git a/installer/templates/phx_single/config/runtime.exs b/installer/templates/phx_single/config/runtime.exs
index a0e59cdcf..d381c8235 100644
--- a/installer/templates/phx_single/config/runtime.exs
+++ b/installer/templates/phx_single/config/runtime.exs
@@ -33,5 +33,23 @@ if config_env() == :prod do
   #     config :<%= @web_app_name %>, <%= @endpoint_module %>, server: true
   #
   # Then you can assemble a release by calling `mix release`.
-  # See `mix help release` for more information.
+  # See `mix help release` for more information.<%= if @mailer do %>
+
+  # ## Configuring the mailer
+  #
+  # In production you need to configure the mailer to use a different adapter.
+  # Also, you may need to configure the Swoosh API client of your choice if you
+  # are not using SMTP. Here is an example of the configuration:
+  #
+  #     config :<%= @app_name %>, <%= @app_module %>.Mailer,
+  #       adapter: Swoosh.Adapters.Mailgun,
+  #       api_key: System.get_env("MAILGUN_API_KEY"),
+  #       domain: System.get_env("MAILGUN_DOMAIN")
+  #
+  # For this example you need include a HTTP client required by Swoosh API client.
+  # Swoosh supports Hackney and Finch out of the box:
+  #
+  #     config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+  #
+  # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.<% end %>
 end
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/config.exs b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
index 6d114e990..18d7962a5 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
@@ -4,8 +4,13 @@ config :<%= @app_name %><%= if @namespaced? do %>,
   ecto_repos: [<%= @app_module %>.Repo]<% end %><% end %><%= if @mailer do %>
 
 # Configures the mailer.
-# Check https://hexdocs.pm/swoosh for different adapters.
-config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.SMTP
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local
 
 # Swoosh API client is needed for adapters other than SMTP.
 config :swoosh, :api_client, false<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
index 4a15639ae..0a3fcd2ab 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
@@ -22,4 +22,22 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
 #     config :<%= @web_app_name %>, <%= @endpoint_module %>, server: true
 #
 # Then you can assemble a release by calling `mix release`.
-# See `mix help release` for more information.
+# See `mix help release` for more information.<%= if @mailer do %>
+
+# ## Configuring the mailer
+#
+# In production you need to configure the mailer to use a different adapter.
+# Also, you may need to configure the Swoosh API client of your choice if you
+# are not using SMTP. Here is an example of the configuration:
+#
+#     config :<%= @app_name %>, <%= @app_module %>.Mailer,
+#       adapter: Swoosh.Adapters.Mailgun,
+#       api_key: System.get_env("MAILGUN_API_KEY"),
+#       domain: System.get_env("MAILGUN_DOMAIN")
+#
+# For this example you need include a HTTP client required by Swoosh API client.
+# Swoosh supports Hackney and Finch out of the box:
+#
+#     config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+#
+# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.<% end %>
diff --git a/installer/templates/phx_umbrella/config/dev.exs b/installer/templates/phx_umbrella/config/dev.exs
index 2126e4bc2..04bd9c0b4 100644
--- a/installer/templates/phx_umbrella/config/dev.exs
+++ b/installer/templates/phx_umbrella/config/dev.exs
@@ -8,10 +8,4 @@ config :phoenix, :plug_init_mode, :runtime
 
 # Set a higher stacktrace during development. Avoid configuring such
 # in production as building large stacktraces may be expensive.
-config :phoenix, :stacktrace_depth, 20<%= if @mailer do %>
-
-# By default, emails will not be sent in localhost.
-# Instead they are available at "/dev/mailbox".
-# Check the application router for details.
-config :<%= @app_name %>, <%= @app_module %>.Mailer,
-  adapter: Swoosh.Adapters.Local<% end %>
+config :phoenix, :stacktrace_depth, 20
diff --git a/lib/mix/phoenix/context.ex b/lib/mix/phoenix/context.ex
index 97a5dbfe4..27fbc6562 100644
--- a/lib/mix/phoenix/context.ex
+++ b/lib/mix/phoenix/context.ex
@@ -22,6 +22,10 @@ defmodule Mix.Phoenix.Context do
     context =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
   end
 
+  def new(context_name, opts) do
+    new(context_name, %Schema{}, opts)
+  end
+
   def new(context_name, %Schema{} = schema, opts) do
     ctx_app   = opts[:context_app] || Mix.Phoenix.context_app()
     base      = Module.concat([Mix.Phoenix.context_base(ctx_app)])
diff --git a/lib/mix/tasks/phx.gen.notifier.ex b/lib/mix/tasks/phx.gen.notifier.ex
new file mode 100644
index 000000000..3f68d735b
--- /dev/null
+++ b/lib/mix/tasks/phx.gen.notifier.ex
@@ -0,0 +1,167 @@
+defmodule Mix.Tasks.Phx.Gen.Notifier do
+  @shortdoc "Generates a notifier that delivers emails by default"
+
+  @moduledoc """
+  Generates a notifier that delivers emails by default.
+
+      mix phx.gen.notifier Accounts User welcome_user reset_password confirmation_instructions
+
+  This task expects a context module name, followed by a
+  notifier name and one or more message names. Messages
+  are the functions that will be created prefixed by "deliver",
+  so the message name should be "snake_case" without punctuation.
+
+  Note that this task expects that you have a mailer defined
+  as "YourApp.Mailer". Check the `mix phx.new` task for more details.
+  """
+
+  use Mix.Task
+
+  @switches [
+    context: :boolean,
+    context_app: :string,
+    prefix: :string
+  ]
+
+  @default_opts [context: true]
+
+  alias Mix.Phoenix.Context
+
+  @doc false
+  def run(args) do
+    if Mix.Project.umbrella?() do
+      Mix.raise(
+        "mix phx.gen.notifier must be invoked from within your *_web application root directory"
+      )
+    end
+
+    {context, notifier_module, messages} = build(args)
+
+    inflections = Mix.Phoenix.inflect(notifier_module)
+
+    binding = [
+      context: context,
+      inflections: inflections,
+      notifier_messages: messages
+    ]
+
+    paths = Mix.Phoenix.generator_paths()
+
+    prompt_for_conflicts(context)
+
+    copy_new_files(context, binding, paths)
+  end
+
+  @doc false
+  def build(args, help \\ __MODULE__) do
+    {opts, parsed, _} = parse_opts(args)
+    [context_name, notifier_name | notifier_messages] = validate_args!(parsed, help)
+
+    notifier_module = inspect(Module.concat(context_name, "#{notifier_name}Notifier"))
+    context = Context.new(notifier_module, opts)
+
+    {context, notifier_module, notifier_messages}
+  end
+
+  defp parse_opts(args) do
+    {opts, parsed, invalid} = OptionParser.parse(args, switches: @switches)
+
+    merged_opts =
+      @default_opts
+      |> Keyword.merge(opts)
+      |> put_context_app(opts[:context_app])
+
+    {merged_opts, parsed, invalid}
+  end
+
+  defp put_context_app(opts, nil), do: opts
+
+  defp put_context_app(opts, string) do
+    Keyword.put(opts, :context_app, String.to_atom(string))
+  end
+
+  defp validate_args!([context, notifier | messages] = args, help) do
+    cond do
+      not Context.valid?(context) ->
+        help.raise_with_help(
+          "Expected the context, #{inspect(context)}, to be a valid module name"
+        )
+
+      not valid_notifier?(notifier) ->
+        help.raise_with_help(
+          "Expected the notifier, #{inspect(notifier)}, to be a valid module name"
+        )
+
+      context == Mix.Phoenix.base() ->
+        help.raise_with_help(
+          "Cannot generate context #{context} because it has the same name as the application"
+        )
+
+      notifier == Mix.Phoenix.base() ->
+        help.raise_with_help(
+          "Cannot generate notifier #{notifier} because it has the same name as the application"
+        )
+
+      Enum.any?(messages, &(!valid_message?(&1))) ->
+        help.raise_with_help(
+          "Cannot generate notifier #{inspect(notifier)} because one of the messages is invalid: #{Enum.map_join(messages, ", ", &inspect/1)}"
+        )
+
+      true ->
+        args
+    end
+  end
+
+  defp validate_args!(_, help) do
+    help.raise_with_help("Invalid arguments")
+  end
+
+  defp valid_notifier?(notifier) do
+    notifier =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
+  end
+
+  defp valid_message?(message_name) do
+    message_name =~ ~r/^[a-z]+(\_[a-z0-9]+)*$/
+  end
+
+  @doc false
+  @spec raise_with_help(String.t()) :: no_return()
+  def raise_with_help(msg) do
+    Mix.raise("""
+    #{msg}
+
+    mix phx.gen.notifier expects a context module name, followed by a
+    notifier name and one or more message names. Messages are the
+    functions that will be created prefixed by "deliver", so the message
+    name should be "snake_case" without punctuation.
+    For example:
+
+        mix phx.gen.notifier Accounts User welcome reset_password
+
+    In this example the notifier will be called `UserNotifier` inside
+    the Accounts context. The functions `deliver_welcome/1` and
+    `reset_password/1` will be created inside this notifier.
+    """)
+  end
+
+  defp copy_new_files(%Context{} = context, binding, paths) do
+    files = files_to_be_generated(context)
+
+    Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.notifier", binding, files)
+
+    context
+  end
+
+  defp files_to_be_generated(%Context{} = context) do
+    [
+      {:eex, "notifier.ex", context.file},
+      {:eex, "notifier_test.exs", context.test_file}
+    ]
+  end
+
+  defp prompt_for_conflicts(context) do
+    context
+    |> files_to_be_generated()
+    |> Mix.Phoenix.prompt_for_conflicts()
+  end
+end
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index cfbb56264..d63ada8ce 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -15,7 +15,7 @@ defmodule Phoenix.CodeReloader do
   This is configured in your application environment like:
 
       config :your_app, YourAppWeb.Endpoint,
-        reloadable_compilers: [:gettext, :phoenix, :elixir],
+        reloadable_compilers: [:gettext, :elixir],
         reloadable_apps: [:ui, :backend]
 
   Keep in mind `:reloadable_compilers` must be a subset of the
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 496a6e5fb..137f5e0c3 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -770,18 +770,19 @@ defmodule Phoenix.Controller do
       Map.get(conn.private, :phoenix_view) ||
         raise "a view module was not specified, set one with put_view/2"
 
-    conn = prepare_assigns(conn, assigns, template, format)
-    data = render_with_layouts(conn, view, template, format)
+    layout_format? = format in layout_formats(conn)
+    conn = prepare_assigns(conn, assigns, template, format, layout_format?)
+    data = render_with_layouts(conn, view, template, format, layout_format?)
 
     conn
     |> ensure_resp_content_type(MIME.type(format))
     |> send_resp(conn.status || 200, data)
   end
 
-  defp render_with_layouts(conn, view, template, format) do
+  defp render_with_layouts(conn, view, template, format, layout_format?) do
     render_assigns = Map.put(conn.assigns, :conn, conn)
 
-    case root_layout(conn) do
+    case layout_format? and root_layout(conn) do
       {layout_mod, layout_tpl} ->
         inner = Phoenix.View.render(view, template, render_assigns)
         root_assigns = render_assigns |> Map.put(:inner_content, inner) |> Map.delete(:layout)
@@ -792,11 +793,11 @@ defmodule Phoenix.Controller do
     end
   end
 
-  defp prepare_assigns(conn, assigns, template, format) do
+  defp prepare_assigns(conn, assigns, template, format, layout_format?) do
     assigns = to_map(assigns)
 
     layout =
-      case layout(conn, assigns, format) do
+      case layout_format? and assigns_layout(conn, assigns) do
         {mod, layout} -> {mod, template_name(layout, format)}
         false -> false
       end
@@ -810,14 +811,10 @@ defmodule Phoenix.Controller do
     end)
   end
 
-  defp layout(conn, assigns, format) do
-    if format in layout_formats(conn) do
-      case Map.fetch(assigns, :layout) do
-        {:ok, layout} -> layout
-        :error -> layout(conn)
-      end
-    else
-      false
+  defp assigns_layout(conn, assigns) do
+    case Map.fetch(assigns, :layout) do
+      {:ok, layout} -> layout
+      :error -> layout(conn)
     end
   end
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 4fd7e8a37..be4264e5c 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -162,15 +162,24 @@ defmodule Phoenix.Endpoint do
       the "watch" mode of the webpack build tool when the server starts.
       You can configure it to whatever build tool or command you want:
 
-          [node: ["node_modules/webpack/bin/webpack.js", "--mode", "development",
-              "--watch", "--watch-options-stdin"]]
+          [
+            node: [
+              "node_modules/webpack/bin/webpack.js",
+              "--mode",
+              "development",
+              "--watch",
+              "--watch-options-stdin"
+            ]
+          ]
+
+      The `:cd` and `:env` options can be given at the end of the list to customize
+      the watcher:
+
+          [node: [..., cd: "assets"]]
 
-      The `:cd` option can be used on a watcher to override the folder from
-      which the watcher will run. By default this will be the project's root:
-      `File.cwd!()`
+      A watcher can also be a module-function-args tuple that will be invoked accordingly:
 
-          [node: ["node_modules/webpack/bin/webpack.js", "--mode", "development",
-              "--watch", "--watch-options-stdin", cd: "my_frontend"]]
+          [another: {Mod, :fun, [arg1, arg2]}]
 
     * `:live_reload` - configuration for the live reload option.
       Configuration requires a `:patterns` option which should be a list of
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 03a486c91..ec2a638e7 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -156,19 +156,12 @@ defmodule Phoenix.Endpoint.Supervisor do
 
   defp watcher_children(_mod, conf, server?) do
     if server? do
-      Enum.map(conf[:watchers], fn {cmd, args} ->
-        {Phoenix.Endpoint.Watcher, watcher_args(cmd, args)}
-      end)
+      Enum.map(conf[:watchers], &{Phoenix.Endpoint.Watcher, &1})
     else
       []
     end
   end
 
-  defp watcher_args(cmd, cmd_args) do
-    {args, opts} = Enum.split_while(cmd_args, &is_binary(&1))
-    {cmd, args, opts}
-  end
-
   @doc """
   The endpoint configuration used at compile time.
   """
@@ -206,7 +199,7 @@ defmodule Phoenix.Endpoint.Supervisor do
      http: false,
      https: false,
      reloadable_apps: nil,
-     reloadable_compilers: [:gettext, :phoenix, :elixir],
+     reloadable_compilers: [:gettext, :elixir],
      secret_key_base: nil,
      static_url: nil,
      url: [host: "localhost", path: "/"],
diff --git a/lib/phoenix/endpoint/watcher.ex b/lib/phoenix/endpoint/watcher.ex
index 5e9cb850f..8d13fb8d1 100644
--- a/lib/phoenix/endpoint/watcher.ex
+++ b/lib/phoenix/endpoint/watcher.ex
@@ -10,25 +10,44 @@ defmodule Phoenix.Endpoint.Watcher do
     }
   end
 
-  def start_link({cmd, args, opts}) do
-    Task.start_link(__MODULE__, :watch, [to_string(cmd), args, opts])
+  def start_link({cmd, args}) do
+    Task.start_link(__MODULE__, :watch, [to_string(cmd), args])
   end
 
-  def watch(cmd, args, opts) do
-    merged_opts = Keyword.merge(
-      [into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts)
-    :ok = validate(cmd, args, merged_opts)
+  def watch(_cmd, {mod, fun, args}) do
+    try do
+      apply(mod, fun, args)
+    catch
+      kind, reason ->
+        # The function returned a non-zero exit code.
+        # Sleep for a couple seconds before exiting to
+        # ensure this doesn't hit the supervisor's
+        # max_restarts/max_seconds limit.
+        Process.sleep(2000)
+        :erlang.raise(kind, reason, __STACKTRACE__)
+    end
+  end
+
+  def watch(cmd, args) when is_list(args) do
+    {args, opts} = Enum.split_while(args, &is_binary(&1))
+    opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts)
+    :ok = validate(cmd, args, opts)
 
     try do
-      System.cmd(cmd, args, merged_opts)
+      System.cmd(cmd, args, opts)
     catch
       :error, :enoent ->
         relative = Path.relative_to_cwd(cmd)
-        Logger.error "Could not start watcher #{inspect relative} from #{inspect cd(merged_opts)}, executable does not exist"
+
+        Logger.error(
+          "Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist"
+        )
+
         exit(:shutdown)
     else
       {_, 0} ->
         :ok
+
       {_, _} ->
         # System.cmd returned a non-zero exit code
         # sleep for a couple seconds before exiting to ensure this doesn't
@@ -40,23 +59,30 @@ defmodule Phoenix.Endpoint.Watcher do
 
   # We specially handle Node.js to make sure we
   # provide a good getting started experience.
-  defp validate("node", [script|_], merged_opts) do
+  defp validate("node", [script | _], merged_opts) do
     script_path = Path.expand(script, cd(merged_opts))
 
     cond do
       !System.find_executable("node") ->
-        Logger.error "Could not start watcher because \"node\" is not available. Your Phoenix " <>
-                     "application is still running, however assets won't be compiled. " <>
-                     "You may fix this by installing \"node\" and then running \"npm install\" inside the \"assets\" directory."
+        Logger.error(
+          "Could not start watcher because \"node\" is not available. Your Phoenix " <>
+            "application is still running, however assets won't be compiled. " <>
+            "You may fix this by installing \"node\" and then running \"npm install\" inside the \"assets\" directory."
+        )
+
         exit(:shutdown)
 
       not File.exists?(script_path) ->
-        Logger.error "Could not start Node.js watcher because script #{inspect script_path} does not " <>
-                     "exist. Your Phoenix application is still running, however assets " <>
-                     "won't be compiled. You may fix this by running \"npm install\" inside the \"assets\" directory."
+        Logger.error(
+          "Could not start Node.js watcher because script #{inspect(script_path)} does not " <>
+            "exist. Your Phoenix application is still running, however assets " <>
+            "won't be compiled. You may fix this by running \"npm install\" inside the \"assets\" directory."
+        )
+
         exit(:shutdown)
 
-      true -> :ok
+      true ->
+        :ok
     end
   end
 
diff --git a/priv/templates/phx.gen.notifier/notifier.ex b/priv/templates/phx.gen.notifier/notifier.ex
new file mode 100644
index 000000000..7a2cb2a52
--- /dev/null
+++ b/priv/templates/phx.gen.notifier/notifier.ex
@@ -0,0 +1,14 @@
+defmodule <%= inspect context.module %> do
+  import Swoosh.Email
+  alias <%= inspect context.base_module %>.Mailer<%= for message <- notifier_messages do %>
+
+  def deliver_<%= message %>(%{name: name, email: email}) do
+    new()
+    |> to({name, email})
+    |> from({"Phoenix Team", "team@example.com"})
+    |> subject("Welcome to Phoenix, #{name}!")
+    |> html_body("<h1>Hello, #{name}</h1>")
+    |> text_body("Hello, #{name}\n")
+    |> Mailer.deliver()
+  end<% end %>
+end


Test patch:
diff --git a/installer/templates/phx_single/config/test.exs b/installer/templates/phx_single/config/test.exs
index c7b86e2d2..914e7fa77 100644
--- a/installer/templates/phx_single/config/test.exs
+++ b/installer/templates/phx_single/config/test.exs
@@ -7,7 +7,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
   server: false<%= if @mailer do %>
 
 # In test we don't send emails.
-config :<%= @app_name %>, <%= @web_namespace %>.Mailer,
+config :<%= @app_name %>, <%= @app_module %>.Mailer,
   adapter: Swoosh.Adapters.Test<% end %>
 
 # Print only warnings and errors during test
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index e9eb557ab..e34f47ef1 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -203,15 +203,11 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       assert_file "phx_blog/config/config.exs", fn file ->
         assert file =~ "config :swoosh"
-        assert file =~ "config :phx_blog, PhxBlogWeb.Mailer, adapter: Swoosh.Adapters.SMTP"
+        assert file =~ "config :phx_blog, PhxBlog.Mailer, adapter: Swoosh.Adapters.Local"
       end
 
       assert_file "phx_blog/config/test.exs", fn file ->
-        assert file =~ "config :phx_blog, PhxBlogWeb.Mailer, adapter: Swoosh.Adapters.Test"
-      end
-
-      assert_file "phx_blog/config/dev.exs", fn file ->
-        assert file =~ "config :phx_blog, PhxBlogWeb.Mailer, adapter: Swoosh.Adapters.Local"
+        assert file =~ "config :phx_blog, PhxBlog.Mailer, adapter: Swoosh.Adapters.Test"
       end
 
       # Install dependencies?
@@ -340,7 +336,7 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       assert_file "phx_blog/config/config.exs", fn file ->
         refute file =~ "config :swoosh"
-        refute file =~ "config :phx_blog, PhxBlogWeb.Mailer, adapter: Swoosh.Adapters.SMTP"
+        refute file =~ "config :phx_blog, PhxBlog.Mailer, adapter: Swoosh.Adapters.Local"
       end
     end
   end
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index f02e4df0a..b7d8e9974 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -216,17 +216,13 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file root_path(@app, "config/config.exs"), fn file ->
         assert file =~ "config :swoosh"
-        assert file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.SMTP"
+        assert file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Local"
       end
 
       assert_file root_path(@app, "config/test.exs"), fn file ->
         assert file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Test"
       end
 
-      assert_file root_path(@app, "config/dev.exs"), fn file ->
-        assert file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Local"
-      end
-
       # Install dependencies?
       assert_received {:mix_shell, :yes?, ["\nFetch and install dependencies?"]}
 
@@ -335,16 +331,12 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file root_path(@app, "config/config.exs"), fn file ->
         refute file =~ "config :swoosh"
-        refute file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.SMTP"
+        refute file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Local"
       end
 
       assert_file root_path(@app, "config/test.exs"), fn file ->
         refute file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Test"
       end
-
-      assert_file root_path(@app, "config/dev.exs"), fn file ->
-        refute file =~ "config :phx_umb, PhxUmb.Mailer, adapter: Swoosh.Adapters.Local"
-      end
     end
   end
 
diff --git a/priv/templates/phx.gen.notifier/notifier_test.exs b/priv/templates/phx.gen.notifier/notifier_test.exs
new file mode 100644
index 000000000..14a76a4b3
--- /dev/null
+++ b/priv/templates/phx.gen.notifier/notifier_test.exs
@@ -0,0 +1,18 @@
+defmodule <%= inspect context.module %>Test do
+  use ExUnit.Case, async: true
+  import Swoosh.TestAssertions
+
+  alias <%= inspect context.module %>.<%= inflections[:alias] %>Notifier<%= for message <- notifier_messages do %>
+
+  test "deliver_<%= message %>/1" do
+    user = %{name: "Alice", email: "alice@example.com"}
+
+    <%= inflections[:alias] %>Notifier.deliver_<%= message %>(user)
+
+    assert_email_sent(
+      subject: "Welcome to Phoenix, Alice!",
+      to: {"Alice", "alice@example.com"},
+      text_body: ~r/Hello, Alice/
+    )
+  end<% end %>
+end
diff --git a/test/mix/tasks/phx.gen.notifier_test.exs b/test/mix/tasks/phx.gen.notifier_test.exs
new file mode 100644
index 000000000..a0fa1ddbf
--- /dev/null
+++ b/test/mix/tasks/phx.gen.notifier_test.exs
@@ -0,0 +1,129 @@
+Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
+
+defmodule Mix.Tasks.Phx.Gen.NotifierTest do
+  use ExUnit.Case
+  import MixHelper
+  alias Mix.Tasks.Phx.Gen
+
+  setup do
+    Mix.Task.clear()
+    :ok
+  end
+
+  test "new notifier", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Notifier.run(~w(Accounts User welcome_user reset_password))
+
+      assert_file("lib/phoenix/accounts/user_notifier.ex", fn file ->
+        assert file =~ ~S|defmodule Phoenix.Accounts.UserNotifier do|
+        assert file =~ ~S|import Swoosh.Email|
+        assert file =~ ~S|alias Phoenix.Mailer|
+
+        assert file =~ ~S|def deliver_welcome_user(%{name: name, email: email}) do|
+        assert file =~ ~S|from({"Phoenix Team", "team@example.com"})|
+
+        assert file =~ ~S|def deliver_reset_password(%{name: name, email: email}) do|
+        assert file =~ ~S|from({"Phoenix Team", "team@example.com"})|
+
+        assert file =~ ~S|Mailer.deliver()|
+      end)
+
+      assert_file("test/phoenix/accounts/user_notifier_test.exs", fn file ->
+        assert file =~ ~S|defmodule Phoenix.Accounts.UserNotifierTest do|
+      end)
+
+      send(self(), {:mix_shell_input, :yes?, true})
+      send(self(), {:mix_shell_input, :yes?, true})
+      send(self(), {:mix_shell_input, :yes?, true})
+
+      Gen.Notifier.run(~w(Accounts User account_confirmation))
+
+      assert_received {:mix_shell, :info,
+                       ["The following files conflict with new files to be generated:" <> notice]}
+
+      assert notice =~ "user_notifier.ex"
+      assert notice =~ "user_notifier_test.exs"
+
+      assert_received {:mix_shell, :yes?, [question]}
+
+      assert question =~ "Proceed with interactive overwrite?"
+
+      assert_file("lib/phoenix/accounts/user_notifier.ex", fn file ->
+        assert file =~ ~S|defmodule Phoenix.Accounts.UserNotifier do|
+        assert file =~ ~S|import Swoosh.Email|
+        assert file =~ ~S|alias Phoenix.Mailer|
+
+        assert file =~ ~S|def deliver_account_confirmation(%{name: name, email: email}) do|
+        assert file =~ ~S|from({"Phoenix Team", "team@example.com"})|
+
+        refute file =~ ~S|def deliver_welcome_user(%{name: name, email: email}) do|
+        refute file =~ ~S|def deliver_reset_password(%{name: name, email: email}) do|
+      end)
+    end)
+  end
+
+  test "generates nested notifier", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Notifier.run(~w(Admin.Accounts User welcome_user reset_password))
+
+      assert_file("lib/phoenix/admin/accounts/user_notifier.ex", fn file ->
+        assert file =~ ~S|defmodule Phoenix.Admin.Accounts.UserNotifier do|
+        assert file =~ ~S|import Swoosh.Email|
+        assert file =~ ~S|alias Phoenix.Mailer|
+
+        assert file =~ ~S|def deliver_welcome_user(%{name: name, email: email}) do|
+        assert file =~ ~S|from({"Phoenix Team", "team@example.com"})|
+
+        assert file =~ ~S|def deliver_reset_password(%{name: name, email: email}) do|
+        assert file =~ ~S|from({"Phoenix Team", "team@example.com"})|
+
+        assert file =~ ~S|Mailer.deliver()|
+      end)
+    end)
+  end
+
+  test "in an umbrella with a context_app, generates the notifier", config do
+    in_tmp_umbrella_project(config.test, fn ->
+      Application.put_env(:phoenix, :generators, context_app: {:another_app, "another_app"})
+      Gen.Notifier.run(~w(Accounts User welcome_user reset_password))
+
+      assert_file("another_app/lib/another_app/accounts/user_notifier.ex", fn file ->
+        assert file =~ ~S|defmodule AnotherApp.Accounts.UserNotifier do|
+        assert file =~ ~S|import Swoosh.Email|
+        assert file =~ ~S|alias AnotherApp.Mailer|
+
+        assert file =~ ~S|Mailer.deliver()|
+      end)
+    end)
+  end
+
+  test "invalid mix arguments", config do
+    in_tmp_project(config.test, fn ->
+      assert_raise Mix.Error, ~r/Expected the context, "blog", to be a valid module name/, fn ->
+        Gen.Notifier.run(~w(blog Post new_post))
+      end
+
+      assert_raise Mix.Error, ~r/Expected the notifier, "posts", to be a valid module name/, fn ->
+        Gen.Notifier.run(~w(Post posts new_post))
+      end
+
+      assert_raise Mix.Error,
+                   ~r/Cannot generate context Phoenix because it has the same name as the application/,
+                   fn ->
+                     Gen.Notifier.run(~w(Phoenix Post new_blog_post))
+                   end
+
+      assert_raise Mix.Error,
+                   ~r/Cannot generate notifier Phoenix because it has the same name as the application/,
+                   fn ->
+                     Gen.Notifier.run(~w(Blog Phoenix new_blog_post))
+                   end
+
+      assert_raise Mix.Error,
+                   ~r/Cannot generate notifier "Post" because one of the messages is invalid: "NewPost"/,
+                   fn ->
+                     Gen.Notifier.run(~w(Blog Post NewPost))
+                   end
+    end)
+  end
+end
diff --git a/test/phoenix/controller/render_test.exs b/test/phoenix/controller/render_test.exs
index b1c0d23e1..d692c2356 100644
--- a/test/phoenix/controller/render_test.exs
+++ b/test/phoenix/controller/render_test.exs
@@ -52,6 +52,19 @@ defmodule Phoenix.Controller.RenderTest do
     assert html_response?(conn)
   end
 
+  test "renders string template without put_root_layout based on layout_formats" do
+    conn =
+      conn()
+      |> put_layout_formats(["not_html"])
+      |> put_layout({MyApp.LayoutView, "app.html"})
+      |> put_root_layout({MyApp.LayoutView, "root.html"})
+      |> render("index.html", title: "Hello")
+
+    refute conn.resp_body =~ "ROOTSTART"
+    refute conn.resp_body =~ "<html>"
+    assert html_response?(conn)
+  end
+
   test "renders atom template with put layout" do
     conn = put_format(layout_conn(), "html")
     conn = render(conn, :index, title: "Hello")
diff --git a/test/phoenix/endpoint/watcher_test.exs b/test/phoenix/endpoint/watcher_test.exs
index e1a85aefb..89b4fe3bc 100644
--- a/test/phoenix/endpoint/watcher_test.exs
+++ b/test/phoenix/endpoint/watcher_test.exs
@@ -4,9 +4,17 @@ defmodule Phoenix.Endpoint.WatcherTest do
   alias Phoenix.Endpoint.Watcher
   import ExUnit.CaptureIO
 
-  test "starts watching and writes to stdio" do
+  test "starts watching and writes to stdio with args" do
     assert capture_io(fn ->
-      {:ok, pid} = Watcher.start_link({"echo", ["hello"], cd: File.cwd!()})
+      {:ok, pid} = Watcher.start_link({"echo", ["hello", cd: File.cwd!()]})
+      ref = Process.monitor(pid)
+      assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
+    end) == "hello\n"
+  end
+
+  test "starts watching and writes to stdio with fun" do
+    assert capture_io(fn ->
+      {:ok, pid} = Watcher.start_link({"echo", {IO, :puts, ["hello"]}})
       ref = Process.monitor(pid)
       assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
     end) == "hello\n"