Skip to content

Unify the UX of template projects on navigation to non-existing page #62067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5fff491
Add re-exec tests for SSR + move NotFound sources into one directory.
ilonatommy May 21, 2025
66cc974
Per-component interactivity: test navigation to non-existing page.
ilonatommy May 21, 2025
8907524
Add streaming SSR tests.
ilonatommy May 21, 2025
3b89c4b
Clean up tests and make them work.
ilonatommy May 21, 2025
217e8dd
Fix interactivity tests.
ilonatommy May 21, 2025
6e3eb98
Remove routing sandwitch.
ilonatommy May 22, 2025
e89fe2d
Trigger streaming.
ilonatommy May 22, 2025
e4b89c1
Templates use reexecution.
ilonatommy May 22, 2025
0f52575
Merge branch 'main' into not-found-works-after-navigation
ilonatommy May 23, 2025
fe7074f
Fix templates and layout.
ilonatommy May 28, 2025
1c2dcae
Fix namespace.
ilonatommy May 28, 2025
9f6df01
Make sure that interactive tests work only when interactivity is on.
ilonatommy May 28, 2025
b69773a
Fix: link has to have ID to check if layout is rendered.
ilonatommy May 28, 2025
6b439b5
Merge branch 'main' into not-found-works-after-navigation
ilonatommy May 28, 2025
f1bbf65
Fix: Wrong placing of NotFoundPage parameter.
ilonatommy May 28, 2025
a10a0fb
Whitespace
ilonatommy May 28, 2025
561c04c
Empty templates should not contain `NotFound.razor`.
ilonatommy May 29, 2025
8d96617
Move layout import for -ai Auto from server to client project.
ilonatommy May 29, 2025
90f6f0f
Feedback: use `RouteView`.
ilonatommy May 29, 2025
bdc5050
Feedback: explicit layout passing is not necessary when rendering wit…
ilonatommy May 29, 2025
05169c9
Fix misscommit.
ilonatommy May 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ internal virtual void Refresh(bool isNavigationIntercepted)
var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan());
var locationPathSpan = TrimQueryOrHash(relativePath);
var locationPath = $"/{locationPathSpan}";
Activity? activity = null;
Activity? activity;

// In order to avoid routing twice we check for RouteData
if (RoutingStateProvider?.RouteData is { } endpointRouteData)
Expand Down Expand Up @@ -286,7 +286,7 @@ internal virtual void Refresh(bool isNavigationIntercepted)
// We did not find a Component that matches the route.
// Only show the NotFound content if the application developer programatically got us here i.e we did not
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
_renderHandle.Render(NotFound ?? DefaultNotFoundContent);
RenderNotFound();
}
else
{
Expand Down Expand Up @@ -382,25 +382,32 @@ private void OnNotFound(object sender, EventArgs args)
if (_renderHandle.IsInitialized)
{
Log.DisplayingNotFound(_logger);
_renderHandle.Render(builder =>
{
if (NotFoundPage != null)
{
builder.OpenComponent(0, NotFoundPage);
builder.CloseComponent();
}
else if (NotFound != null)
{
NotFound(builder);
}
else
{
DefaultNotFoundContent(builder);
}
});
RenderNotFound();
}
}

private void RenderNotFound()
{
_renderHandle.Render(builder =>
{
if (NotFoundPage != null)
{
builder.OpenComponent<RouteView>(0);
builder.AddAttribute(1, nameof(RouteView.RouteData),
new RouteData(NotFoundPage, _emptyParametersDictionary));
builder.CloseComponent();
}
else if (NotFound != null)
{
NotFound(builder);
}
else
{
DefaultNotFoundContent(builder);
}
});
}

async Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1411,4 +1411,41 @@ public void NavigatesWithInteractivityByRequestRedirection(bool controlFlowByExc
Browser.Click(By.Id("redirectButton"));
Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
}

