From b83b110284d58318a0196fe969fdde1b15b1833c Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 28 Mar 2019 16:07:42 +0000 Subject: [PATCH 1/4] Basic implementation of IComponentContext with IsConnected flag --- .../src/Hosting/WebAssemblyHostBuilder.cs | 2 +- .../Services/WebAssemblyComponentContext.cs | 12 +++++ .../test/WebAssemblyComponentContextTest.cs | 16 +++++++ .../src/Services/IComponentContext.cs | 19 ++++++++ .../src/Circuits/DefaultCircuitFactory.cs | 2 + .../src/Circuits/RemoteComponentContext.cs | 20 ++++++++ .../ComponentServiceCollectionExtensions.cs | 1 + .../Circuits/RemoteComponentContextTest.cs | 46 +++++++++++++++++++ 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/Components/Blazor/Blazor/src/Services/WebAssemblyComponentContext.cs create mode 100644 src/Components/Blazor/Blazor/test/WebAssemblyComponentContextTest.cs create mode 100644 src/Components/Components/src/Services/IComponentContext.cs create mode 100644 src/Components/Server/src/Circuits/RemoteComponentContext.cs create mode 100644 src/Components/Server/test/Circuits/RemoteComponentContextTest.cs diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs index ed1530d2baba..690b2eaff7e7 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs @@ -88,7 +88,7 @@ private void CreateServiceProvider() services.AddSingleton(_BrowserHostBuilderContext); services.AddSingleton(); services.AddSingleton(WebAssemblyJSRuntime.Instance); - + services.AddSingleton(); services.AddSingleton(WebAssemblyUriHelper.Instance); services.AddSingleton(s => { diff --git a/src/Components/Blazor/Blazor/src/Services/WebAssemblyComponentContext.cs b/src/Components/Blazor/Blazor/src/Services/WebAssemblyComponentContext.cs new file mode 100644 index 000000000000..ff1d9882d83b --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Services/WebAssemblyComponentContext.cs @@ -0,0 +1,12 @@ +// 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.Components.Services; + +namespace Microsoft.AspNetCore.Blazor.Services +{ + internal class WebAssemblyComponentContext : IComponentContext + { + public bool IsConnected => true; + } +} diff --git a/src/Components/Blazor/Blazor/test/WebAssemblyComponentContextTest.cs b/src/Components/Blazor/Blazor/test/WebAssemblyComponentContextTest.cs new file mode 100644 index 000000000000..3923dda5e6cb --- /dev/null +++ b/src/Components/Blazor/Blazor/test/WebAssemblyComponentContextTest.cs @@ -0,0 +1,16 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Blazor.Services.Test +{ + public class WebAssemblyComponentContextTest + { + [Fact] + public void IsConnected() + { + Assert.True(new WebAssemblyComponentContext().IsConnected); + } + } +} diff --git a/src/Components/Components/src/Services/IComponentContext.cs b/src/Components/Components/src/Services/IComponentContext.cs new file mode 100644 index 000000000000..fa851e9e547f --- /dev/null +++ b/src/Components/Components/src/Services/IComponentContext.cs @@ -0,0 +1,19 @@ +// 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. + +namespace Microsoft.AspNetCore.Components.Services +{ + /// + /// Provides information about the environment in which components are executing. + /// + public interface IComponentContext + { + /// + /// Gets a flag to indicate whether there is an active connection to the user's display. + /// + /// During prerendering, the value will always be false. + /// During server-side execution, the value can be true or false depending on whether there is an active SignalR connection. + /// During client-side execution, the value will always be true. + bool IsConnected { get; } + } +} diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index b0a493c25ba6..7618e01c1e1c 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -41,7 +41,9 @@ public override CircuitHost CreateCircuitHost( var scope = _scopeFactory.CreateScope(); var encoder = scope.ServiceProvider.GetRequiredService(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService(); + var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService(); jsRuntime.Initialize(client); + componentContext.Initialize(client); var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService(); if (client != CircuitClientProxy.OfflineClient) diff --git a/src/Components/Server/src/Circuits/RemoteComponentContext.cs b/src/Components/Server/src/Circuits/RemoteComponentContext.cs new file mode 100644 index 000000000000..ecca025e9831 --- /dev/null +++ b/src/Components/Server/src/Circuits/RemoteComponentContext.cs @@ -0,0 +1,20 @@ +// 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; +using Microsoft.AspNetCore.Components.Services; + +namespace Microsoft.AspNetCore.Components.Server.Circuits +{ + internal class RemoteComponentContext : IComponentContext + { + private CircuitClientProxy _clientProxy; + + public bool IsConnected => _clientProxy != null && _clientProxy.Connected; + + internal void Initialize(CircuitClientProxy clientProxy) + { + _clientProxy = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy)); + } + } +} diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index de390a7ee73c..cbb666ef4736 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -44,6 +44,7 @@ public static IServiceCollection AddRazorComponents(this IServiceCollection serv // Standard razor component services implementations services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Components/Server/test/Circuits/RemoteComponentContextTest.cs b/src/Components/Server/test/Circuits/RemoteComponentContextTest.cs new file mode 100644 index 000000000000..0cdd6167ecee --- /dev/null +++ b/src/Components/Server/test/Circuits/RemoteComponentContextTest.cs @@ -0,0 +1,46 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.SignalR; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Browser.Rendering +{ + public class RemoteComponentContextTest + { + [Fact] + public void IfNotInitialized_IsConnectedReturnsFalse() + { + Assert.False(new RemoteComponentContext().IsConnected); + } + + [Fact] + public void IfInitialized_IsConnectedValueDeterminedByCircuitProxy() + { + // Arrange + var clientProxy = new FakeClientProxy(); + var circuitProxy = new CircuitClientProxy(clientProxy, "test connection"); + var remoteComponentContext = new RemoteComponentContext(); + + // Act/Assert: Can observe connected state + remoteComponentContext.Initialize(circuitProxy); + Assert.True(remoteComponentContext.IsConnected); + + // Act/Assert: Can observe disconnected state + circuitProxy.SetDisconnected(); + Assert.False(remoteComponentContext.IsConnected); + } + + private class FakeClientProxy : IClientProxy + { + public Task SendCoreAsync(string method, object[] args, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } + } +} From b66364e962f73eba1fc5120f90bd290b30631d95 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 29 Mar 2019 09:35:39 +0000 Subject: [PATCH 2/4] Update ref assembly code --- .../ref/Microsoft.AspNetCore.Components.netstandard2.0.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 77ce77402b6f..437edd95a814 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -836,6 +836,10 @@ public enum NavLinkMatch } namespace Microsoft.AspNetCore.Components.Services { + public partial interface IComponentContext + { + bool IsConnected { get; } + } public partial interface IUriHelper { event System.EventHandler OnLocationChanged; From 5b59ea966160540ec84088444a991c759b37bffa Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 29 Mar 2019 11:33:35 +0000 Subject: [PATCH 3/4] Begin infrastructure for prerendered E2E tests --- .../TestServer/Pages/PrerenderedHost.cshtml | 14 ++++++++++++++ .../test/testassets/TestServer/Startup.cs | 14 ++++++++++++++ .../test/testassets/TestServer/TestServer.csproj | 3 ++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml new file mode 100644 index 000000000000..3da96f5d98d8 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml @@ -0,0 +1,14 @@ +@page +@using BasicTestApp.RouterTest + + + + Prerendering tests + + + + @(await Html.RenderComponentAsync()) + + + + diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index 492aa9391ab2..86f4acdcc1bb 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -1,4 +1,5 @@ using BasicTestApp; +using BasicTestApp.RouterTest; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Hosting; @@ -68,6 +69,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + // Separately, mount a prerendered server-side Blazor app on /prerendered + app.Map("/prerendered", subdirApp => + { + subdirApp.UsePathBase("/prerendered"); + subdirApp.UseStaticFiles(); + subdirApp.UseRouting(); + subdirApp.UseEndpoints(endpoints => + { + endpoints.MapFallbackToPage("/PrerenderedHost"); + endpoints.MapComponentHub("app"); + }); + }); } private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app) diff --git a/src/Components/test/testassets/TestServer/TestServer.csproj b/src/Components/test/testassets/TestServer/TestServer.csproj index 289ad93655e5..281e0fbca387 100644 --- a/src/Components/test/testassets/TestServer/TestServer.csproj +++ b/src/Components/test/testassets/TestServer/TestServer.csproj @@ -1,7 +1,8 @@ - + netcoreapp3.0 + true From edf03470022e98f48bf5a31cf3d40400961e6252 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 29 Mar 2019 12:03:08 +0000 Subject: [PATCH 4/4] Actual E2E test for prerendered-to-interactive transition --- .../ServerExecutionTests/PrerenderingTest.cs | 47 +++++++++++++++++++ .../PrerenderedToInteractiveTransition.razor | 20 ++++++++ .../TestServer/Pages/PrerenderedHost.cshtml | 15 +++++- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor diff --git a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs new file mode 100644 index 000000000000..38673a9639cc --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs @@ -0,0 +1,47 @@ +// 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.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests +{ + public class PrerenderingTest : ServerTestBase + { + public PrerenderingTest( + BrowserFixture browserFixture, + AspNetSiteServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + _serverFixture.Environment = AspNetEnvironment.Development; + _serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; + } + + [Fact] + public void CanTransitionFromPrerenderedToInteractiveMode() + { + Navigate("/prerendered/prerendered-transition"); + + // Prerendered output shows "not connected" + Browser.Equal("not connected", () => Browser.FindElement(By.Id("connected-state")).Text); + + // Once connected, output changes + BeginInteractivity(); + Browser.Equal("connected", () => Browser.FindElement(By.Id("connected-state")).Text); + + // ... and now the counter works + Browser.FindElement(By.Id("increment-count")).Click(); + Browser.Equal("1", () => Browser.FindElement(By.Id("count")).Text); + } + + private void BeginInteractivity() + { + Browser.FindElement(By.Id("load-boot-script")).Click(); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor new file mode 100644 index 000000000000..749b49078222 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor @@ -0,0 +1,20 @@ +@page "/prerendered-transition" +@using Microsoft.AspNetCore.Components.Services +@inject IComponentContext ComponentContext + +

Hello

+ +

+ Current state: + @(ComponentContext.IsConnected ? "connected" : "not connected") +

+ +

+ Clicks: + @count + +

+ +@functions { + int count; +} diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml index 3da96f5d98d8..02c033a91771 100644 --- a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml @@ -9,6 +9,19 @@ @(await Html.RenderComponentAsync()) - + @* + So that E2E tests can make assertions about both the prerendered and + interactive states, we only load the .js file when told to. + *@ +
+ +