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<IWebAssemblyHost, WebAssemblyHost>();
|
||||
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
|
||||
|
||||
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
|
||||
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
||||
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
|
||||
{
|
||||
public partial interface IComponentContext
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
}
|
||||
public partial interface IUriHelper
|
||||
{
|
||||
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 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
services.AddScoped<IUriHelper, RemoteUriHelper>();
|
||||
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
|
||||
services.AddScoped<IComponentContext, RemoteComponentContext>();
|
||||
|
||||
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.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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue