Skip to content

[Blazor][Fixes #12788] Fixes blazor reconnection tests #12813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/Components/Web.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ async function initializeConnection(options: BlazorOptions, logger: Logger): Pro
unhandledError(connection, error, logger);
});

window['Blazor']._internal.forceCloseConnection = () => connection.stop();

try {
await connection.start();
} catch (ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ void AssertState(string username)

private void PerformReconnection()
{
((IJavaScriptExecutor)Browser).ExecuteScript("Blazor._internal.forceCloseConnection()");
Browser.ExecuteAsyncScript($"fetch('/WebSockets/Interrupt?WebSockets.Identifier={SessionIdentifier}').then(r => window['WebSockets.{SessionIdentifier}'] = r.ok)");
Browser.HasJavaScriptValue(true, $"window['WebSockets.{SessionIdentifier}']", (r) => r != null);

// Wait until the reconnection dialog has been shown but is now hidden
new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class ServerReconnectionTest : BasicTestAppTestBase
{
public ServerReconnectionTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}

public string SessionIdentifier { get; set; } = Guid.NewGuid().ToString();

protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: false);

Browser.Manage().Cookies.DeleteCookieNamed("WebSockets.Identifier");
Browser.Manage().Cookies.AddCookie(new Cookie("WebSockets.Identifier", SessionIdentifier));
Browser.Navigate().Refresh();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to just replace this with navigation + reload? (line 34)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean line 32? I don't see why

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean line 30. Do we need to navigate to the root and then trigger a refresh?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the server-side case yes because the connection already got established by the time we set the cookies.

These tests don't run client-side.

}

[Fact]
public void ReconnectUI()
{
MountTestComponent<ReconnectComponent>();
Browser.Equal("0", () => Browser.FindElement(By.Id("counter-count")).Text);

var counterButton = Browser.FindElement(By.Id("counter-click"));
for (int i = 0; i < 10; i++)
{
counterButton.Click();
}

Disconnect();

// We should see the 'reconnecting' UI appear
Browser.True(
() => Browser.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "block",
TimeSpan.FromSeconds(10));

// Then it should disappear
Browser.True(() => Browser.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "none",
TimeSpan.FromSeconds(10));

counterButton = Browser.FindElement(By.Id("counter-click"));
for (int i = 0; i < 10; i++)
{
counterButton.Click();
}

Browser.Equal("20", () => Browser.FindElement(By.Id("counter-count")).Text);
}

[Fact]
public void RendersContinueAfterReconnect()
{
MountTestComponent<ReconnectTicker>();

var selector = By.ClassName("tick-value");
var element = Browser.FindElement(selector);

var initialValue = element.Text;

Disconnect();

// We should see the 'reconnecting' UI appear
Browser.True(
() => Browser.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "block",
TimeSpan.FromSeconds(10));

// Then it should disappear
Browser.True(() => Browser.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "none",
TimeSpan.FromSeconds(10));

// We should receive a render that occurred while disconnected
var currentValue = element.Text;
Assert.NotEqual(initialValue, currentValue);

// Verify it continues to tick
new WebDriverWait(Browser, TimeSpan.FromSeconds(10)).Until(
_ => element.Text != currentValue);
}

private void Disconnect()
{
var javascript = (IJavaScriptExecutor)Browser;
Browser.ExecuteAsyncScript($"fetch('/WebSockets/Interrupt?WebSockets.Identifier={SessionIdentifier}').then(r => window['WebSockets.{SessionIdentifier}'] = r.ok)");
Browser.HasJavaScriptValue(true, $"window['WebSockets.{SessionIdentifier}']", (r) => r != null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,52 +138,6 @@ public void HasFetchDataPage()
}
}

[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12788")]
public void ReconnectUI()
{
Browser.FindElement(By.LinkText("Counter")).Click();

var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript("Blazor._internal.forceCloseConnection()");

// We should see the 'reconnecting' UI appear
var reconnectionDialog = WaitUntilReconnectionDialogExists();
Browser.True(() => reconnectionDialog.GetCssValue("display") == "block");

// Then it should disappear
new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
.Until(driver => reconnectionDialog.GetCssValue("display") == "none");
}

[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12788")]
public void RendersContinueAfterReconnect()
{
Browser.FindElement(By.LinkText("Ticker")).Click();
var selector = By.ClassName("tick-value");
var element = Browser.FindElement(selector);

var initialValue = element.Text;

var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript("Blazor._internal.forceCloseConnection()");

// We should see the 'reconnecting' UI appear
var reconnectionDialog = WaitUntilReconnectionDialogExists();
Browser.True(() => reconnectionDialog.GetCssValue("display") == "block");

// Then it should disappear
new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
.Until(driver => reconnectionDialog.GetCssValue("display") == "none");

// We should receive a render that occurred while disconnected
var currentValue = element.Text;
Assert.NotEqual(initialValue, currentValue);

// Verify it continues to tick
new WebDriverWait(Browser, TimeSpan.FromSeconds(10)).Until(
_ => element.Text != currentValue);
}

// Since we've removed stateful prerendering, the name which is passed in
// during prerendering cannot be retained. The first interactive render
// will remove it.
Expand All @@ -204,13 +158,5 @@ public void ErrorsStopTheRenderingProcess()
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("Connection disconnected.")));
}

