Clean up use of relative URLs. Fixes #844 and #845 (#878)

* 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:
Steve Sanderson 2018-05-24 09:54:43 +01:00 committed by GitHub
parent 7f3ba97fb7
commit 353da42cce
19 changed files with 193 additions and 146 deletions

View File

@ -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.

View File

@ -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>

View File

@ -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);
}

View File

@ -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())
});
}
}

View File

@ -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 "/";
}
}
}

View File

@ -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");
}
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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)
&& (

View File

@ -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))

View File

@ -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 &lt;base&gt; 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);
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -1,4 +1,3 @@
@page "/RouterTest"
@using BasicTestApp.RouterTest
@page "/"
<div id="test-info">This is the default page.</div>
<Links />

View File

@ -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>

View File

@ -1,4 +1,3 @@
@page "/RouterTest/Other"
@using BasicTestApp.RouterTest
@page "/Other"
<div id="test-info">This is another page.</div>
<Links />

View File

@ -1,2 +1 @@
@using Microsoft.AspNetCore.Blazor.Routing
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly />

View File

@ -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 />