Repository: phoenixframework/phoenix
Pull request #5432 (2023-04-21 16:10:38)

Update testing channels guide - to match a phoenix 1.7.2 project
* update listings
* replace MyAppWeb with HelloWeb
* remove listing for handle_out/3 since it is not defined in generated HelloWeb.RoomChannel


Patch:
diff --git a/guides/contexts.md b/guides/contexts.md
index a7c6a8064..7ce19df4e 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -696,7 +696,7 @@ Now that our cart is associated to the items we place in it, let's set up the ca
   end
 ```
 
-First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need. An isolated context API with the bare minimum knowledge necessary to reference a product in our system. Next, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
+First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need: an isolated context API with the bare minimum knowledge necessary to reference a product in our system. Next, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
 
 With our schemas in place, we can start integrating the new data structures and `ShoppingCart` context APIs into our web-facing features.
 
@@ -711,7 +711,7 @@ We won't focus on a real user authentication system at this point, but by the ti
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
 +   plug :fetch_current_user
@@ -1040,9 +1040,9 @@ Let's head back to the browser and try it out. Add a few products to your cart,
 
 ## Adding an Orders context
 
-With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all the user has to add items in order to purchase any products, but should the order checkout process be grouped here?
+With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all, the user has to add items in order to purchase any products – but should the order checkout process be grouped here?
 
-If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items *at payment transaction time*. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see ordering can reasonably stand on its own with its own data concerns and business rules.
+If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items *at payment transaction time*. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see that ordering can stand on its own with its own data concerns and business rules.
 
 Naming wise, `Orders` clearly defines the scope of our context, so let's get started by again taking advantage of the context generators. Run the following command in your console:
 
@@ -1063,7 +1063,7 @@ Remember to update your repository by running migrations:
     $ mix ecto.migrate
 ```
 
-We generated a `Orders` context. We added a `user_uuid` field to associate our placeholder current user to an order, along with a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:
+We generated an `Orders` context. We added a `user_uuid` field to associate our placeholder current user to an order, along with a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:
 
 ```elixir
   def change do
@@ -1220,7 +1220,7 @@ end
 
 We wrote the `create` action to call an as-yet-implemented `Orders.complete_order/1` function. Our code is technically "creating" an order, but it's important to step back and consider the naming of your interfaces. The act of *completing* an order is extremely important in our system. Money changes hands in a transaction, physical goods could be automatically shipped, etc. Such an operation deserves a better, more obvious function name, such as `complete_order`. If the order is completed successfully we redirect to the show page, otherwise a flash error is shown as we redirect back to the cart page.
 
-Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be specially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).
+Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be especially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).
 
 Now we can implement our `Orders.complete_order/1` function. To complete an order, our job will require a few operations:
 
@@ -1262,7 +1262,7 @@ From our requirements alone, we can start to see why a generic `create_order` fu
   end
 ```
 
-We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product *at payment transaction time*, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we can again make use of `Ecto.Multi` to execute our operations in a database transaction. We start by inserting the order, followed by a `run` operation. The `Ecto.Multi.run/3` function allows us to run any code in the function which must either succeed with `{:ok, result}` or error, which halts and rolls back the transaction. Here, we simply can call into our shopping cart context and ask it to prune all items in a cart. Running the transaction will execute the multi as before and we return the result to the caller.
+We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product *at payment transaction time*, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we can again make use of `Ecto.Multi` to execute our operations in a database transaction. We start by inserting the order, followed by a `run` operation. The `Ecto.Multi.run/3` function allows us to run any code in the function which must either succeed with `{:ok, result}` or error, which halts and rolls back the transaction. Here, we simply call into our shopping cart context and ask it to prune all items in a cart. Running the transaction will execute the multi as before and we return the result to the caller.
 
 To close out our order completion, we need to implement the `ShoppingCart.prune_cart_items/1` function in `lib/hello/shopping_cart.ex`:
 
@@ -1356,6 +1356,6 @@ As we explored the context API, you might have wondered:
 
 Although Changesets are part of Ecto, they are not tied to the database, and they can be used to map data from and to any source, which makes it a general and useful data structure for tracking field changes, perform validations, and generate error messages.
 
-For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer. Regardless if you are talking to an API or the database.
+For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer - regardless if you are talking to an API or the database.
 
 Finally, note that your controllers and views are not hardcoded to work exclusively with Ecto either. Instead, Phoenix defines protocols such as `Phoenix.Param` and `Phoenix.HTML.FormData`, which allow any library to extend how Phoenix generates URL parameters or renders forms. Conveniently for us, the `phoenix_ecto` project implements those protocols, but you could as well bring your own data structures and implement them yourself.
