[Blazor] Adds E2E reliability tests for JS interop (#11958)

* Adds tests using ignitor to cover a variety of JS interop scenarios.
* Validates that these errors don't bring down the circuit.
This commit is contained in:
Javier Calvarro Nelson 2019-07-12 19:58:12 +02:00 committed by GitHub
parent 840c226e28
commit 6e53dac454
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 724 additions and 85 deletions

View File

@ -38,6 +38,7 @@
<ProjectReference Include="..\testassets\ComponentsApp.App\ComponentsApp.App.csproj" />
<ProjectReference Include="..\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj" />
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\testassets\Ignitor\Ignitor.csproj" />
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
</ItemGroup>

View File

@ -0,0 +1,337 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Xunit;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
{
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
private readonly AspNetSiteServerFixture _serverFixture;
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture)
{
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
_serverFixture = serverFixture;
}
public BlazorClient Client { get; set; } = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
[Fact]
public async Task CannotInvokeNonJSInvokableMethods()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"System.IO.FileSystem",
"WriteAllText",
null,
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeNonExistingMethods()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"BasicTestApp",
"MadeUpMethod",
null,
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsWithWrongNumberOfArguments()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"Microsoft.AspNetCore.Components.Server",
"NotifyLocationChanged",
null,
JsonSerializer.Serialize(new[] { _serverFixture.RootUri }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsEmptyAssemblyName()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"",
"NotifyLocationChanged",
null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsEmptyMethodName()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"Microsoft.AspNetCore.Components.Server",
"",
null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsWithWrongReferenceId()
{
// Arrange
var expectedDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]";
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027Reverse\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"BasicTestApp",
"CreateImportant",
null,
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedDotNetObjectRef));
await Client.InvokeDotNetMethod(
"1",
null,
"Reverse",
1,
JsonSerializer.Serialize(Array.Empty<object>()));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", "[\"1\",true,\"tnatropmI\"]"));
await Client.InvokeDotNetMethod(
"1",
null,
"Reverse",
3, // non existing ref
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsWrongReferenceIdType()
{
// Arrange
var expectedImportantDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]";
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
await Client.InvokeDotNetMethod(
"1",
"BasicTestApp",
"CreateImportant",
null,
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedImportantDotNetObjectRef));
// Act
await Client.InvokeDotNetMethod(
"1",
"BasicTestApp",
"ReceiveTrivial",
null,
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
{
// Arrange
var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details.";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.ClickAsync("triggerjsinterop");
Assert.Single(interopCalls, (4, "sendMalformedCallbackReturn", (string)null));
await Client.InvokeDotNetMethod(
0,
"Microsoft.JSInterop",
"DotNetDispatcher.EndInvoke",
null,
"[4, true, \"{\"]");
var text = Assert.Single(
Client.FindElementById("errormessage").Children.OfType<TextNode>(),
e => expectedError == e.TextContent);
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsWithInvalidArgumentsPayload()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"Microsoft.AspNetCore.Components.Server",
"NotifyLocationChanged",
null,
"[ \"invalidPayload\"}");
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
[Fact]
public async Task CannotInvokeJSInvokableMethodsWithMalformedArgumentPayload()
{
// Arrange
var expectedError = "[\"1\"," +
"false," +
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
var (interopCalls, batches) = ConfigureClient();
await GoToTestComponent(batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"BasicTestApp",
"ReceiveTrivial",
null,
"[ { \"data\": {\"}} ]");
// Assert
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
await ValidateClientKeepsWorking(Client, batches);
}
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) =>
ValidateClientKeepsWorking(Client, () => batches.Count);
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
{
var currentBatches = countAccessor();
await Client.ClickAsync("thecounter");
Assert.Equal(currentBatches + 1, countAccessor());
}
private async Task GoToTestComponent(List<(int, int, byte[])> batches)
{
var rootUri = _serverFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
Assert.Equal(2, batches.Count);
}
private (List<(int, string, string)>, List<(int, int, byte[])>) ConfigureClient()
{
var interopCalls = new List<(int, string, string)>();
Client.JSInterop += (int arg1, string arg2, string arg3) => interopCalls.Add((arg1, arg2, arg3));
var batches = new List<(int, int, byte[])>();
Client.RenderBatchReceived += (id, renderer, data) => batches.Add((id, renderer, data));
return (interopCalls, batches);
}
}
}