private IWebElement WaitUntilReconnectionDialogExists()
{
IWebElement reconnectionDialog = null;
new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
.Until(driver => (reconnectionDialog = driver.FindElement(By.Id("components-reconnect-modal"))) != null);
return reconnectionDialog;
}
}
}
8 changes: 8 additions & 0 deletions src/Components/test/E2ETest/Tests/AuthTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
Expand All @@ -22,6 +23,8 @@ public class AuthTest : BasicTestAppTestBase
protected const string PageRequiringPolicy = "Page requiring policy";
protected const string PageRequiringRole = "Page requiring role";

public string SessionIdentifier { get; } = Guid.NewGuid().ToString();

public AuthTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
Expand Down Expand Up @@ -188,6 +191,11 @@ public void Router_RequireRole_NotAuthorized()
protected IWebElement MountAndNavigateToAuthTest(string authLinkText)
{
Navigate(ServerPathBase);

Browser.Manage().Cookies.DeleteCookieNamed("WebSockets.Identifier");
Browser.Manage().Cookies.AddCookie(new Cookie("WebSockets.Identifier", SessionIdentifier));
Browser.Navigate().Refresh();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a drawback to this running for the client-side tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test don't run on the client, when they do, we can simply key this off to whether or not we are running server-side.

var appElement = MountTestComponent<BasicTestApp.AuthTest.AuthRouter>();
WaitUntilExists(By.Id("auth-links"));
appElement.FindElement(By.LinkText(authLinkText)).Click();
Expand Down
2 changes: 2 additions & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
<option value="BasicTestApp.ReconnectComponent">Reconnect component</option>
<option value="BasicTestApp.ReconnectTicker">Reconnect ticker</option>
<option value="BasicTestApp.RedTextComponent">Red text</option>
<option value="BasicTestApp.ReliabilityComponent">Server reliability component</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<h1>Reconnect component</h1>

<p>This component demonstrates reconnection capabilities for blazor server-side. We click this button a few times,
we force a disconnection and we make sure we are able to successfully reconnect and continue interacting with
the application.</p>

<p>Current count: <span id="counter-count">@currentCount</span></p>
<p><button id="counter-click" @onclick="@IncrementCount">Click me</button></p>

@code {
int currentCount = 0;

void IncrementCount() => currentCount++;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
@page "/ticker"
@implements IDisposable
@implements System.IDisposable
@using System.Threading

<h1>Reconnect component</h1>

<p>
This component demonstrates reconnection capabilities for blazor server-side. The ticker starts ticking for a
few seconds. Then the application reconnects and the renders resume updating the ticker value. (The renders happened
while the circuit was disconnected).
</p>

<h1>Ticker</h1>
<p class="tick-value">@tick</p>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="ticker">
<span class="oi oi-list-rich" aria-hidden="true"></span> Ticker
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="greeter">
<span class="oi oi-list-rich" aria-hidden="true"></span> Greeter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;


namespace ComponentsApp.Server
{
public class Startup
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Components.TestServer
{
public class InterruptibleSocketMiddleware
{
public InterruptibleSocketMiddleware(
RequestDelegate next,
InterruptibleWebSocketOptions options)
{
Next = next;
Options = options;
}

public RequestDelegate Next { get; }
public ConcurrentDictionary<string, InterruptibleWebSocket> Registry => Options.Registry;
public InterruptibleWebSocketOptions Options { get; }

public async Task Invoke(HttpContext context)
{
var socketsFeature = context.Features.Get<IHttpWebSocketFeature>();
if (context.Request.Path.Equals(Options.InterruptPath) && context.Request.Query.TryGetValue(Options.WebSocketIdParameterName,out var currentIdentifier))
{
if (Registry.TryGetValue(currentIdentifier, out var webSocket))
{
webSocket.Disable();
return;
}
else
{
context.Response.StatusCode = 400;
return;
}
}

if (context.Request.Path.Equals(Options.WebSocketPath, StringComparison.OrdinalIgnoreCase) &&
context.Request.Cookies.TryGetValue(Options.WebSocketIdParameterName, out var identifier))
{
context.Features.Set<IHttpWebSocketFeature>(new InterruptibleWebSocketFeature(socketsFeature, identifier, Registry));
}

await Next(context);
}
}
}
Loading