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

Merged
merged 22 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 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
1e207c2
Add error pages middlewarefor reexecution + use status code pages on …
ilonatommy May 30, 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
1 change: 1 addition & 0 deletions src/Components/Components/src/LayoutAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ public LayoutAttribute([DynamicallyAccessedMembers(Component)] Type layoutType)
/// The type of the layout. The type must implement <see cref="IComponent"/>
/// and must accept a parameter with the name 'Body'.
/// </summary>
[DynamicallyAccessedMembers(Component)]
public Type LayoutType { get; private set; }
}
49 changes: 33 additions & 16 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary

private bool _onNavigateCalled;

[DynamicallyAccessedMembers(LinkerFlags.Component)]
private Type? _notFoundLayoutType;

[Inject] private NavigationManager NavigationManager { get; set; }

[Inject] private INavigationInterception NavigationInterception { get; set; }
Expand Down Expand Up @@ -156,6 +159,9 @@ public async Task SetParametersAsync(ParameterView parameters)
throw new InvalidOperationException($"The type {NotFoundPage.FullName} " +
$"does not have a {typeof(RouteAttribute).FullName} applied to it.");
}

var layoutAttr = NotFoundPage.GetTypeInfo().GetCustomAttribute<LayoutAttribute>();
_notFoundLayoutType = layoutAttr?.LayoutType;
}

if (!_onNavigateCalled)
Expand Down Expand Up @@ -223,7 +229,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 +292,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,23 +388,34 @@ private void OnNotFound(object sender, EventArgs args)
if (_renderHandle.IsInitialized)
{
Log.DisplayingNotFound(_logger);
_renderHandle.Render(builder =>
RenderNotFound();
}
}

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

async Task IHandleAfterRender.OnAfterRenderAsync()
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));
}
}
}
Loading
Loading