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:
parent
49b074d3c0
commit
77d9fae439
|
|
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
services.AddSingleton(_BrowserHostBuilderContext);
|
services.AddSingleton(_BrowserHostBuilderContext);
|
||||||
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
|
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
|
||||||
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
|
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
|
||||||
|
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
|
||||||
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
||||||
services.AddSingleton<HttpClient>(s =>
|
services.AddSingleton<HttpClient>(s =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -836,6 +836,10 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
}
|
}
|
||||||
namespace Microsoft.AspNetCore.Components.Services
|
namespace Microsoft.AspNetCore.Components.Services
|
||||||
{
|
{
|
||||||
|
public partial interface IComponentContext
|
||||||
|
{
|
||||||
|
bool IsConnected { get; }
|
||||||
|
}
|
||||||
public partial interface IUriHelper
|
public partial interface IUriHelper
|
||||||
{
|
{
|
||||||
event System.EventHandler<string> OnLocationChanged;
|
event System.EventHandler<string> OnLocationChanged;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,7 +41,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var scope = _scopeFactory.CreateScope();
|
var scope = _scopeFactory.CreateScope();
|
||||||
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
|
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
|
||||||
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
|
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
|
||||||
|
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
|
||||||
jsRuntime.Initialize(client);
|
jsRuntime.Initialize(client);
|
||||||
|
componentContext.Initialize(client);
|
||||||
|
|
||||||
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
|
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
|
||||||
if (client != CircuitClientProxy.OfflineClient)
|
if (client != CircuitClientProxy.OfflineClient)
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
// Standard razor component services implementations
|
// Standard razor component services implementations
|
||||||
services.AddScoped<IUriHelper, RemoteUriHelper>();
|
services.AddScoped<IUriHelper, RemoteUriHelper>();
|
||||||
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
|
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
|
||||||
|
services.AddScoped<IComponentContext, RemoteComponentContext>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using BasicTestApp;
|
using BasicTestApp;
|
||||||
|
using BasicTestApp.RouterTest;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Components.Server;
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
@ -68,6 +69,19 @@ namespace TestServer
|
||||||
{
|
{
|
||||||
endpoints.MapControllers();
|
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)
|
private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue