Skip to content

Commit 66a457d

Browse files
authored
Unify the UX of template projects on navigation to non-existing page (#62067)
* Add re-exec tests for SSR + move NotFound sources into one directory. * Per-component interactivity: test navigation to non-existing page. * Add streaming SSR tests. * Templates use reexecution. * Fix layout rendering for `NotFoundPage`.
1 parent 8d4b843 commit 66a457d

31 files changed

+271
-48
lines changed

src/Components/Components/src/Routing/Router.cs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ internal virtual void Refresh(bool isNavigationIntercepted)
223223
var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan());
224224
var locationPathSpan = TrimQueryOrHash(relativePath);
225225
var locationPath = $"/{locationPathSpan}";
226-
Activity? activity = null;
226+
Activity? activity;
227227

228228
// In order to avoid routing twice we check for RouteData
229229
if (RoutingStateProvider?.RouteData is { } endpointRouteData)
@@ -286,7 +286,7 @@ internal virtual void Refresh(bool isNavigationIntercepted)
286286
// We did not find a Component that matches the route.
287287
// Only show the NotFound content if the application developer programatically got us here i.e we did not
288288
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
289-
_renderHandle.Render(NotFound ?? DefaultNotFoundContent);
289+
RenderNotFound();
290290
}
291291
else
292292
{
@@ -382,25 +382,32 @@ private void OnNotFound(object sender, EventArgs args)
382382
if (_renderHandle.IsInitialized)
383383
{
384384
Log.DisplayingNotFound(_logger);
385-
_renderHandle.Render(builder =>
386-
{
387-
if (NotFoundPage != null)
388-
{
389-
builder.OpenComponent(0, NotFoundPage);
390-
builder.CloseComponent();
391-
}
392-
else if (NotFound != null)
393-
{
394-
NotFound(builder);
395-
}
396-
else
397-
{
398-
DefaultNotFoundContent(builder);
399-
}
400-
});
385+
RenderNotFound();
401386
}
402387
}
403388

389+
private void RenderNotFound()
390+
{
391+
_renderHandle.Render(builder =>
392+
{
393+
if (NotFoundPage != null)
394+
{
395+
builder.OpenComponent<RouteView>(0);
396+
builder.AddAttribute(1, nameof(RouteView.RouteData),
397+
new RouteData(NotFoundPage, _emptyParametersDictionary));
398+
builder.CloseComponent();
399+
}
400+
else if (NotFound != null)
401+
{
402+
NotFound(builder);
403+
}
404+
else
405+
{
406+
DefaultNotFoundContent(builder);
407+
}
408+
});
409+
}
410+
404411
async Task IHandleAfterRender.OnAfterRenderAsync()
405412
{
406413
if (!_navigationInterceptionEnabled)

src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,4 +1411,41 @@ public void NavigatesWithInteractivityByRequestRedirection(bool controlFlowByExc
14111411
Browser.Click(By.Id("redirectButton"));
14121412
Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
14131413
}
1414+
1415+
[Theory]
1416+
// prerendering (SSR) is tested in NoInteractivityTest
1417+
[InlineData("ServerNonPrerendered")]
1418+
[InlineData("WebAssemblyNonPrerendered")]
1419+
public void ProgrammaticNavigationToNotExistingPathReExecutesTo404(string renderMode)
1420+
{
1421+
Navigate($"{ServerPathBase}/reexecution/redirection-not-found?renderMode={renderMode}&navigate-programmatically=true");
1422+
Assert404ReExecuted();
1423+
}
1424+
1425+
[Theory]
1426+
// prerendering (SSR) is tested in NoInteractivityTest
1427+
[InlineData("ServerNonPrerendered")]
1428+
[InlineData("WebAssemblyNonPrerendered")]
1429+
public void LinkNavigationToNotExistingPathReExecutesTo404(string renderMode)
1430+
{
1431+
Navigate($"{ServerPathBase}/reexecution/redirection-not-found?renderMode={renderMode}");
1432+
Browser.Click(By.Id("link-to-not-existing-page"));
1433+
Assert404ReExecuted();
1434+
}
1435+
1436+
[Theory]
1437+
// prerendering (SSR) is tested in NoInteractivityTest
1438+
[InlineData("ServerNonPrerendered")]
1439+
[InlineData("WebAssemblyNonPrerendered")]
1440+
public void BrowserNavigationToNotExistingPathReExecutesTo404(string renderMode)
1441+
{
1442+
// non-existing path has to have re-execution middleware set up
1443+
// so it has to have "reexecution" prefix. Otherwise middleware mapping
1444+
// will not be activated, see configuration in Startup
1445+
Navigate($"{ServerPathBase}/reexecution/not-existing-page?renderMode={renderMode}");
1446+
Assert404ReExecuted();
1447+
}
1448+
1449+
private void Assert404ReExecuted() =>
1450+
Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);
14141451
}

