From 6e53dac454b624f975444d61580099149cbfc95a Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 12 Jul 2019 19:58:12 +0200 Subject: [PATCH] [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. --- ...soft.AspNetCore.Components.E2ETests.csproj | 1 + .../InteropReliabilityTests.cs | 337 ++++++++++++++++++ .../test/testassets/BasicTestApp/Index.razor | 99 ++--- .../ServerReliability/JSInterop.cs | 38 ++ .../ReliabilityComponent.razor | 40 +++ .../test/testassets/Ignitor/BlazorClient.cs | 251 +++++++++++-- .../test/testassets/Ignitor/ElementNode.cs | 37 ++ .../test/testassets/Ignitor/Error.cs | 4 +- .../test/testassets/Ignitor/Program.cs | 2 +- 9 files changed, 724 insertions(+), 85 deletions(-) create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs create mode 100644 src/Components/test/testassets/BasicTestApp/ServerReliability/JSInterop.cs create mode 100644 src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor diff --git a/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj b/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj index 895b4f171c..9e85027c89 100644 --- a/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj +++ b/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs new file mode 100644 index 0000000000..73e7a1f8b2 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs @@ -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 + { + 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())); + + Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedDotNetObjectRef)); + + await Client.InvokeDotNetMethod( + "1", + null, + "Reverse", + 1, + JsonSerializer.Serialize(Array.Empty())); + + // 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())); + + 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())); + + 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(), + 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 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); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 8f090e6cbe..df71e0cb93 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -1,68 +1,69 @@ @using Microsoft.AspNetCore.Components.RenderTree
- Select test: - - - - + + + + + + + + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + + + + + + - - - - - - + - - + + + + + + + + + + + + + + + + + + + + @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription diff --git a/src/Components/test/testassets/BasicTestApp/ServerReliability/JSInterop.cs b/src/Components/test/testassets/BasicTestApp/ServerReliability/JSInterop.cs new file mode 100644 index 0000000000..f8a858d4f4 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/ServerReliability/JSInterop.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.JSInterop; + +namespace BasicTestApp.ServerReliability +{ + public class JSInterop + { + [JSInvokable] + public static DotNetObjectRef CreateImportant() + { + return DotNetObjectRef.Create(new ImportantInformation()); + } + + [JSInvokable] + public static string ReceiveTrivial(DotNetObjectRef 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"; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor new file mode 100644 index 0000000000..949bf7c150 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor @@ -0,0 +1,40 @@ +@inject Microsoft.JSInterop.IJSRuntime JSRuntime +@namespace BasicTestApp +

Server reliability

+

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. +

+ +

Current count: @currentCount

+

Error = @error

+ + + + +@code +{ + int currentCount = 0; + string error = ""; + + void IncrementCount() + { + currentCount++; + } + + async Task TriggerJSInterop() + { + try + { + var result = await JSRuntime.InvokeAsync( + "sendMalformedCallbackReturn", + Array.Empty()); + } + catch (Exception e) + { + error = e.Message; + } + } +} diff --git a/src/Components/test/testassets/Ignitor/BlazorClient.cs b/src/Components/test/testassets/Ignitor/BlazorClient.cs index 30a8e01061..aced279167 100644 --- a/src/Components/test/testassets/Ignitor/BlazorClient.cs +++ b/src/Components/test/testassets/Ignitor/BlazorClient.cs @@ -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 TaskCompletionSource { get; } + private CancellableOperation NextBatchReceived { get; set; } + + private CancellableOperation NextJSInteropReceived { get; set; } + public bool ConfirmRenderBatch { get; set; } = true; public event Action JSInterop; + public event Action RenderBatchReceived; + public event Action 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 action, TimeSpan? timeout = null) + { + var task = WaitForRenderBatch(timeout); + await action(); + await task; + } + + public async Task ExpectJSInterop(Func 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 ConnectAsync(Uri uri, bool prerendered) { var builder = new HubConnectionBuilder(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - 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("JS.BeginInvokeJS", OnBeginInvokeJS); HubConnection.On("JS.RenderBatch", OnRenderBatch); @@ -75,55 +163,92 @@ namespace Ignitor if (prerendered) { CircuitId = await GetPrerenderedCircuitIdAsync(uri); - return await HubConnection.InvokeAsync("ConnectCircuit", CircuitId); + var result = false; + await ExpectRenderBatch(async () => result = await HubConnection.InvokeAsync("ConnectCircuit", CircuitId)); + return result; } else { - CircuitId = await HubConnection.InvokeAsync("StartCircuit", uri, uri); + await ExpectRenderBatch( + async () => CircuitId = await HubConnection.InvokeAsync("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 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(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 Completion { get; set; } + + public CancellationTokenSource Cancellation { get; set; } + + public CancellationTokenRegistration CancellationRegistration { get; set; } + } } } diff --git a/src/Components/test/testassets/Ignitor/ElementNode.cs b/src/Components/test/testassets/Ignitor/ElementNode.cs index 0e1846a6a4..5ba32154c1 100644 --- a/src/Components/test/testassets/Ignitor/ElementNode.cs +++ b/src/Components/test/testassets/Ignitor/ElementNode.cs @@ -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) diff --git a/src/Components/test/testassets/Ignitor/Error.cs b/src/Components/test/testassets/Ignitor/Error.cs index b2b8e07e67..e452e5d195 100644 --- a/src/Components/test/testassets/Ignitor/Error.cs +++ b/src/Components/test/testassets/Ignitor/Error.cs @@ -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; } } diff --git a/src/Components/test/testassets/Ignitor/Program.cs b/src/Components/test/testassets/Ignitor/Program.cs index ec3a3ede04..c0ffc80fa9 100644 --- a/src/Components/test/testassets/Ignitor/Program.cs +++ b/src/Components/test/testassets/Ignitor/Program.cs @@ -41,7 +41,7 @@ namespace Ignitor { if (batchId < 1000) { - client.ClickAsync("thecounter"); + var _ = client.ClickAsync("thecounter"); } else {