diff --git a/guides/controllers.md b/guides/controllers.md
index 85df2c9a9..6ca334642 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -216,7 +216,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html", "json"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -380,7 +380,7 @@ The flash functionality is handy when mixed with redirects. Perhaps you want to
   def home(conn, _params) do
     conn
     |> put_flash(:error, "Let's pretend we have an error.")
-    |> redirect(to: ~p"/redirect_test"))
+    |> redirect(to: ~p"/redirect_test")
   end
 ```
 
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index b6f0b52a5..64f01a4d6 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -30,7 +30,7 @@ There are a number of places to connect with community members at all experience
 
   * [Phoenix in Action (by Geoffrey Lessel - 2019)](https://manning.com/books/phoenix-in-action)
 
-  * [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - 2017)](https://shankardevy.com/phoenix-book/)
+  * [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - 2017)](https://shankardevy.com/phoenix-book/). First book of the series Mastering Phoenix Framework is [free to read online](https://shankardevy.com/phoenix-inside-out-mpf/)
 
   * [Functional Web Development with Elixir, OTP, and Phoenix Rethink the Modern Web App (by Lance Halvorsen - 2017)](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/)
 
diff --git a/guides/plug.md b/guides/plug.md
index 469a9b0e5..97d525a9e 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -189,7 +189,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
     plug HelloWeb.Plugs.Locale, "en"
diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md
index 90d5d1a80..79a1e88a7 100644
--- a/guides/request_lifecycle.md
+++ b/guides/request_lifecycle.md
@@ -46,7 +46,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
diff --git a/guides/routing.md b/guides/routing.md
index b2690a591..856982a3c 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -16,7 +16,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -236,14 +236,14 @@ When building paths for nested routes, we will need to interpolate the IDs where
 ```elixir
 user_id = 42
 post_id = 17
-~p"/users/#{user_id}/#{post_id}"
+~p"/users/#{user_id}/posts/#{post_id}"
 "/users/42/posts/17"
 ```
 
 Verified routes also support the `Phoenix.Param` protocol, but we don't need to concern ourselves with elixir protocols just yet. Just know that once we start building our application with structs like `%User{}` and `%Post{}`, we'll be able to interpolate those data structures directly into our `~p` paths and phoenix will pluck out the correct fields to use in the route.
 
 ```elixir
-~p"/users/#{user}/#{post}"
+~p"/users/#{user}/posts/#{post}"
 "/users/42/posts/17"
 ```
 
@@ -457,7 +457,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -502,7 +502,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
diff --git a/installer/templates/phx_single/config/prod.exs b/installer/templates/phx_single/config/prod.exs
index f6e8c82ed..a9c197346 100644
--- a/installer/templates/phx_single/config/prod.exs
+++ b/installer/templates/phx_single/config/prod.exs
@@ -1,8 +1,4 @@
-import Config
-
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.<%= if @javascript or @css do %>
+import Config<%= if @javascript or @css do %>
 
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
index a3369183b..4acae1147 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
@@ -1,7 +1,3 @@
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.
-#
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
 # manifest is generated by the `mix phx.digest` task,
diff --git a/installer/templates/phx_web/router.ex b/installer/templates/phx_web/router.ex
index 2cc4b1e6b..9d49c3d86 100644
--- a/installer/templates/phx_web/router.ex
+++ b/installer/templates/phx_web/router.ex
@@ -5,7 +5,7 @@ defmodule <%= @web_namespace %>.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {<%= @web_namespace %>.Layouts, :root}
+    plug :put_root_layout, html: {<%= @web_namespace %>.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end<% end %>
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index f7dd9d93b..b4241dd8e 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -53,16 +53,18 @@ defmodule Phoenix.Endpoint.RenderErrors do
 
   @doc false
   def __catch__(conn, kind, reason, stack, opts) do
-    receive do
-      @already_sent ->
-        send(self(), @already_sent)
-        %Plug.Conn{conn | state: :sent}
-    after
-      0 ->
-        instrument_render_and_send(conn, kind, reason, stack, opts)
-    end
+    conn =
+      receive do
+        @already_sent ->
+          send(self(), @already_sent)
+          %Plug.Conn{conn | state: :sent}
+      after
+        0 ->
+          instrument_render_and_send(conn, kind, reason, stack, opts)
+      end
 
-    :erlang.raise(kind, reason, stack)
+    maybe_raise(kind, reason, stack)
+    conn
   end
 
   defp instrument_render_and_send(conn, kind, reason, stack, opts) do
@@ -91,6 +93,9 @@ defmodule Phoenix.Endpoint.RenderErrors do
   defp error_conn(_conn, :error, %NoRouteError{conn: conn}), do: conn
   defp error_conn(conn, _kind, _reason), do: conn
 
+  defp maybe_raise(:error, %NoRouteError{}, _stack), do: :ok
+  defp maybe_raise(kind, reason, stack), do: :erlang.raise(kind, reason, stack)
+
   ## Rendering
 
   @doc false
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 91174dca4..2d0618a1b 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -506,10 +506,15 @@ defmodule Phoenix.Router do
     {matches, {pipelines, _}} =
       Enum.map_reduce(routes_with_exprs, {[], %{}}, &build_match/2)
 
-    verifies =
+    routes_per_path =
       routes_with_exprs
       |> Enum.group_by(&elem(&1, 1).path, &elem(&1, 0))
-      |> Enum.map(&build_verify/1)
+
+    verifies =
+      routes_with_exprs
+      |> Enum.map(&elem(&1, 1).path)
+      |> Enum.uniq()
+      |> Enum.map(&build_verify(&1, routes_per_path))
 
     verify_catch_all =
       quote generated: true do
@@ -574,7 +579,9 @@ defmodule Phoenix.Router do
     end
   end
 
-  defp build_verify({path, routes}) do
+  defp build_verify(path, routes_per_path) do
+    routes = Map.get(routes_per_path, path)
+
     forward_plug =
       Enum.find_value(routes, fn
         %{kind: :forward, plug: plug} -> plug


Test patch:
diff --git a/guides/testing/testing_channels.md b/guides/testing/testing_channels.md
index 592b88b69..1692e3fba 100644
--- a/guides/testing/testing_channels.md
+++ b/guides/testing/testing_channels.md
@@ -85,7 +85,7 @@ setup do
   {:ok, _, socket} =
     HelloWeb.UserSocket
     |> socket("user_id", %{some: :assign})
-    |> subscribe_and_join(RoomChannel, "room:lobby")
+    |> subscribe_and_join(HelloWeb.RoomChannel, "room:lobby")
 
   %{socket: socket}
 end
@@ -106,7 +106,7 @@ test "ping replies with status ok", %{socket: socket} do
 end
 ```
 