src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,43 @@ public void CanRenderNotFoundPageAfterStreamingStarted()
8787
Browser.Equal("Default Not Found Page", () => Browser.Title);
8888
}
8989

90+
[Theory]
91+
[InlineData(true)]
92+
[InlineData(false)]
93+
public void ProgrammaticNavigationToNotExistingPathReExecutesTo404(bool streaming)
94+
{
95+
string streamingPath = streaming ? "-streaming" : "";
96+
Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}?navigate-programmatically=true");
97+
Assert404ReExecuted();
98+
}
99+
100+
[Theory]
101+
[InlineData(true)]
102+
[InlineData(false)]
103+
public void LinkNavigationToNotExistingPathReExecutesTo404(bool streaming)
104+
{
105+
string streamingPath = streaming ? "-streaming" : "";
106+
Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}");
107+
Browser.Click(By.Id("link-to-not-existing-page"));
108+
Assert404ReExecuted();
109+
}
110+
111+
[Theory]
112+
[InlineData(true)]
113+
[InlineData(false)]
114+
public void BrowserNavigationToNotExistingPathReExecutesTo404(bool streaming)
115+
{
116+
// non-existing path has to have re-execution middleware set up
117+
// so it has to have "reexecution" prefix. Otherwise middleware mapping
118+
// will not be activated, see configuration in Startup
119+
string streamingPath = streaming ? "-streaming" : "";
120+
Navigate($"{ServerPathBase}/reexecution/not-existing-page-ssr{streamingPath}");
121+
Assert404ReExecuted();
122+
}
123+
124+
private void Assert404ReExecuted() =>
125+
Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);
126+
90127
[Theory]
91128
[InlineData(true)]
92129
[InlineData(false)]
@@ -99,6 +136,9 @@ public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage)
99136
{
100137
var infoText = Browser.FindElement(By.Id("test-info")).Text;
101138
Assert.Contains("Welcome On Custom Not Found Page", infoText);
139+
// custom page should have a custom layout
140+
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
141+
Assert.Contains("About", aboutLink);
102142
}
103143
else
104144
{

src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool useCustomNot
3939
{
4040
var infoText = Browser.FindElement(By.Id("test-info")).Text;
4141
Assert.Contains("Welcome On Custom Not Found Page", infoText);
42+
// custom page should have a custom layout
43+
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
44+
Assert.Contains("About", aboutLink);
4245
}
4346
else
4447
{

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,42 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
4848

4949
app.Map("/subdir", app =>
5050
{
51-
if (!env.IsDevelopment())
51+
app.Map("/reexecution", reexecutionApp =>
5252
{
53-
app.UseExceptionHandler("/Error", createScopeForErrors: true);
54-
}
53+
if (!env.IsDevelopment())
54+
{
55+
app.UseExceptionHandler("/Error", createScopeForErrors: true);
56+
}
5557

56-
app.UseStaticFiles();
57-
app.UseRouting();
58-
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(app);
59-
app.UseAntiforgery();
60-
app.UseEndpoints(endpoints =>
61-
{
62-
endpoints.MapRazorComponents<TRootComponent>();
58+
reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);
59+
reexecutionApp.UseStaticFiles();
60+
reexecutionApp.UseRouting();
61+
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(reexecutionApp);
62+
reexecutionApp.UseAntiforgery();
63+
reexecutionApp.UseEndpoints(endpoints =>
64+
{
65+
endpoints.MapRazorComponents<TRootComponent>();
66+
});
6367
});
68+
69+
ConfigureSubdirPipeline(app, env);
70+
});
71+
}
72+
73+
private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
74+
{
75+
if (!env.IsDevelopment())
76+
{
77+
app.UseExceptionHandler("/Error", createScopeForErrors: true);
78+
}
79+
80+
app.UseStaticFiles();
81+
app.UseRouting();
82+
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(app);
83+
app.UseAntiforgery();
84+
app.UseEndpoints(endpoints =>
85+
{
86+
endpoints.MapRazorComponents<TRootComponent>();
6487
});
6588
}
6689
}

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
7676
app.Map("/reexecution", reexecutionApp =>
7777
{
7878
reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);
79-
8079
reexecutionApp.UseRouting();
80+
8181
reexecutionApp.UseAntiforgery();
82-
reexecutionApp.UseEndpoints(endpoints =>
83-
{
84-
endpoints.MapRazorComponents<TRootComponent>();
85-
});
82+
ConfigureEndpoints(reexecutionApp, env);
8683
});
8784

8885
ConfigureSubdirPipeline(app, env);
8986
});
9087
}
9188