[Theory]
// prerendering (SSR) is tested in NoInteractivityTest
[InlineData("ServerNonPrerendered")]
[InlineData("WebAssemblyNonPrerendered")]
public void ProgrammaticNavigationToNotExistingPathReExecutesTo404(string renderMode)
{
Navigate($"{ServerPathBase}/reexecution/redirection-not-found?renderMode={renderMode}&navigate-programmatically=true");
Assert404ReExecuted();
}

[Theory]
// prerendering (SSR) is tested in NoInteractivityTest
[InlineData("ServerNonPrerendered")]
[InlineData("WebAssemblyNonPrerendered")]
public void LinkNavigationToNotExistingPathReExecutesTo404(string renderMode)
{
Navigate($"{ServerPathBase}/reexecution/redirection-not-found?renderMode={renderMode}");
Browser.Click(By.Id("link-to-not-existing-page"));
Assert404ReExecuted();
}

[Theory]
// prerendering (SSR) is tested in NoInteractivityTest
[InlineData("ServerNonPrerendered")]
[InlineData("WebAssemblyNonPrerendered")]
public void BrowserNavigationToNotExistingPathReExecutesTo404(string renderMode)
{
// non-existing path has to have re-execution middleware set up
// so it has to have "reexecution" prefix. Otherwise middleware mapping
// will not be activated, see configuration in Startup
Navigate($"{ServerPathBase}/reexecution/not-existing-page?renderMode={renderMode}");
Assert404ReExecuted();
}

private void Assert404ReExecuted() =>
Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,43 @@ public void CanRenderNotFoundPageAfterStreamingStarted()
Browser.Equal("Default Not Found Page", () => Browser.Title);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void ProgrammaticNavigationToNotExistingPathReExecutesTo404(bool streaming)
{
string streamingPath = streaming ? "-streaming" : "";
Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}?navigate-programmatically=true");
Assert404ReExecuted();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void LinkNavigationToNotExistingPathReExecutesTo404(bool streaming)
{
string streamingPath = streaming ? "-streaming" : "";
Navigate($"{ServerPathBase}/reexecution/redirection-not-found-ssr{streamingPath}");
Browser.Click(By.Id("link-to-not-existing-page"));
Assert404ReExecuted();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void BrowserNavigationToNotExistingPathReExecutesTo404(bool streaming)
{
// non-existing path has to have re-execution middleware set up
// so it has to have "reexecution" prefix. Otherwise middleware mapping
// will not be activated, see configuration in Startup
string streamingPath = streaming ? "-streaming" : "";
Navigate($"{ServerPathBase}/reexecution/not-existing-page-ssr{streamingPath}");
Assert404ReExecuted();
}

private void Assert404ReExecuted() =>
Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand All @@ -99,6 +136,9 @@ public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage)
{
var infoText = Browser.FindElement(By.Id("test-info")).Text;
Assert.Contains("Welcome On Custom Not Found Page", infoText);
// custom page should have a custom layout
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
Assert.Contains("About", aboutLink);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool useCustomNot
{
var infoText = Browser.FindElement(By.Id("test-info")).Text;
Assert.Contains("Welcome On Custom Not Found Page", infoText);
// custom page should have a custom layout
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
Assert.Contains("About", aboutLink);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,37 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

app.Map("/subdir", app =>
{
if (!env.IsDevelopment())
app.Map("/reexecution", reexecutionApp =>
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseRouting();
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(app);
app.UseAntiforgery();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
reexecutionApp.UseStaticFiles();
reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);
reexecutionApp.UseRouting();
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(reexecutionApp);
reexecutionApp.UseAntiforgery();
reexecutionApp.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
});
});

ConfigureSubdirPipeline(app, env);
});
}

private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseRouting();
RazorComponentEndpointsStartup<TRootComponent>.UseFakeAuthState(app);
app.UseAntiforgery();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.Map("/reexecution", reexecutionApp =>
{
reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);

reexecutionApp.UseRouting();

reexecutionApp.UseAntiforgery();
reexecutionApp.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
});
ConfigureEndpoints(reexecutionApp, env);
});