View File

@ -1,68 +1,69 @@
@using Microsoft.AspNetCore.Components.RenderTree
<div id="test-selector">
Select test:
<select @bind=SelectedComponentTypeName>
Select test:
<select id="test-selector-select" @bind=SelectedComponentTypeName>
<option value="none">Choose...</option>
<option value="BasicTestApp.InteropComponent">Interop component</option>
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
<option value="BasicTestApp.CounterComponent">Counter</option>
<option value="BasicTestApp.CounterComponentUsingChild">Counter using child component</option>
<option value="BasicTestApp.CounterComponentWrapper">Counter wrapped in parent</option>
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
<option value="BasicTestApp.InputEventComponent">Input events</option>
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
<option value="BasicTestApp.RedTextComponent">Red text</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
<option value="BasicTestApp.CulturePicker">Culture Picker</option>
<option value="BasicTestApp.DataDashComponent">data-* attribute rendering</option>
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
<option value="BasicTestApp.SvgComponent">SVG</option>
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
<option value="BasicTestApp.RouterTest.TestRouterWithoutNotFoundContent">Router without NotFoundContent</option>
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
<option value="BasicTestApp.FormsTest.SimpleValidationComponent">Simple validation</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
<option value="BasicTestApp.InputEventComponent">Input events</option>
<option value="BasicTestApp.InteropComponent">Interop component</option>
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
<option value="BasicTestApp.KeyCasesComponent">Key cases</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
<option value="BasicTestApp.LaggyTypingComponent">Laggy typing</option>
<option value="BasicTestApp.CulturePicker">Culture Picker</option>
<option value="BasicTestApp.LocalizedText">Localized Text</option>
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
<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.RedTextComponent">Red text</option>
<option value="BasicTestApp.ReliabilityComponent">Server reliability component</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
<option value="BasicTestApp.RouterTest.TestRouterWithoutNotFoundContent">Router without NotFoundContent</option>
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
<option value="BasicTestApp.SvgComponent">SVG</option>
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
</select>
<span id="runtime-info"><code><tt>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</tt></code></span>

View File

@ -0,0 +1,38 @@
using System;
using Microsoft.JSInterop;
namespace BasicTestApp.ServerReliability
{
public class JSInterop
{
[JSInvokable]
public static DotNetObjectRef<ImportantInformation> CreateImportant()
{
return DotNetObjectRef.Create(new ImportantInformation());
}
[JSInvokable]
public static string ReceiveTrivial(DotNetObjectRef<TrivialInformation> information)
{
return information.Value.Message;
}
}
public class ImportantInformation
{
public string Message { get; set; } = "Important";
[JSInvokable]
public string Reverse()
{
var messageChars = Message.ToCharArray();
Array.Reverse(messageChars);
return new string(messageChars);
}
}
public class TrivialInformation
{
public string Message { get; set; } = "Trivial";
}
}

View File

@ -0,0 +1,40 @@
@inject Microsoft.JSInterop.IJSRuntime JSRuntime
@namespace BasicTestApp
<h1>Server reliability</h1>
<p>This component is used on the server-side execution model to validate that the circuit is resilient to failures, intentional or not.
The tests that use this component trigger invalid .NET calls to which the server replies with proper JS interop messages indicating a
failure to perform the call.
We include a counter to ensure that the circuit is still alive and working fine after the error.
</p>
<p>Current count: @currentCount</p>
<p id="errormessage">Error = @error</p>
<button id="triggerjsinterop" @onclick="@TriggerJSInterop"></button>
<button id="thecounter" @onclick="@IncrementCount">Click me</button>
@code
{
int currentCount = 0;
string error = "";
void IncrementCount()
{
currentCount++;
}
async Task TriggerJSInterop()
{
try
{
var result = await JSRuntime.InvokeAsync<int>(
"sendMalformedCallbackReturn",
Array.Empty<object>());
}
catch (Exception e)
{
error = e.Message;
}
}
}

View File