92-
protected virtual void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
89+
private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
9390
{
9491
WebAssemblyTestHelper.ServeCoopHeadersIfWebAssemblyThreadingEnabled(app);
9592

@@ -106,11 +103,15 @@ protected virtual void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHost
106103
{
107104
if (ctx.Request.Query.ContainsKey("add-csp"))
108105
{
109-
ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'");
106+
ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'");
110107
}
111108
return nxt();
112109
});
110+
ConfigureEndpoints(app, env);
111+
}
113112

113+
private void ConfigureEndpoints(IApplicationBuilder app, IWebHostEnvironment env)
114+
{
114115
_ = app.UseEndpoints(endpoints =>
115116
{
116117
var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json");

src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@using Components.TestServer.RazorComponents.Pages.Forms
2-
@using Components.WasmMinimal.Pages
2+
@using Components.WasmMinimal.Pages.NotFound
33

44
@code {
55
[Parameter]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@page "/redirection-not-found-ssr-streaming"
2+
@page "/reexecution/redirection-not-found-ssr-streaming"
3+
@attribute [StreamRendering(true)]
4+
5+
<Components.WasmMinimal.Pages.NotFound.RedirectionNotFoundComponent StartStreaming="true" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@page "/redirection-not-found-ssr"
2+
@page "/reexecution/redirection-not-found-ssr"
3+
@attribute [StreamRendering(false)]
4+
5+
<Components.WasmMinimal.Pages.NotFound.RedirectionNotFoundComponent />

src/Components/test/testassets/Components.WasmMinimal/Pages/CustomNotFoundPage.razor

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@page "/render-custom-not-found-page"
2+
@layout NotFoundLayout
3+
4+
<h3 id="test-info">Welcome On Custom Not Found Page</h3>
5+
<p>Sorry, the page you are looking for does not exist.</p>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@inherits LayoutComponentBase
2+
3+
<div class="page">
4+
<header class="top-bar">
5+
<a id="about-link" href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
6+
</header>
7+
<main>
8+
<article class="content px-4">
9+
@Body
10+
</article>
11+
</main>
12+
</div>
13+
14+
<style>
15+
.top-bar {
16+
background-color: #0078d4;
17+
color: white;
18+
padding: 10px;
19+
text-align: center;
20+
width: 100%;
21+
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
22+
}
23+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@page "/redirection-not-found"
2+
@page "/reexecution/redirection-not-found"
3+
4+
<RedirectionNotFoundComponent @rendermode="@RenderModeHelper.GetRenderMode(_renderMode)" WaitForInteractivity="true"/>
5+
6+
@code{
7+
[Parameter, SupplyParameterFromQuery(Name = "renderMode")]
8+
public string? RenderModeStr { get; set; }
9+
10+
private RenderModeId _renderMode;
11+
12+
protected override void OnInitialized()
13+
{
14+
if (!string.IsNullOrEmpty(RenderModeStr))
15+
{
16+
_renderMode = RenderModeHelper.ParseRenderMode(RenderModeStr);
17+
}
18+
else
19+
{
20+
throw new ArgumentException("RenderModeStr cannot be null or empty. Did you mean to redirect to /redirection-not-found-ssr?", nameof(RenderModeStr));
21+
}
22+
}
23+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@inject NavigationManager NavigationManager
2+
3+
@if (!WaitForInteractivity || RendererInfo.IsInteractive)
4+
{
5+
<h1>Original page</h1>
6+
7+
<p id="test-info">Any content</p>
8+
<a id="link-to-not-existing-page" href="@_nonExistingPath">
9+
Go to not-existing-page
10+
</a>
11+
}
12+
13+
@code{
14+
[Parameter]
15+
[SupplyParameterFromQuery(Name = "navigate-programmatically")]
16+
public bool? NavigateProgrammatically { get; set; }
17+
18+
[Parameter]
19+
public bool StartStreaming { get; set; } = false;
20+
21+
[Parameter]
22+
public bool WaitForInteractivity { get; set; } = false;
23+
24+
private string _nonExistingPath = string.Empty;
25+
26+
protected override async Task OnInitializedAsync()
27+
{
28+
if (StartStreaming)
29+
{
30+
await Task.Yield();
31+
}
32+
_nonExistingPath = $"{NavigationManager.BaseUri}reexecution/not-existing-page";
33+
if (NavigateProgrammatically == true)
34+
{
35+
NavigationManager.NavigateTo(_nonExistingPath);
36+
}
37+
}
38+
}

src/Components/test/testassets/Components.TestServer/RenderModeHelper.cs renamed to src/Components/test/testassets/Components.WasmMinimal/RenderModeHelper.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using Microsoft.AspNetCore.Components;
66
using Microsoft.AspNetCore.Components.Web;
77

8-
namespace TestServer;
9-
108
public static class RenderModeHelper
119
{
1210
public static IComponentRenderMode GetRenderMode(RenderModeId renderMode)

0 commit comments

Comments
 (0)