diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Routing/BrowserRouter.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Routing/BrowserRouter.cs index 87fa2ba250..37564091f2 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Routing/BrowserRouter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Routing/BrowserRouter.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing /// public class BrowserRouter : IComponent, IDisposable { + static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; + RenderHandle _renderHandle; string _baseUriPrefix; string _locationAbsolute; @@ -74,6 +76,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing throw new InvalidOperationException($"No value was specified for {nameof(PagesNamespace)}."); } + locationPath = StringUntilAny(locationPath, _queryOrHashStartChar); var componentTypeName = $"{PagesNamespace}{locationPath.Replace('/', '.')}"; if (componentTypeName[componentTypeName.Length - 1] == '.') { @@ -84,6 +87,14 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing ?? throw new InvalidOperationException($"{nameof(BrowserRouter)} cannot find any component type with name {componentTypeName}."); } + private string StringUntilAny(string str, char[] chars) + { + var firstIndex = str.IndexOfAny(chars); + return firstIndex < 0 + ? str + : str.Substring(0, firstIndex); + } + private Type FindComponentTypeInAssemblyOrReferences(Assembly assembly, string typeName) => assembly.GetType(typeName, throwOnError: false, ignoreCase: true) ?? assembly.GetReferencedAssemblies() diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Routing/UriHelper.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Routing/UriHelper.cs index da02e965eb..846127b141 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Routing/UriHelper.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Routing/UriHelper.cs @@ -61,14 +61,22 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing /// A relative URI path. public static string ToBaseRelativePath(string baseUriPrefix, string absoluteUri) { - // The absolute URI must be of the form "{baseUriPrefix}/something", - // and from that we return "/something" (also stripping any querystring - // and/or hash value) - if (absoluteUri.StartsWith(baseUriPrefix, StringComparison.Ordinal) + if (absoluteUri.Equals(baseUriPrefix, 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] == '/') { - // TODO: Remove querystring and/or hash + // The absolute URI must be of the form "{baseUriPrefix}/something", + // and from that we return "/something" (also stripping any querystring + // and/or hash value) return absoluteUri.Substring(baseUriPrefix.Length); } diff --git a/test/Microsoft.AspNetCore.Blazor.Browser.Test/UriHelperTest.cs b/test/Microsoft.AspNetCore.Blazor.Browser.Test/UriHelperTest.cs index 24fe9f2419..12261e1154 100644 --- a/test/Microsoft.AspNetCore.Blazor.Browser.Test/UriHelperTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Browser.Test/UriHelperTest.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Test [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) { var actualResult = UriHelper.ToBaseRelativePath(baseUriPrefix, absoluteUri); @@ -35,7 +36,6 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Test [Theory] [InlineData("scheme://host", "otherscheme://host/")] // Mismatched prefix is error [InlineData("scheme://host", "scheme://otherhost/")] // Mismatched prefix is error - [InlineData("scheme://host/path", "scheme://host/path")] // URI isn't within base URI space public void ThrowsForInvalidBaseRelativePaths(string baseUriPrefix, string absoluteUri) { var ex = Assert.Throws(() => diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs new file mode 100644 index 0000000000..6ef63822f6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs @@ -0,0 +1,41 @@ +// 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 BasicTestApp; +using Microsoft.AspNetCore.Blazor.Components; +using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; +using System; + +namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure +{ + public class BasicTestAppTestBase : ServerTestBase> + { + public const string ServerPathBase = "/subdir"; + + public BasicTestAppTestBase(BrowserFixture browserFixture, DevHostServerFixture serverFixture) + : base(browserFixture, serverFixture) + { + serverFixture.PathBase = ServerPathBase; + } + + protected IWebElement MountTestComponent() where TComponent : IComponent + { + var componentTypeName = typeof(TComponent).FullName; + WaitUntilDotNetRunningInBrowser(); + ((IJavaScriptExecutor)Browser).ExecuteScript( + $"mountTestComponent('{componentTypeName}')"); + return Browser.FindElement(By.TagName("app")); + } + + protected void WaitUntilDotNetRunningInBrowser() + { + new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(driver => + { + return ((IJavaScriptExecutor)driver) + .ExecuteScript("return window.isTestReady;"); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs index 3883ccf899..66006e0773 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs @@ -8,23 +8,19 @@ using System.Linq; using System.Numerics; using BasicTestApp; using BasicTestApp.HierarchicalImportsTest.Subdir; -using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using OpenQA.Selenium; -using OpenQA.Selenium.Support.UI; using Xunit; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { - public class ComponentRenderingTest - : ServerTestBase> + public class ComponentRenderingTest : BasicTestAppTestBase { public ComponentRenderingTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) : base(browserFixture, serverFixture) { - serverFixture.PathBase = "/subdir"; - Navigate("/subdir", noReload: true); + Navigate(ServerPathBase, noReload: true); } [Fact] @@ -203,23 +199,5 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests elem => Assert.Equal(typeof(Complex).FullName, elem.Text), elem => Assert.Equal(typeof(AssemblyHashAlgorithm).FullName, elem.Text)); } - - private IWebElement MountTestComponent() where TComponent: IComponent - { - var componentTypeName = typeof(TComponent).FullName; - WaitUntilDotNetRunningInBrowser(); - ((IJavaScriptExecutor)Browser).ExecuteScript( - $"mountTestComponent('{componentTypeName}')"); - return Browser.FindElement(By.TagName("app")); - } - - private void WaitUntilDotNetRunningInBrowser() - { - new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(driver => - { - return ((IJavaScriptExecutor)driver) - .ExecuteScript("return window.isTestReady;"); - }); - } } } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs new file mode 100644 index 0000000000..09c9524b77 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs @@ -0,0 +1,114 @@ +using System; +using BasicTestApp; +using BasicTestApp.RouterTest; +using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; +using OpenQA.Selenium; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests +{ + public class RoutingTest : BasicTestAppTestBase, IDisposable + { + private readonly ServerFixture _server; + + public RoutingTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) + : base(browserFixture, serverFixture) + { + _server = serverFixture; + Navigate(ServerPathBase, noReload: true); + } + + [Fact] + public void CanArriveAtDefaultPage() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/"); + + var app = MountTestComponent(); + Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanArriveAtNonDefaultPage() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other"); + + var app = MountTestComponent(); + Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToOtherPage() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Other")).Click(); + Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToDefaultPage() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Default")).Click(); + Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToOtherPageWithQueryString() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Other with query")).Click(); + Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToDefaultPageWithQueryString() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Default with query")).Click(); + Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToOtherPageWithHash() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Other with hash")).Click(); + Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text); + } + + [Fact] + public void CanFollowLinkToDefaultPageWithHash() + { + SetUrlViaPushState($"{ServerPathBase}/RouterTest/Other"); + + var app = MountTestComponent(); + app.FindElement(By.LinkText("Default with hash")).Click(); + Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text); + } + + public void Dispose() + { + // Clear any existing state + SetUrlViaPushState(ServerPathBase); + MountTestComponent(); + } + + private void SetUrlViaPushState(string relativeUri) + { + var jsExecutor = (IJavaScriptExecutor)Browser; + var absoluteUri = new Uri(_server.RootUri, relativeUri); + jsExecutor.ExecuteScript($"history.pushState(null, '', '{absoluteUri.ToString()}')"); + } + } +} diff --git a/test/testapps/BasicTestApp/Properties/launchSettings.json b/test/testapps/BasicTestApp/Properties/launchSettings.json index ba24c7d2e9..ceb8a2f502 100644 --- a/test/testapps/BasicTestApp/Properties/launchSettings.json +++ b/test/testapps/BasicTestApp/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "http://localhost:63796/subdir", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,6 +19,7 @@ "BasicTestApp": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "http://localhost:63797/subdir", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/test/testapps/BasicTestApp/RouterTest/Default.cshtml b/test/testapps/BasicTestApp/RouterTest/Default.cshtml new file mode 100644 index 0000000000..20b87b9c00 --- /dev/null +++ b/test/testapps/BasicTestApp/RouterTest/Default.cshtml @@ -0,0 +1,3 @@ +@using BasicTestApp.RouterTest +
This is the default page.
+ diff --git a/test/testapps/BasicTestApp/RouterTest/Links.cshtml b/test/testapps/BasicTestApp/RouterTest/Links.cshtml new file mode 100644 index 0000000000..70efead6a4 --- /dev/null +++ b/test/testapps/BasicTestApp/RouterTest/Links.cshtml @@ -0,0 +1,8 @@ + diff --git a/test/testapps/BasicTestApp/RouterTest/Other.cshtml b/test/testapps/BasicTestApp/RouterTest/Other.cshtml new file mode 100644 index 0000000000..c3b66c3033 --- /dev/null +++ b/test/testapps/BasicTestApp/RouterTest/Other.cshtml @@ -0,0 +1,3 @@ +@using BasicTestApp.RouterTest +
This is another page.
+ diff --git a/test/testapps/BasicTestApp/RouterTest/TestRouter.cshtml b/test/testapps/BasicTestApp/RouterTest/TestRouter.cshtml new file mode 100644 index 0000000000..9a5cb63bbb --- /dev/null +++ b/test/testapps/BasicTestApp/RouterTest/TestRouter.cshtml @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Blazor.Browser.Routing +