ConfigureSubdirPipeline(app, env);
});
}

protected virtual void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)
{
WebAssemblyTestHelper.ServeCoopHeadersIfWebAssemblyThreadingEnabled(app);

Expand All @@ -106,11 +103,15 @@ protected virtual void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHost
{
if (ctx.Request.Query.ContainsKey("add-csp"))
{
ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'");
ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'");
}
return nxt();
});
ConfigureEndpoints(app, env);
}

private void ConfigureEndpoints(IApplicationBuilder app, IWebHostEnvironment env)
{
_ = app.UseEndpoints(endpoints =>
{
var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@using Components.TestServer.RazorComponents.Pages.Forms
@using Components.WasmMinimal.Pages
@using Components.WasmMinimal.Pages.NotFound

@code {
[Parameter]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@page "/redirection-not-found-ssr-streaming"
@page "/reexecution/redirection-not-found-ssr-streaming"
@attribute [StreamRendering(true)]

<Components.WasmMinimal.Pages.NotFound.RedirectionNotFoundComponent StartStreaming="true" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@page "/redirection-not-found-ssr"
@page "/reexecution/redirection-not-found-ssr"
@attribute [StreamRendering(false)]

<Components.WasmMinimal.Pages.NotFound.RedirectionNotFoundComponent />

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@page "/render-custom-not-found-page"
@layout NotFoundLayout

<h3 id="test-info">Welcome On Custom Not Found Page</h3>
<p>Sorry, the page you are looking for does not exist.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@inherits LayoutComponentBase

<div class="page">
<header class="top-bar">
<a id="about-link" href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</header>
<main>
<article class="content px-4">
@Body
</article>
</main>
</div>

<style>
.top-bar {
background-color: #0078d4;
color: white;
padding: 10px;
text-align: center;
width: 100%;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@page "/redirection-not-found"
@page "/reexecution/redirection-not-found"

<RedirectionNotFoundComponent @rendermode="@RenderModeHelper.GetRenderMode(_renderMode)" WaitForInteractivity="true"/>

@code{
[Parameter, SupplyParameterFromQuery(Name = "renderMode")]
public string? RenderModeStr { get; set; }

private RenderModeId _renderMode;

protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(RenderModeStr))
{
_renderMode = RenderModeHelper.ParseRenderMode(RenderModeStr);
}
else
{
throw new ArgumentException("RenderModeStr cannot be null or empty. Did you mean to redirect to /redirection-not-found-ssr?", nameof(RenderModeStr));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@inject NavigationManager NavigationManager

@if (!WaitForInteractivity || RendererInfo.IsInteractive)
{
<h1>Original page</h1>

<p id="test-info">Any content</p>
<a id="link-to-not-existing-page" href="@_nonExistingPath">
Go to not-existing-page
</a>
}

@code{
[Parameter]
[SupplyParameterFromQuery(Name = "navigate-programmatically")]
public bool? NavigateProgrammatically { get; set; }

[Parameter]
public bool StartStreaming { get; set; } = false;

[Parameter]
public bool WaitForInteractivity { get; set; } = false;

private string _nonExistingPath = string.Empty;

protected override async Task OnInitializedAsync()
{
if (StartStreaming)
{
await Task.Yield();
}
_nonExistingPath = $"{NavigationManager.BaseUri}reexecution/not-existing-page";
if (NavigateProgrammatically == true)
{
NavigationManager.NavigateTo(_nonExistingPath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace TestServer;

public static class RenderModeHelper
{
public static IComponentRenderMode GetRenderMode(RenderModeId renderMode)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@using Microsoft.AspNetCore.Components.Routing
@using Components.WasmMinimal.Pages
@using Components.WasmMinimal.Pages.NotFound
@inject NavigationManager NavigationManager

@code {
Expand Down
Loading
Loading