-This tests the following code in our `MyAppWeb.RoomChannel`:
+This tests the following code in our `HelloWeb.RoomChannel`:
 
 ```elixir
 # Channels can be used in a request/response fashion
@@ -124,7 +124,7 @@ We emulate the client pushing a message to the channel with `push/3`. In the lin
 
 ### Testing a Broadcast
 
-It is common to receive messages from the client and broadcast to everyone subscribed to a current topic. This common pattern is simple to express in Phoenix and is one of the generated `handle_in/3` callbacks in our `MyAppWeb.RoomChannel`.
+It is common to receive messages from the client and broadcast to everyone subscribed to a current topic. This common pattern is simple to express in Phoenix and is one of the generated `handle_in/3` callbacks in our `HelloWeb.RoomChannel`.
 
 ```elixir
 def handle_in("shout", payload, socket) do
@@ -150,17 +150,10 @@ Since the `handle_in/3` callback for the `"shout"` event just broadcasts the sam
 
 ### Testing an asynchronous push from the server
 
-The last test in our `MyAppWeb.RoomChannelTest` verifies that broadcasts from the server are pushed to the client. Unlike the previous tests discussed, we are indirectly testing that our channel's `handle_out/3` callback is triggered. This `handle_out/3` is defined in our `MyApp.RoomChannel` as:
-
-```elixir
-def handle_out(event, payload, socket) do
-  push(socket, event, payload)
-  {:noreply, socket}
-end
-```
+The last test in our `HelloWeb.RoomChannelTest` verifies that broadcasts from the server are pushed to the client. Unlike the previous tests discussed, we are indirectly testing that the channel's `handle_out/3` callback is triggered. By default, `handle_out/3` is implemented for us and simply pushes the message on to the client.
 
 Since the `handle_out/3` event is only triggered when we call `broadcast/3` from our channel, we will need to emulate that in our test. We do that by calling `broadcast_from` or `broadcast_from!`. Both serve the same purpose with the only difference of `broadcast_from!` raising an error when broadcast fails.
 
-The line `broadcast_from!(socket, "broadcast", %{"some" => "data"})` will trigger our `handle_out/3` callback above which pushes the same event and payload back to the client. To test this, we do `assert_push "broadcast", %{"some" => "data"}`.
+The line `broadcast_from!(socket, "broadcast", %{"some" => "data"})` will trigger the `handle_out/3` callback which pushes the same event and payload back to the client. To test this, we do `assert_push "broadcast", %{"some" => "data"}`.
 
 That's it. Now you are ready to develop and fully test real-time applications. To learn more about other functionality provided when testing channels, check out the documentation for [`Phoenix.ChannelTest`](https://hexdocs.pm/phoenix/Phoenix.ChannelTest.html).
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 2e86d82b9..684c9c735 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -266,7 +266,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file(web_path(@app, "lib/phx_umb_web/router.ex"), fn file ->
         assert file =~ ~s[plug :fetch_live_flash]
-        assert file =~ ~s[plug :put_root_layout, {PhxUmbWeb.Layouts, :root}]
+        assert file =~ ~s[plug :put_root_layout, html: {PhxUmbWeb.Layouts, :root}]
         assert file =~ ~s[get "/", PageController]
       end)
 
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 64db87e55..7dbebd42c 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -98,7 +98,7 @@ defmodule Phoenix.Controller.ControllerTest do
     conn = conn(:get, "/")
     assert root_layout(conn) == false
 
