Supply "IsConnected" state info to components (#8888)

* Basic implementation of IComponentContext with IsConnected flag

* Update ref assembly code

* Begin infrastructure for prerendered E2E tests

* Actual E2E test for prerendered-to-interactive transition
This commit is contained in:
Steve Sanderson 2019-03-29 14:53:29 +00:00 committed by GitHub
parent 49b074d3c0
commit 77d9fae439
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 231 additions and 2 deletions

View File

@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton(_BrowserHostBuilderContext);
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
services.AddSingleton<HttpClient>(s =>
{

View File

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

View File

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

View File

@ -836,6 +836,10 @@ namespace Microsoft.AspNetCore.Components.Routing
}
namespace Microsoft.AspNetCore.Components.Services
{
public partial interface IComponentContext
{
bool IsConnected { get; }
}
public partial interface IUriHelper
{
event System.EventHandler<string> OnLocationChanged;

View File

@ -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
{
/// <summary>
/// Provides information about the environment in which components are executing.
/// </summary>
public interface IComponentContext
{
/// <summary>
/// Gets a flag to indicate whether there is an active connection to the user's display.
/// </summary>
/// <example>During prerendering, the value will always be false.</example>
/// <example>During server-side execution, the value can be true or false depending on whether there is an active SignalR connection.</example>
/// <example>During client-side execution, the value will always be true.</example>
bool IsConnected { get; }
}
}

View File

@ -41,7 +41,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var scope = _scopeFactory.CreateScope();
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
jsRuntime.Initialize(client);
componentContext.Initialize(client);
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
if (client != CircuitClientProxy.OfflineClient)

View File

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

View File

@ -44,6 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Standard razor component services implementations
services.AddScoped<IUriHelper, RemoteUriHelper>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<IComponentContext, RemoteComponentContext>();
return services;
}

View File

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

View File

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

View File

@ -0,0 +1,20 @@
@page "/prerendered-transition"
@using Microsoft.AspNetCore.Components.Services
@inject IComponentContext ComponentContext
<h1>Hello</h1>
<p>
Current state:
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
</p>
<p>
Clicks:
<strong id="count">@count</strong>
<button id="increment-count" onclick="@(() => count++)">Click me</button>
</p>
@functions {
int count;
}

View File

@ -0,0 +1,27 @@
@page
@using BasicTestApp.RouterTest
<!DOCTYPE html>
<html>
<head>
<title>Prerendering tests</title>
<base href="~/" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<TestRouter>())</app>
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<button id="load-boot-script" onclick="loadBootScript(event)">Load boot script</button>
<script>
function loadBootScript(event) {
event.srcElement.disabled = true;
var scriptElem = document.createElement('script');
scriptElem.src = '_framework/components.server.js';
document.body.appendChild(scriptElem);
}
</script>
</body>
</html>

View File

@ -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 @@ namespace TestServer
{
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<TestRouter>("app");
});
});
}
private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app)

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>