@ -7,7 +7,6 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
@ -27,44 +26,133 @@ namespace Ignitor
{
TaskCompletionSource.TrySetCanceled();
});
ImplicitWait = DefaultLatencyTimeout != null;
}
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
private CancellationTokenSource CancellationTokenSource { get; }
private CancellationToken CancellationToken => CancellationTokenSource.Token;
private TaskCompletionSource<object> TaskCompletionSource { get; }
private CancellableOperation NextBatchReceived { get; set; }
private CancellableOperation NextJSInteropReceived { get; set; }
public bool ConfirmRenderBatch { get; set; } = true;
public event Action<int, string, string> JSInterop;
public event Action<int, int, byte[]> RenderBatchReceived;
public event Action<Error> OnCircuitError;
public string CircuitId { get; set; }
public ElementHive Hive { get; set; } = new ElementHive();
public bool ImplicitWait { get; set; }
public HubConnection HubConnection { get; set; }
public Task ClickAsync(string elementId)
public Task PrepareForNextBatch(TimeSpan? timeout)
{
if (NextBatchReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextBatchReceived = new CancellableOperation(timeout);
return NextBatchReceived.Completion.Task;
}
public Task PrepareForNextJSInterop()
{
if (NextJSInteropReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextJSInteropReceived = new CancellableOperation(DefaultLatencyTimeout);
return NextJSInteropReceived.Completion.Task;
}
public async Task ClickAsync(string elementId)
{
if (!Hive.TryFindElementById(elementId, out var elementNode))
{
throw new InvalidOperationException($"Could not find element with id {elementId}.");
}
return elementNode.ClickAsync(HubConnection);
await ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
}
public ElementHive Hive { get; set; }
public async Task SelectAsync(string elementId, string value)
{
if (!Hive.TryFindElementById(elementId, out var elementNode))
{
throw new InvalidOperationException($"Could not find element with id {elementId}.");
}
await ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
}
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
{
var task = WaitForRenderBatch(timeout);
await action();
await task;
}
public async Task ExpectJSInterop(Func<Task> action)
{
var task = WaitForJSInterop();
await action();
await task;
}
private Task WaitForRenderBatch(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
return PrepareForNextBatch(timeout ?? DefaultLatencyTimeout);
}
return Task.CompletedTask;
}
private async Task WaitForJSInterop()
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
await PrepareForNextJSInterop();
}
}
public async Task<bool> ConnectAsync(Uri uri, bool prerendered)
{
var builder = new HubConnectionBuilder();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
builder.WithUrl(new Uri(uri, "_blazor/"));
builder.WithUrl(GetHubUrl(uri));
builder.ConfigureLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Trace));
var hive = new ElementHive();
HubConnection = builder.Build();
await HubConnection.StartAsync(CancellationToken);
Console.WriteLine("Connected");
HubConnection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
HubConnection.On<int, int, byte[]>("JS.RenderBatch", OnRenderBatch);
@ -75,55 +163,92 @@ namespace Ignitor
if (prerendered)
{
CircuitId = await GetPrerenderedCircuitIdAsync(uri);
return await HubConnection.InvokeAsync<bool>("ConnectCircuit", CircuitId);
var result = false;
await ExpectRenderBatch(async () => result = await HubConnection.InvokeAsync<bool>("ConnectCircuit", CircuitId));
return result;
}
else
{
CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri);
await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", new Uri(uri.GetLeftPart(UriPartial.Authority)), uri),
TimeSpan.FromSeconds(10));
return CircuitId != null;
}
}
void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
{
try
{
JSInterop?.Invoke(asyncHandle, identifier, argsJson);
}
void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
NextJSInteropReceived?.Completion?.TrySetResult(null);
}
catch (Exception e)
{
NextJSInteropReceived?.Completion?.TrySetException(e);
}
}
private void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
{
try
{
RenderBatchReceived?.Invoke(browserRendererId, batchId, batchData);
var batch = RenderBatchReader.Read(batchData);
hive.Update(batch);
Hive.Update(batch);
if (ConfirmRenderBatch)
{
HubConnection.InvokeAsync("OnRenderCompleted", batchId, /* error */ null);
}
RenderBatchReceived?.Invoke(browserRendererId, batchId, batchData);
NextBatchReceived?.Completion?.TrySetResult(null);
}
void OnError(Error error)
catch (Exception e)
{
Console.WriteLine("ERROR: " + error.Stack);
}
Task OnClosedAsync(Exception ex)
{
if (ex == null)
{
TaskCompletionSource.TrySetResult(null);
}
else
{
TaskCompletionSource.TrySetException(ex);
}
return Task.CompletedTask;
NextBatchReceived?.Completion?.TrySetResult(e);
}
}
void InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson)
private void OnError(Error error)
{
HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson);
OnCircuitError?.Invoke(error);
}
private Task OnClosedAsync(Exception ex)
{
if (ex == null)
{
TaskCompletionSource.TrySetResult(null);
}
else
{
TaskCompletionSource.TrySetException(ex);
}
return Task.CompletedTask;
}
private Uri GetHubUrl(Uri uri)
{
if (uri.Segments.Length == 1)
{
return new Uri(uri, "_blazor");
}
else
{
var builder = new UriBuilder(uri);
builder.Path += builder.Path.EndsWith("/") ? "_blazor" : "/_blazor";
return builder.Uri;
}
}
public async Task InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson)
{
await ExpectJSInterop(() => HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson));
}
private static async Task<string> GetPrerenderedCircuitIdAsync(Uri uri)
@ -144,5 +269,65 @@ namespace Ignitor
CancellationTokenSource.Cancel();
CancellationTokenSource.Dispose();
}
public ElementNode FindElementById(string id)
{
if (!Hive.TryFindElementById(id, out var element))
{
throw new InvalidOperationException("Element not found.");
}
return element;
}
private class CancellableOperation
{
public CancellableOperation(TimeSpan? timeout)
{
Timeout = timeout;
Initialize();
}
private void Initialize()
{
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
Completion.Task.ContinueWith(
(task, state) =>
{
var operation = (CancellableOperation)state;
operation.Dispose();
},
this,
TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
if (Timeout != null)
{
Cancellation = new CancellationTokenSource(Timeout.Value);
CancellationRegistration = Cancellation.Token.Register(
(self) =>
{
var operation = (CancellableOperation)self;
operation.Completion.TrySetCanceled(operation.Cancellation.Token);
operation.Cancellation.Dispose();
operation.CancellationRegistration.Dispose();
},
this);
}
}
private void Dispose()
{
Completion = null;
Cancellation.Dispose();
CancellationRegistration.Dispose();
}
public TimeSpan? Timeout { get; }
public TaskCompletionSource<object> Completion { get; set; }
public CancellationTokenSource Cancellation { get; set; }
public CancellationTokenRegistration CancellationRegistration { get; set; }
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
@ -62,6 +63,42 @@ namespace Ignitor
_events[eventName] = descriptor;
}
internal async Task SelectAsync(HubConnection connection, string value)
{
if (!Events.TryGetValue("change", out var changeEventDescriptor))
{
throw new InvalidOperationException("Element does not have a click event.");
}
var mouseEventArgs = new UIChangeEventArgs()
{
Type = changeEventDescriptor.EventName,
Value = value
};
var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = changeEventDescriptor.EventId,
EventArgsType = "change",
EventFieldInfo = new EventFieldInfo
{
ComponentId = 0,
FieldValue = value
}
};
var serializedJson = JsonSerializer.Serialize(mouseEventArgs, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var argsObject = new object[] { browserDescriptor, serializedJson };
var callId = "0";
var assemblyName = "Microsoft.AspNetCore.Components.Web";
var methodIdentifier = "DispatchEvent";
var dotNetObjectId = 0;
var clickArgs = JsonSerializer.Serialize(argsObject, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
await connection.InvokeAsync("BeginInvokeDotNetFromJS", callId, assemblyName, methodIdentifier, dotNetObjectId, clickArgs);
}
public class ElementEventDescriptor
{
public ElementEventDescriptor(string eventName, int eventId)

View File

@ -1,9 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Ignitor
{
internal class Error
public class Error
{
public string Stack { get; set; }
}

View File

@ -41,7 +41,7 @@ namespace Ignitor
{
if (batchId < 1000)
{
client.ClickAsync("thecounter");
var _ = client.ClickAsync("thecounter");
}
else
{