-    conn = put_root_layout(conn, {AppView, "root.html"})
+    conn = put_root_layout(conn, html: {AppView, "root.html"})
     assert root_layout(conn) == {AppView, "root.html"}
 
     conn = put_root_layout(conn, "bare.html")
diff --git a/test/phoenix/controller/render_test.exs b/test/phoenix/controller/render_test.exs
index 8f9399f70..b47c4e614 100644
--- a/test/phoenix/controller/render_test.exs
+++ b/test/phoenix/controller/render_test.exs
@@ -45,7 +45,7 @@ defmodule Phoenix.Controller.RenderTest do
     conn =
       conn()
       |> put_layout({MyApp.LayoutView, "app.html"})
-      |> put_root_layout({MyApp.LayoutView, "root.html"})
+      |> put_root_layout(html: {MyApp.LayoutView, "root.html"})
       |> render("index.html", title: "Hello")
 
     assert conn.resp_body == "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
@@ -63,7 +63,7 @@ defmodule Phoenix.Controller.RenderTest do
     conn =
       conn()
       |> put_layout({MyApp.LayoutView, "app.html"})
-      |> put_root_layout({MyApp.LayoutView, :root})
+      |> put_root_layout(html: {MyApp.LayoutView, :root})
       |> render("index.html", title: "Hello")
 
     assert conn.resp_body == "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
diff --git a/test/phoenix/endpoint/render_errors_test.exs b/test/phoenix/endpoint/render_errors_test.exs
index ef2014ff6..cfd24fcb3 100644
--- a/test/phoenix/endpoint/render_errors_test.exs
+++ b/test/phoenix/endpoint/render_errors_test.exs
@@ -96,33 +96,21 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do
     assert_received {:plug_conn, :sent}
   end
 
-  test "call/2 is overridden with no route match as HTML" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown")
-                 end
+  test "call/2 is overridden with no route match as HTML and does not reraise" do
+    call(Router, :get, "/unknown")
 
     assert_received {:plug_conn, :sent}
   end
 
-  test "call/2 is overridden with no route match as JSON" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown?_format=json")
-                 end
+  test "call/2 is overridden with no route match as JSON and does not reraise" do
+    call(Router, :get, "/unknown?_format=json")
 
     assert_received {:plug_conn, :sent}
   end
 
   @tag :capture_log
-  test "call/2 is overridden with no route match while malformed format" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown?_format=unknown")
-                 end
+  test "call/2 is overridden with no route match while malformed format and does not reraise" do
+    call(Router, :get, "/unknown?_format=unknown")
 
     assert_received {:plug_conn, :sent}
   end
@@ -321,12 +309,8 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do
            end) =~ "Could not render errors due to unknown format \"unknown\""
   end
 
-  test "exception page for NoRouteError with plug_status 404" do
-    body =
-      assert_render(404, conn(:get, "/"), [], fn ->
-        raise Phoenix.Router.NoRouteError, conn: conn(:get, "/"), router: nil, plug_status: 404
-      end)
-
-    assert body == "Got 404 from error with GET"
+  test "exception page for NoRouteError with plug_status 404 renders and does not reraise" do
+    conn = call(Router, :get, "/unknown")
+    assert Phoenix.ConnTest.response(conn, 404) =~ "Got 404 from error with GET"
   end
 end
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 095c0c9b7..c67d7c040 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -49,6 +49,14 @@ defmodule Phoenix.VerifiedRoutesTest do
     forward "/plug_forward", UserController
   end
 
+  defmodule CatchAllWarningRouter do
+    use Phoenix.Router
+    alias Phoenix.VerifiedRoutesTest.PostController
+
+    get "/", PostController, :root
+    get "/*path", PostController, :root, warn_on_verify: true
+  end
+
   # Emulate regular endpoint functions
 
   defmodule Endpoint do
@@ -528,6 +536,19 @@ defmodule Phoenix.VerifiedRoutesTest do
         assert warnings =~
                  ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/should-warn/foobar"|
       end
+
+      test "~p does not warn if route without warn_on_verify: true matches first" do
+        warnings =
+          ExUnit.CaptureIO.capture_io(:stderr, fn ->
+            defmodule VerifyFalse do
+              use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: CatchAllWarningRouter
+
+              def test, do: ~p"/"
+            end
+          end)
+
+        assert warnings == ""
+      end
     end
   end
 end