* Handle links to empty-string href, resolved against base href Needed to change the URLs used in E2E tests to be able to cover this (i.e., removed the /RouterTest prefixes so the default relative URL became an empty string) * Change links in StandaloneApp sample to be relative * Standardize on base URIs having trailing slash everywhere Hence also change terminology from "base URI prefix" to simply "base URI" * Handle link highlighting when visiting base-href-but-without-trailing-slash * Removing leading slashes from base-relative URLs in templates
This commit is contained in:
parent
7f3ba97fb7
commit
353da42cce
|
|
@ -34,10 +34,10 @@ else
|
|||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<a href="/fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
||||
<a href="fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
||||
◀ Previous
|
||||
</a>
|
||||
<a href="/fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
||||
<a href="fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
||||
Next ▶
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -57,7 +57,7 @@ else
|
|||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
|
||||
$"/sample-data/weather.json?date={StartDate.ToString("yyyy-MM-dd")}");
|
||||
$"sample-data/weather.json?date={StartDate.ToString("yyyy-MM-dd")}");
|
||||
|
||||
// Because StandaloneApp doesn't really have a server endpoint to get dynamic data from,
|
||||
// fake the DateFormatted values here. This would not apply in a real app.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="top-row pl-4 navbar navbar-dark">
|
||||
<a class="navbar-brand" href="/">Blazor app</a>
|
||||
<a class="navbar-brand" href="">Blazor app</a>
|
||||
<button class="navbar-toggler" onclick=@ToggleNavMenu>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
|
@ -8,17 +8,17 @@
|
|||
<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/" Match=NavLinkMatch.All>
|
||||
<NavLink class="nav-link" href="" Match=NavLinkMatch.All>
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/counter">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/fetchdata">
|
||||
<NavLink class="nav-link" href="fetchdata">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||
</NavLink>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||
import { platform } from '../Environment';
|
||||
import { MethodHandle, System_String } from '../Platform/Platform';
|
||||
const registeredFunctionPrefix = 'Microsoft.AspNetCore.Blazor.Browser.Services.BrowserUriHelper';
|
||||
|
|
@ -22,9 +22,10 @@ registerFunction(`${registeredFunctionPrefix}.enableNavigationInterception`, ()
|
|||
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A');
|
||||
if (anchorTarget) {
|
||||
const href = anchorTarget.getAttribute('href');
|
||||
if (isWithinBaseUriSpace(toAbsoluteUri(href))) {
|
||||
const absoluteHref = toAbsoluteUri(href);
|
||||
if (isWithinBaseUriSpace(absoluteHref)) {
|
||||
event.preventDefault();
|
||||
performInternalNavigation(href);
|
||||
performInternalNavigation(absoluteHref);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -37,15 +38,16 @@ registerFunction(`${registeredFunctionPrefix}.navigateTo`, (uriDotNetString: Sys
|
|||
});
|
||||
|
||||
export function navigateTo(uri: string) {
|
||||
if (isWithinBaseUriSpace(toAbsoluteUri(uri))) {
|
||||
performInternalNavigation(uri);
|
||||
const absoluteUri = toAbsoluteUri(uri);
|
||||
if (isWithinBaseUriSpace(absoluteUri)) {
|
||||
performInternalNavigation(absoluteUri);
|
||||
} else {
|
||||
location.href = uri;
|
||||
}
|
||||
}
|
||||
|
||||
function performInternalNavigation(href: string) {
|
||||
history.pushState(null, /* ignored title */ '', href);
|
||||
function performInternalNavigation(absoluteInternalHref: string) {
|
||||
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
|
||||
handleInternalNavigation();
|
||||
}
|
||||
|
||||
|
|
@ -80,10 +82,10 @@ function findClosestAncestor(element: Element | null, tagName: string) {
|
|||
}
|
||||
|
||||
function isWithinBaseUriSpace(href: string) {
|
||||
const baseUriPrefixWithTrailingSlash = toBaseUriPrefixWithTrailingSlash(document.baseURI!); // TODO: Might baseURI really be null?
|
||||
return href.startsWith(baseUriPrefixWithTrailingSlash);
|
||||
const baseUriWithTrailingSlash = toBaseUriWithTrailingSlash(document.baseURI!); // TODO: Might baseURI really be null?
|
||||
return href.startsWith(baseUriWithTrailingSlash);
|
||||
}
|
||||
|
||||
function toBaseUriPrefixWithTrailingSlash(baseUri: string) {
|
||||
function toBaseUriWithTrailingSlash(baseUri: string) {
|
||||
return baseUri.substr(0, baseUri.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Http;
|
||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
serviceCollection.AddSingleton<IUriHelper>(uriHelper);
|
||||
serviceCollection.AddSingleton(new HttpClient(new BrowserHttpMessageHandler())
|
||||
{
|
||||
BaseAddress = new Uri(uriHelper.GetBaseUriPrefix())
|
||||
BaseAddress = new Uri(uriHelper.GetBaseUri())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Interop;
|
||||
|
|
@ -22,8 +22,11 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
static bool _hasEnabledNavigationInterception;
|
||||
static string _cachedAbsoluteUri;
|
||||
static EventHandler<string> _onLocationChanged;
|
||||
static string _baseUriStringNoTrailingSlash; // No trailing slash so we can just prepend it to suffixes
|
||||
static Uri _baseUriWithTrailingSlash; // With trailing slash so it can be used in new Uri(base, relative)
|
||||
|
||||
// These two are always kept in sync. We store both representations to
|
||||
// avoid having to convert between them on demand.
|
||||
static Uri _baseUriWithTrailingSlash;
|
||||
static string _baseUriStringWithTrailingSlash;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<string> OnLocationChanged
|
||||
|
|
@ -44,10 +47,10 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetBaseUriPrefix()
|
||||
public string GetBaseUri()
|
||||
{
|
||||
EnsureBaseUriPopulated();
|
||||
return _baseUriStringNoTrailingSlash;
|
||||
return _baseUriStringWithTrailingSlash;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -81,27 +84,25 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToBaseRelativePath(string baseUriPrefix, string absoluteUri)
|
||||
public string ToBaseRelativePath(string baseUri, string absoluteUri)
|
||||
{
|
||||
if (absoluteUri.Equals(baseUriPrefix, StringComparison.Ordinal))
|
||||
if (absoluteUri.StartsWith(baseUri, StringComparison.Ordinal))
|
||||
{
|
||||
// Special case: if you're exactly at the base URI, treat it as if you
|
||||
// were at "{baseUriPrefix}/" (i.e., with a following slash). It's a bit
|
||||
// ambiguous because we don't know whether the server would return the
|
||||
// same page whether or not the slash is present, but ASP.NET Core at
|
||||
// least does by default when using PathBase.
|
||||
return "/";
|
||||
}
|
||||
else if (absoluteUri.StartsWith(baseUriPrefix, StringComparison.Ordinal)
|
||||
&& absoluteUri.Length > baseUriPrefix.Length
|
||||
&& absoluteUri[baseUriPrefix.Length] == '/')
|
||||
// The absolute URI must be of the form "{baseUri}something" (where
|
||||
// baseUri ends with a slash), and from that we return "something"
|
||||
return absoluteUri.Substring(baseUri.Length);
|
||||
} else if ($"{absoluteUri}/".Equals(baseUri, StringComparison.Ordinal))
|
||||
{
|
||||
// The absolute URI must be of the form "{baseUriPrefix}/something",
|
||||
// and from that we return "/something"
|
||||
return absoluteUri.Substring(baseUriPrefix.Length);
|
||||
// Special case: for the base URI "/something/", if you're at
|
||||
// "/something" then treat it as if you were at "/something/" (i.e.,
|
||||
// with the trailing slash). It's a bit ambiguous because we don't know
|
||||
// whether the server would return the same page whether or not the
|
||||
// slash is present, but ASP.NET Core at least does by default when
|
||||
// using PathBase.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"The URI '{absoluteUri}' is not contained by the base URI '{baseUriPrefix}'.");
|
||||
throw new ArgumentException($"The URI '{absoluteUri}' is not contained by the base URI '{baseUri}'.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -118,12 +119,12 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
private static void EnsureBaseUriPopulated()
|
||||
{
|
||||
// The <base href> is fixed for the lifetime of the page, so just cache it
|
||||
if (_baseUriStringNoTrailingSlash == null)
|
||||
if (_baseUriStringWithTrailingSlash == null)
|
||||
{
|
||||
var baseUri = RegisteredFunction.InvokeUnmarshalled<string>(
|
||||
var baseUriAbsolute = RegisteredFunction.InvokeUnmarshalled<string>(
|
||||
$"{_functionPrefix}.getBaseURI");
|
||||
_baseUriStringNoTrailingSlash = ToBaseUriPrefix(baseUri);
|
||||
_baseUriWithTrailingSlash = new Uri(_baseUriStringNoTrailingSlash + "/");
|
||||
_baseUriStringWithTrailingSlash = ToBaseUri(baseUriAbsolute);
|
||||
_baseUriWithTrailingSlash = new Uri(_baseUriStringWithTrailingSlash);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,25 +148,25 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the document's document.baseURI value, returns the URI prefix
|
||||
/// that can be prepended to URI paths to produce an absolute URI.
|
||||
/// This is computed by removing the final slash and any following characters.
|
||||
/// Given the document's document.baseURI value, returns the URI
|
||||
/// that can be prepended to relative URI paths to produce an absolute URI.
|
||||
/// This is computed by removing anything after the final slash.
|
||||
/// Internal for tests.
|
||||
/// </summary>
|
||||
/// <param name="baseUri">The page's document.baseURI value.</param>
|
||||
/// <param name="absoluteBaseUri">The page's document.baseURI value.</param>
|
||||
/// <returns>The URI prefix</returns>
|
||||
internal static string ToBaseUriPrefix(string baseUri)
|
||||
internal static string ToBaseUri(string absoluteBaseUri)
|
||||
{
|
||||
if (baseUri != null)
|
||||
if (absoluteBaseUri != null)
|
||||
{
|
||||
var lastSlashIndex = baseUri.LastIndexOf('/');
|
||||
var lastSlashIndex = absoluteBaseUri.LastIndexOf('/');
|
||||
if (lastSlashIndex >= 0)
|
||||
{
|
||||
return baseUri.Substring(0, lastSlashIndex);
|
||||
return absoluteBaseUri.Substring(0, lastSlashIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,6 @@ else
|
|||
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("/api/SampleData/WeatherForecasts");
|
||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="top-row pl-4 navbar navbar-dark">
|
||||
<a class="navbar-brand" href="/">BlazorHosted-CSharp</a>
|
||||
<a class="navbar-brand" href="">BlazorHosted-CSharp</a>
|
||||
<button class="navbar-toggler" onclick=@ToggleNavMenu>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
|
@ -8,17 +8,17 @@
|
|||
<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/" Match=NavLinkMatch.All>
|
||||
<NavLink class="nav-link" href="" Match=NavLinkMatch.All>
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/counter">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/fetchdata">
|
||||
<NavLink class="nav-link" href="fetchdata">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||
</NavLink>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ else
|
|||
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("/sample-data/weather.json");
|
||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
||||
}
|
||||
|
||||
class WeatherForecast
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="top-row pl-4 navbar navbar-dark">
|
||||
<a class="navbar-brand" href="/">BlazorStandalone-CSharp</a>
|
||||
<a class="navbar-brand" href="">BlazorStandalone-CSharp</a>
|
||||
<button class="navbar-toggler" onclick=@ToggleNavMenu>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
|
@ -8,17 +8,17 @@
|
|||
<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/" Match=NavLinkMatch.All>
|
||||
<NavLink class="nav-link" href="" Match=NavLinkMatch.All>
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/counter">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/fetchdata">
|
||||
<NavLink class="nav-link" href="fetchdata">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||
</NavLink>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
|
@ -96,18 +96,45 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
|
||||
private bool ShouldMatch(string currentUriAbsolute)
|
||||
{
|
||||
if (Match == NavLinkMatch.Prefix)
|
||||
if (EqualsHrefExactlyOrIfTrailingSlashAdded(currentUriAbsolute))
|
||||
{
|
||||
return StartsWithAndHasSeparator(currentUriAbsolute, _hrefAbsolute);
|
||||
return true;
|
||||
}
|
||||
else if (Match == NavLinkMatch.All)
|
||||
|
||||
if (Match == NavLinkMatch.Prefix
|
||||
&& IsStrictlyPrefixWithSeparator(currentUriAbsolute, _hrefAbsolute))
|
||||
{
|
||||
return string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.Ordinal);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute)
|
||||
{
|
||||
if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported {nameof(NavLinkMatch)} value: {Match}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentUriAbsolute.Length == _hrefAbsolute.Length - 1)
|
||||
{
|
||||
// Special case: highlight links to http://host/path/ even if you're
|
||||
// at http://host/path (with no trailing slash)
|
||||
//
|
||||
// This is because the router accepts an absolute URI value of "same
|
||||
// as base URI but without trailing slash" as equivalent to "base URI",
|
||||
// which in turn is because it's common for servers to return the same page
|
||||
// for http://host/vdir as they do for host://host/vdir/ as it's no
|
||||
// good to display a blank page in that case.
|
||||
if (_hrefAbsolute[_hrefAbsolute.Length - 1] == '/'
|
||||
&& _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Render(RenderTreeBuilder builder)
|
||||
|
|
@ -134,15 +161,10 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
=> str1 == null ? str2
|
||||
: (str2 == null ? str1 : $"{str1} {str2}");
|
||||
|
||||
private static bool StartsWithAndHasSeparator(string value, string prefix)
|
||||
private static bool IsStrictlyPrefixWithSeparator(string value, string prefix)
|
||||
{
|
||||
var valueLength = value.Length;
|
||||
var prefixLength = prefix.Length;
|
||||
if (prefixLength == valueLength)
|
||||
{
|
||||
return string.Equals(value, prefix, StringComparison.Ordinal);
|
||||
}
|
||||
else if (valueLength > prefixLength)
|
||||
if (value.Length > prefixLength)
|
||||
{
|
||||
return value.StartsWith(prefix, StringComparison.Ordinal)
|
||||
&& (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
|
||||
|
||||
RenderHandle _renderHandle;
|
||||
string _baseUriPrefix;
|
||||
string _baseUri;
|
||||
string _locationAbsolute;
|
||||
|
||||
[Inject] private IUriHelper UriHelper { get; set; }
|
||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
public void Init(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
_baseUriPrefix = UriHelper.GetBaseUriPrefix();
|
||||
_baseUri = UriHelper.GetBaseUri();
|
||||
_locationAbsolute = UriHelper.GetAbsoluteUri();
|
||||
UriHelper.OnLocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
|
@ -76,13 +76,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
|
||||
private void Refresh()
|
||||
{
|
||||
var locationPath = UriHelper.ToBaseRelativePath(_baseUriPrefix, _locationAbsolute);
|
||||
var locationPath = UriHelper.ToBaseRelativePath(_baseUri, _locationAbsolute);
|
||||
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
|
||||
var context = new RouteContext(locationPath);
|
||||
Routes.Route(context);
|
||||
if (context.Handler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"'{nameof(Router)}' cannot find any component with a route for '{locationPath}'.");
|
||||
throw new InvalidOperationException($"'{nameof(Router)}' cannot find any component with a route for '/{locationPath}'.");
|
||||
}
|
||||
|
||||
if (!typeof(IComponent).IsAssignableFrom(context.Handler))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -30,26 +30,26 @@ namespace Microsoft.AspNetCore.Blazor.Services
|
|||
Uri ToAbsoluteUri(string href);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI prefix that can be prepended before URI paths to produce an absolute URI.
|
||||
/// Gets the base URI (with trailing slash) that can be prepended before relative URI paths to produce an absolute URI.
|
||||
/// Typically this corresponds to the 'href' attribute on the document's <base> element.
|
||||
/// </summary>
|
||||
/// <returns>The URI prefix.</returns>
|
||||
string GetBaseUriPrefix();
|
||||
/// <returns>The URI prefix, which has a trailing slash.</returns>
|
||||
string GetBaseUri();
|
||||
|
||||
/// <summary>
|
||||
/// Given a base URI prefix (e.g., one previously returned by <see cref="GetBaseUriPrefix"/>),
|
||||
/// Given a base URI (e.g., one previously returned by <see cref="GetBaseUri"/>),
|
||||
/// converts an absolute URI into one relative to the base URI prefix.
|
||||
/// </summary>
|
||||
/// <param name="baseUriPrefix">The base URI prefix (e.g., previously returned by <see cref="GetBaseUriPrefix"/>).</param>
|
||||
/// <param name="locationAbsolute">An absolute URI that is within the space of the base URI prefix.</param>
|
||||
/// <param name="baseUri">The base URI prefix (e.g., previously returned by <see cref="GetBaseUri"/>).</param>
|
||||
/// <param name="locationAbsolute">An absolute URI that is within the space of the base URI.</param>
|
||||
/// <returns>A relative URI path.</returns>
|
||||
string ToBaseRelativePath(string baseUriPrefix, string locationAbsolute);
|
||||
string ToBaseRelativePath(string baseUri, string locationAbsolute);
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
|
||||
/// prefix (as returned by <see cref="GetBaseUriPrefix"/>).</param>
|
||||
/// (as returned by <see cref="GetBaseUri"/>).</param>
|
||||
void NavigateTo(string uri);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,41 +12,43 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Test
|
|||
private BrowserUriHelper _browserUriHelper = new BrowserUriHelper();
|
||||
|
||||
[Theory]
|
||||
[InlineData("scheme://host/", "scheme://host")]
|
||||
[InlineData("scheme://host:123/", "scheme://host:123")]
|
||||
[InlineData("scheme://host/path", "scheme://host")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/path")]
|
||||
[InlineData("scheme://host/path/page?query=string&another=here", "scheme://host/path")]
|
||||
public void ComputesCorrectBaseUriPrefix(string baseUri, string expectedResult)
|
||||
[InlineData("scheme://host/", "scheme://host/")]
|
||||
[InlineData("scheme://host:123/", "scheme://host:123/")]
|
||||
[InlineData("scheme://host/path", "scheme://host/")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/path/")]
|
||||
[InlineData("scheme://host/path/page?query=string&another=here", "scheme://host/path/")]
|
||||
public void ComputesCorrectBaseUri(string baseUri, string expectedResult)
|
||||
{
|
||||
var actualResult = BrowserUriHelper.ToBaseUriPrefix(baseUri);
|
||||
var actualResult = BrowserUriHelper.ToBaseUri(baseUri);
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("scheme://host", "scheme://host/", "/")]
|
||||
[InlineData("scheme://host", "scheme://host/path", "/path")]
|
||||
[InlineData("scheme://host/path", "scheme://host/path/", "/")]
|
||||
[InlineData("scheme://host/path", "scheme://host/path/more", "/more")]
|
||||
[InlineData("scheme://host/path", "scheme://host/path", "/")]
|
||||
public void ComputesCorrectValidBaseRelativePaths(string baseUriPrefix, string absoluteUri, string expectedResult)
|
||||
[InlineData("scheme://host/", "scheme://host", "")]
|
||||
[InlineData("scheme://host/", "scheme://host/", "")]
|
||||
[InlineData("scheme://host/", "scheme://host/path", "path")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/path/", "")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/path/more", "more")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/path", "")]
|
||||
public void ComputesCorrectValidBaseRelativePaths(string baseUri, string absoluteUri, string expectedResult)
|
||||
{
|
||||
var actualResult = _browserUriHelper.ToBaseRelativePath(baseUriPrefix, absoluteUri);
|
||||
var actualResult = _browserUriHelper.ToBaseRelativePath(baseUri, absoluteUri);
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("scheme://host", "otherscheme://host/")] // Mismatched prefix is error
|
||||
[InlineData("scheme://host", "scheme://otherhost/")] // Mismatched prefix is error
|
||||
public void ThrowsForInvalidBaseRelativePaths(string baseUriPrefix, string absoluteUri)
|
||||
[InlineData("scheme://host/", "otherscheme://host/")]
|
||||
[InlineData("scheme://host/", "scheme://otherhost/")]
|
||||
[InlineData("scheme://host/path/", "scheme://host/")]
|
||||
public void ThrowsForInvalidBaseRelativePaths(string baseUri, string absoluteUri)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
_browserUriHelper.ToBaseRelativePath(baseUriPrefix, absoluteUri);
|
||||
_browserUriHelper.ToBaseRelativePath(baseUri, absoluteUri);
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
$"The URI '{absoluteUri}' is not contained by the base URI '{baseUriPrefix}'.",
|
||||
$"The URI '{absoluteUri}' is not contained by the base URI '{baseUri}'.",
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -31,27 +31,41 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanArriveAtDefaultPage()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("Default (matches all)");
|
||||
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanArriveAtDefaultPageWithoutTrailingSlash()
|
||||
{
|
||||
// This is a bit of a degenerate case because ideally devs would configure their
|
||||
// servers to enforce a canonical URL (with trailing slash) for the homepage.
|
||||
// But in case they don't want to, we need to handle it the same as if the URL does
|
||||
// have a trailing slash.
|
||||
SetUrlViaPushState($"{ServerPathBase}");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanArriveAtPageWithParameters()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/WithParameters/Name/Dan/LastName/Roth");
|
||||
SetUrlViaPushState($"{ServerPathBase}/WithParameters/Name/Ghi/LastName/O'Jkl");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
Assert.Equal("Your full name is Dan Roth.", app.FindElement(By.Id("test-info")).Text);
|
||||
Assert.Equal("Your full name is Ghi O'Jkl.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanArriveAtNonDefaultPage()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other");
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text);
|
||||
|
|
@ -61,7 +75,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanFollowLinkToOtherPage()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Other")).Click();
|
||||
|
|
@ -72,7 +86,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanFollowLinkToOtherPageWithBaseRelativeUrl()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Other with base-relative URL (matches all)")).Click();
|
||||
|
|
@ -80,32 +94,43 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFollowLinkToEmptyStringHrefAsBaseRelativeUrl()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Default with base-relative URL (matches all)")).Click();
|
||||
Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFollowLinkToPageWithParameters()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other");
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("With parameters")).Click();
|
||||
Assert.Equal("Your full name is Steve Sanderson.", app.FindElement(By.Id("test-info")).Text);
|
||||
Assert.Equal("Your full name is Abc McDef.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("With parameters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFollowLinkToDefaultPage()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other");
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Default (matches all)")).Click();
|
||||
Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("Default (matches all)");
|
||||
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFollowLinkToOtherPageWithQueryString()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Other with query")).Click();
|
||||
|
|
@ -116,7 +141,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanFollowLinkToDefaultPageWithQueryString()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other");
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Default with query")).Click();
|
||||
|
|
@ -127,7 +152,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanFollowLinkToOtherPageWithHash()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Other with hash")).Click();
|
||||
|
|
@ -138,7 +163,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanFollowLinkToDefaultPageWithHash()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other");
|
||||
SetUrlViaPushState($"{ServerPathBase}/Other");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.LinkText("Default with hash")).Click();
|
||||
|
|
@ -149,7 +174,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CanNavigateProgrammatically()
|
||||
{
|
||||
SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
|
||||
SetUrlViaPushState($"{ServerPathBase}/");
|
||||
|
||||
var app = MountTestComponent<TestRouter>();
|
||||
app.FindElement(By.TagName("button")).Click();
|
||||
|
|
@ -168,7 +193,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
{
|
||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||
var absoluteUri = new Uri(_server.RootUri, relativeUri);
|
||||
jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString()}')");
|
||||
jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString().Replace("'", "\\'")}')");
|
||||
}
|
||||
|
||||
private void AssertHighlightedLinks(params string[] linkTexts)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
@page "/RouterTest"
|
||||
@using BasicTestApp.RouterTest
|
||||
@page "/"
|
||||
<div id="test-info">This is the default page.</div>
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
@page "/Links"
|
||||
@using Microsoft.AspNetCore.Blazor.Routing
|
||||
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper uriHelper
|
||||
<style type="text/css">a.active { background-color: yellow; font-weight: bold; }</style>
|
||||
<ul>
|
||||
<li><NavLink href="/subdir/RouterTest/" Match=NavLinkMatch.All>Default (matches all)</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/?abc=123">Default with query</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/#blah">Default with hash</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/Other">Other</NavLink></li>
|
||||
<li><NavLink href="RouterTest/Other" Match=NavLinkMatch.All>Other with base-relative URL (matches all)</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/Other?abc=123">Other with query</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/Other#blah">Other with hash</NavLink></li>
|
||||
<li><NavLink href="/subdir/RouterTest/WithParameters/Name/Steve/LastName/Sanderson">With parameters</NavLink></li>
|
||||
<li><NavLink href="/subdir/" Match=NavLinkMatch.All>Default (matches all)</NavLink></li>
|
||||
<li><NavLink href="" Match=NavLinkMatch.All>Default with base-relative URL (matches all)</NavLink></li>
|
||||
<li><NavLink href="/subdir/?abc=123">Default with query</NavLink></li>
|
||||
<li><NavLink href="/subdir/#blah">Default with hash</NavLink></li>
|
||||
<li><NavLink href="/subdir/Other">Other</NavLink></li>
|
||||
<li><NavLink href="Other" Match=NavLinkMatch.All>Other with base-relative URL (matches all)</NavLink></li>
|
||||
<li><NavLink href="/subdir/Other?abc=123">Other with query</NavLink></li>
|
||||
<li><NavLink href="/subdir/Other#blah">Other with hash</NavLink></li>
|
||||
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With parameters</NavLink></li>
|
||||
</ul>
|
||||
|
||||
<button onclick=@(x => uriHelper.NavigateTo("RouterTest/Other"))>
|
||||
<button onclick=@(x => uriHelper.NavigateTo("Other"))>
|
||||
Programmatic navigation
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
@page "/RouterTest/Other"
|
||||
@using BasicTestApp.RouterTest
|
||||
@page "/Other"
|
||||
<div id="test-info">This is another page.</div>
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
@using Microsoft.AspNetCore.Blazor.Routing
|
||||
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
@page "/RouterTest/WithParameters/Name/{firstName}/LastName/{lastName}"
|
||||
@using BasicTestApp.RouterTest
|
||||
@using Microsoft.AspNetCore.Blazor.Components
|
||||
@page "/WithParameters/Name/{firstName}/LastName/{lastName}"
|
||||
<div id="test-info">Your full name is @FirstName @LastName.</div>
|
||||
<Links />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue