diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index d1e851f090..0d7c7336dd 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -649,12 +649,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { _intializationStarted = LoggerMessage.Define( LogLevel.Debug, - EventIds.InitializationFailed, + EventIds.InitializationStarted, "Circuit initialization started."); _intializationSucceded = LoggerMessage.Define( LogLevel.Debug, - EventIds.InitializationFailed, + EventIds.InitializationSucceeded, "Circuit initialization succeeded."); _intializationFailed = LoggerMessage.Define( diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs new file mode 100644 index 0000000000..8621107ec4 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubInvalidEventTest.cs @@ -0,0 +1,138 @@ +// 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.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.E2ETest; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests +{ + public class ComponentHubInvalidEventTest : IgnitorTest + { + public ComponentHubInvalidEventTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) + : base(serverFixture, output) + { + } + + protected override void InitializeFixture(AspNetSiteServerFixture serverFixture) + { + serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; + } + + protected async override Task InitializeAsync() + { + var rootUri = ServerFixture.RootUri; + Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app"); + Assert.Single(Batches); + + await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent"); + Assert.Equal(2, Batches.Count); + } + + [Fact] + public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings() + { + // Arrange + var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + + $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data."; + + var eventDescriptor = Serialize(new WebEventDescriptor() + { + BrowserRendererId = 0, + EventHandlerId = 3, + EventArgsType = "mouse", + }); + + // Act + await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( + "DispatchBrowserEvent", + eventDescriptor, + "{sadfadsf]")); + + // Assert + var actualError = Assert.Single(Errors); + Assert.Equal(expectedError, actualError); + Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); + Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'.")); + } + + [Fact] + public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings() + { + // Arrange + var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + + $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event."; + + var eventDescriptor = Serialize(new WebEventDescriptor() + { + BrowserRendererId = 0, + EventHandlerId = 1990, + EventArgsType = "mouse", + }); + + var eventArgs = new MouseEventArgs + { + Type = "click", + Detail = 1, + ScreenX = 47, + ScreenY = 258, + ClientX = 47, + ClientY = 155, + }; + + // Act + await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( + "DispatchBrowserEvent", + eventDescriptor, + Serialize(eventArgs))); + + // Assert + var actualError = Assert.Single(Errors); + Assert.Equal(expectedError, actualError); + Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); + Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) == + (LogLevel.Debug, + "There was an error dispatching the event '1990' to the application.", + "There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')")); + } + + [Fact] + public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings() + { + // Arrange + var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + + $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'."; + + + Client.ConfirmRenderBatch = false; + await Client.ClickAsync("counter"); + + // Act + await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( + "OnRenderCompleted", + 1846, + null)); + + // Assert + var actualError = Assert.Single(Errors); + Assert.Equal(expectedError, actualError); + Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); + + var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); + Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message); + Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message); + } + + private string Serialize(T browserEventDescriptor) => + JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options); + } +} diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs index a1401a4aca..8d770e64de 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -13,58 +11,22 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { - public class ComponentHubReliabilityTest : IClassFixture, IDisposable + public class ComponentHubReliabilityTest : IgnitorTest { - private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10); - private readonly AspNetSiteServerFixture _serverFixture; - public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) + : base(serverFixture, output) { - _serverFixture = serverFixture; - Output = output; + } + protected override void InitializeFixture(AspNetSiteServerFixture serverFixture) + { serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; - CreateDefaultConfiguration(); - } - - public BlazorClient Client { get; set; } - public ITestOutputHelper Output { get; set; } - private IList Batches { get; set; } = new List(); - private IList Errors { get; set; } = new List(); - private ConcurrentQueue Logs { get; set; } = new ConcurrentQueue(); - - public TestSink TestSink { get; set; } - - private void CreateDefaultConfiguration() - { - Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; - Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); - Client.OnCircuitError += (error) => Errors.Add(error); - Client.LoggerProvider = new XunitLoggerProvider(Output); - Client.FormatError = (error) => - { - var logs = string.Join(Environment.NewLine, Logs); - return new Exception(error + Environment.NewLine + logs); - }; - - _ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI. - TestSink = _serverFixture.Host.Services.GetRequiredService(); - TestSink.MessageLogged += LogMessages; - } - - private void LogMessages(WriteContext context) - { - var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception); - Logs.Enqueue(log); - Output.WriteLine(log.ToString()); } [Fact] @@ -72,10 +34,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "The circuit host '.*?' has already been initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); + var descriptors = await Client.GetPrerenderDescriptors(baseUri); // Act @@ -96,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "The uris provided are invalid."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var uri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app"); var descriptors = await Client.GetPrerenderDescriptors(uri); @@ -117,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "The circuit failed to initialize."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var uri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app"); var descriptors = await Client.GetPrerenderDescriptors(uri); @@ -138,7 +101,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Circuit not initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.Empty(Batches); @@ -164,7 +127,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Circuit not initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.Empty(Batches); @@ -188,7 +151,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Circuit not initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.Empty(Batches); @@ -206,125 +169,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization")); } - private async Task GoToTestComponent(IList batches) - { - var rootUri = _serverFixture.RootUri; - Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app"); - Assert.Single(batches); - - await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent"); - Assert.Equal(2, batches.Count); - } - - [Fact] - public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings() - { - // Arrange - var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + - $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data."; - - var eventDescriptor = Serialize(new WebEventDescriptor() - { - BrowserRendererId = 0, - EventHandlerId = 3, - EventArgsType = "mouse", - }); - - await GoToTestComponent(Batches); - Assert.Equal(2, Batches.Count); - - // Act - await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( - "DispatchBrowserEvent", - eventDescriptor, - "{sadfadsf]")); - - // Assert - var actualError = Assert.Single(Errors); - Assert.Equal(expectedError, actualError); - Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); - Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'.")); - } - - [Fact] - public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings() - { - // Arrange - var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + - $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event."; - - var eventDescriptor = Serialize(new WebEventDescriptor() - { - BrowserRendererId = 0, - EventHandlerId = 1990, - EventArgsType = "mouse", - }); - - var eventArgs = new MouseEventArgs - { - Type = "click", - Detail = 1, - ScreenX = 47, - ScreenY = 258, - ClientX = 47, - ClientY = 155, - }; - - await GoToTestComponent(Batches); - Assert.Equal(2, Batches.Count); - - // Act - await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( - "DispatchBrowserEvent", - eventDescriptor, - Serialize(eventArgs))); - - // Assert - var actualError = Assert.Single(Errors); - Assert.Equal(expectedError, actualError); - Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); - Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) == - (LogLevel.Debug, - "There was an error dispatching the event '1990' to the application.", - "There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')")); - } - - [Fact] - public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings() - { - // Arrange - var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " + - $"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'."; - - await GoToTestComponent(Batches); - Assert.Equal(2, Batches.Count); - - Client.ConfirmRenderBatch = false; - await Client.ClickAsync("counter"); - - // Act - await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( - "OnRenderCompleted", - 1846, - null)); - - // Assert - var actualError = Assert.Single(Errors); - Assert.Equal(expectedError, actualError); - Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); - - var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed"); - Assert.Equal(LogLevel.Debug, entry.LogLevel); - Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message); - Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message); - } - [Fact] public async Task CannotInvokeOnRenderCompletedBeforeInitialization() { // Arrange var expectedError = "Circuit not initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.Empty(Batches); @@ -347,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Circuit not initialized."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.Empty(Batches); @@ -373,7 +223,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " + "Location change to 'http://example.com' failed."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); @@ -402,7 +252,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " + "Location change failed."; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); @@ -421,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests var entry = Assert.Single(Logs, l => l.EventId.Name == "LocationChangeFailed"); Assert.Equal(LogLevel.Error, entry.LogLevel); - Assert.Matches($"Location change to '{new Uri(_serverFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message); + Assert.Matches($"Location change to '{new Uri(ServerFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message); } [Theory] @@ -436,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Unhandled exception in circuit .*"; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); @@ -467,7 +317,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { // Arrange var expectedError = "Unhandled exception in circuit .*"; - var rootUri = _serverFixture.RootUri; + var rootUri = ServerFixture.RootUri; var baseUri = new Uri(rootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); @@ -493,47 +343,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Logs, e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError)); } - - public void Dispose() - { - TestSink.MessageLogged -= LogMessages; - } - - private string Serialize(T browserEventDescriptor) => - JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options); - - [DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")] - private class LogMessage - { - public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception) - { - LogLevel = logLevel; - EventId = eventId; - Message = message; - Exception = exception; - } - - public LogLevel LogLevel { get; set; } - public EventId EventId { get; set; } - public string Message { get; set; } - public Exception Exception { get; set; } - - public override string ToString() - { - return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}"; - } - } - - private class Batch - { - public Batch(int id, byte[] data) - { - Id = id; - Data = data; - } - - public int Id { get; } - public byte[] Data { get; } - } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/IgnitorTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/IgnitorTest.cs new file mode 100644 index 0000000000..ed9cd36c68 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/IgnitorTest.cs @@ -0,0 +1,131 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Ignitor; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components +{ + // Base class for Ignitor-based tests. + public abstract class IgnitorTest : IClassFixture, IAsyncLifetime + where TFixture : ServerFixture + { + private static readonly TimeSpan DefaultTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(30); + + protected IgnitorTest(TFixture serverFixture, ITestOutputHelper output) + { + ServerFixture = serverFixture; + Output = output; + } + + protected BlazorClient Client { get; private set; } + + protected ConcurrentQueue Logs { get; } = new ConcurrentQueue(); + + protected ITestOutputHelper Output { get; } + + protected TFixture ServerFixture { get; } + + protected TimeSpan Timeout { get; set; } = DefaultTimeout; + + private TestSink TestSink { get; set; } + + protected IReadOnlyCollection Batches => Client?.Operations?.Batches; + + protected IReadOnlyCollection DotNetCompletions => Client?.Operations?.DotNetCompletions; + + protected IReadOnlyCollection Errors => Client?.Operations?.Errors; + + protected IReadOnlyCollection JSInteropCalls => Client?.Operations?.JSInteropCalls; + + // Called to initialize the fixture as part of InitializeAsync. + protected virtual void InitializeFixture(TFixture serverFixture) + { + } + + async Task IAsyncLifetime.InitializeAsync() + { + Client = new BlazorClient() + { + CaptureOperations = true, + DefaultOperationTimeout = Timeout, + }; + Client.LoggerProvider = new XunitLoggerProvider(Output); + Client.FormatError = (error) => + { + var logs = string.Join(Environment.NewLine, Logs); + return new Exception(error + Environment.NewLine + logs); + }; + + InitializeFixture(ServerFixture); + _ = ServerFixture.RootUri; // This is needed for the side-effects of starting the server. + + if (ServerFixture is WebHostServerFixture hostFixture) + { + TestSink = hostFixture.Host.Services.GetRequiredService(); + TestSink.MessageLogged += TestSink_MessageLogged; + } + + await InitializeAsync(); + } + + async Task IAsyncLifetime.DisposeAsync() + { + if (TestSink != null) + { + TestSink.MessageLogged -= TestSink_MessageLogged; + } + + await DisposeAsync(); + } + + protected virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + protected virtual Task DisposeAsync() + { + return Task.CompletedTask; + } + + private void TestSink_MessageLogged(WriteContext context) + { + var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception); + Logs.Enqueue(log); + Output.WriteLine(log.ToString()); + } + + [DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")] + protected sealed class LogMessage + { + public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception) + { + LogLevel = logLevel; + EventId = eventId; + Message = message; + Exception = exception; + } + + public LogLevel LogLevel { get; set; } + public EventId EventId { get; set; } + public string Message { get; set; } + public Exception Exception { get; set; } + + public override string ToString() + { + return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}"; + } + } + } +} diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs index ff81e5e337..a848669b78 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -21,55 +20,26 @@ using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { - [Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.All)] - public class InteropReliabilityTests : IClassFixture, IDisposable + public class InteropReliabilityTests : IgnitorTest { - private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30); - private readonly AspNetSiteServerFixture _serverFixture; - public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) + : base(serverFixture, output) { - _serverFixture = serverFixture; - Output = output; + } + protected override void InitializeFixture(AspNetSiteServerFixture serverFixture) + { serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; - CreateDefaultConfiguration(); } - public BlazorClient Client { get; set; } - public ITestOutputHelper Output { get; set; } - private IList Batches { get; set; } = new List(); - private List DotNetCompletions = new List(); - private List JSInteropCalls = new List(); - private IList Errors { get; set; } = new List(); - private ConcurrentQueue Logs { get; set; } = new ConcurrentQueue(); - - public TestSink TestSink { get; set; } - - private void CreateDefaultConfiguration() + protected async override Task InitializeAsync() { - Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; - Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); - Client.DotNetInteropCompletion += (method) => DotNetCompletions.Add(new DotNetCompletion(method)); - Client.JSInterop += (asyncHandle, identifier, argsJson) => JSInteropCalls.Add(new JSInteropCall(asyncHandle, identifier, argsJson)); - Client.OnCircuitError += (error) => Errors.Add(error); - Client.LoggerProvider = new XunitLoggerProvider(Output); - Client.FormatError = (error) => - { - var logs = string.Join(Environment.NewLine, Logs); - return new Exception(error + Environment.NewLine + logs); - }; + var rootUri = ServerFixture.RootUri; + Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app"); + Assert.Single(Batches); - _ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI. - TestSink = _serverFixture.Host.Services.GetRequiredService(); - TestSink.MessageLogged += LogMessages; - } - - private void LogMessages(WriteContext context) - { - var log = new LogMessage(context.LogLevel, context.Message, context.Exception); - Logs.Enqueue(log); - Output.WriteLine(log.ToString()); + await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent"); + Assert.Equal(2, Batches.Count); } [Fact] @@ -79,7 +49,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests 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.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); // Act await Client.InvokeDotNetMethod( @@ -90,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -102,8 +71,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", @@ -113,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -125,18 +92,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", "Microsoft.AspNetCore.Components.Server", "NotifyLocationChanged", null, - JsonSerializer.Serialize(new[] { _serverFixture.RootUri })); + JsonSerializer.Serialize(new[] { ServerFixture.RootUri })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -148,18 +113,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", "", "NotifyLocationChanged", null, - JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false })); + JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -171,18 +134,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", "Microsoft.AspNetCore.Components.Server", "", null, - JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false })); + JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -196,8 +157,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", @@ -206,7 +165,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests null, JsonSerializer.Serialize(Array.Empty())); - Assert.Single(DotNetCompletions, c => c.Message == expectedDotNetObjectRef); + Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef); await Client.InvokeDotNetMethod( "1", @@ -216,7 +175,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests JsonSerializer.Serialize(Array.Empty())); // Assert - Assert.Single(DotNetCompletions, c => c.Message == "[\"1\",true,\"tnatropmI\"]"); + Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]"); await Client.InvokeDotNetMethod( "1", @@ -225,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests 3, // non existing ref JsonSerializer.Serialize(Array.Empty())); - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -238,8 +197,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - await Client.InvokeDotNetMethod( "1", "BasicTestApp", @@ -247,7 +204,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests null, JsonSerializer.Serialize(Array.Empty())); - Assert.Single(DotNetCompletions, c => c.Message == expectedImportantDotNetObjectRef); + Assert.Single(DotNetCompletions, c => c == expectedImportantDotNetObjectRef); // Act await Client.InvokeDotNetMethod( @@ -258,19 +215,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } })); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] - [Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)] 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."; - await GoToTestComponent(Batches); - // Act await Client.ClickAsync("triggerjsinterop-malformed"); @@ -278,11 +232,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.NotEqual(default, call); var id = call.AsyncHandle; - await Client.HubConnection.InvokeAsync( - "EndInvokeJSFromDotNet", - id, - true, - $"[{id}, true, \"{{\"]"); + await Client.ExpectRenderBatch(async () => + { + await Client.HubConnection.InvokeAsync( + "EndInvokeJSFromDotNet", + id, + true, + $"[{id}, true, \"{{\"]"); + }); var text = Assert.Single( Client.FindElementById("errormessage-malformed").Children.OfType(), @@ -295,10 +252,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task JSInteropCompletionSuccess() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act await Client.ClickAsync("triggerjsinterop-success"); @@ -307,27 +260,27 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.NotEqual(default, call); var id = call.AsyncHandle; - await Client.HubConnection.InvokeAsync( - "EndInvokeJSFromDotNet", - id++, - true, - $"[{id}, true, null]"); + await Client.ExpectRenderBatch(async () => + { + await Client.HubConnection.InvokeAsync( + "EndInvokeJSFromDotNet", + id, + true, + $"[{id}, true, null]"); + }); Assert.Single( Client.FindElementById("errormessage-success").Children.OfType(), e => "" == e.TextContent); - Assert.Contains((LogLevel.Debug, "EndInvokeJSSucceeded"), logEvents); + var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSSucceeded"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); } [Fact] public async Task JSInteropThrowsInUserCode() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act await Client.ClickAsync("triggerjsinterop-failure"); @@ -349,9 +302,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Client.FindElementById("errormessage-failure").Children.OfType(), e => "There was an error invoking sendFailureCallbackReturn" == e.TextContent); - Assert.Contains((LogLevel.Debug, "EndInvokeJSFailed"), logEvents); + var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSFailed"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); - Assert.DoesNotContain(logEvents, m => m.logLevel > LogLevel.Information); + Assert.DoesNotContain(Logs, m => m.LogLevel > LogLevel.Information); await ValidateClientKeepsWorking(Client, Batches); } @@ -360,10 +314,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task MalformedJSInteropCallbackDisposesCircuit() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act await Client.ClickAsync("triggerjsinterop-malformed"); @@ -386,7 +336,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Client.FindElementById("errormessage-malformed").Children.OfType(), e => "" == e.TextContent); - Assert.Contains((LogLevel.Debug, "EndInvokeDispatchException"), logEvents); + var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeDispatchException"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); await Client.ExpectCircuitErrorAndDisconnect(async () => { @@ -402,8 +353,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", @@ -413,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "[ \"invalidPayload\"}"); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -425,8 +374,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "false," + "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; - await GoToTestComponent(Batches); - // Act await Client.InvokeDotNetMethod( "1", @@ -436,7 +383,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "[ { \"data\": {\"}} ]"); // Assert - Assert.Single(DotNetCompletions, c => c.Message == expectedError); + Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } @@ -444,10 +391,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act await Client.ExpectCircuitError(async () => @@ -458,9 +401,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests null); }); - Assert.Contains( - (LogLevel.Debug, "DispatchEventFailedToParseEventData"), - logEvents); + var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => @@ -473,10 +415,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task DispatchingEventsWithInvalidEventDescriptor() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act await Client.ExpectCircuitError(async () => @@ -487,9 +425,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "{}"); }); - Assert.Contains( - (LogLevel.Debug, "DispatchEventFailedToParseEventData"), - logEvents); + var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData"); + Assert.Equal(LogLevel.Debug, entry.LogLevel); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => @@ -502,10 +439,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task DispatchingEventsWithInvalidEventArgs() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception)); // Act var browserDescriptor = new WebEventDescriptor() @@ -524,9 +457,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests }); Assert.Contains( - logEvents, - e => e.eventIdName == "DispatchEventFailedToParseEventData" && e.logLevel == LogLevel.Debug && - e.exception.Message == "There was an error parsing the event arguments. EventId: '6'."); + Logs, + e => e.EventId.Name == "DispatchEventFailedToParseEventData" && e.LogLevel == LogLevel.Debug && + e.Exception.Message == "There was an error parsing the event arguments. EventId: '6'."); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => @@ -539,10 +472,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task DispatchingEventsWithInvalidEventHandlerId() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception)); // Act var mouseEventArgs = new MouseEventArgs() @@ -566,9 +495,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests }); Assert.Contains( - logEvents, - e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug && - e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'.")); + Logs, + e => e.EventId.Name == "DispatchEventFailedToDispatchEvent" && e.LogLevel == LogLevel.Debug && + e.Exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'.")); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => @@ -581,21 +510,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit() { // Arrange - await GoToTestComponent(Batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception)); // Act - await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); - - await Task.Delay(1000); + await Client.ExpectCircuitError(async () => + { + await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); + }); Assert.Contains( - logEvents, - e => LogLevel.Error == e.logLevel && - "CircuitUnhandledException" == e.eventIdName && - "Handler threw an exception" == e.exception.Message); + Logs, + e => LogLevel.Error == e.LogLevel && + "CircuitUnhandledException" == e.EventId.Name && + "Handler threw an exception" == e.Exception.Message); // Now if you try to click again, you will get *forcibly* disconnected for trying to talk to // a circuit that's gone. @@ -605,7 +531,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests }); } - private Task ValidateClientKeepsWorking(BlazorClient Client, IList batches) => + private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection batches) => ValidateClientKeepsWorking(Client, () => batches.Count); private async Task ValidateClientKeepsWorking(BlazorClient Client, Func countAccessor) @@ -615,75 +541,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.Equal(currentBatches + 1, countAccessor()); } - - private async Task GoToTestComponent(IList batches) - { - var rootUri = _serverFixture.RootUri; - Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app"); - Assert.Single(batches); - - await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent"); - Assert.Equal(2, batches.Count); - } - - public void Dispose() - { - TestSink.MessageLogged -= LogMessages; - } - - private class LogMessage - { - public LogMessage(LogLevel logLevel, string message, Exception exception) - { - LogLevel = logLevel; - Message = message; - Exception = exception; - } - - public LogLevel LogLevel { get; set; } - public string Message { get; set; } - public Exception Exception { get; set; } - - public override string ToString() - { - return $"{LogLevel}: {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}"; - } - } - - private class Batch - { - public Batch(int id, byte[] data) - { - Id = id; - Data = data; - } - - public int Id { get; } - public byte[] Data { get; } - } - - private class DotNetCompletion - { - public DotNetCompletion(string message) - { - Message = message; - } - - public string Message { get; } - } - - private class JSInteropCall - { - public JSInteropCall(int asyncHandle, string identifier, string argsJson) - { - AsyncHandle = asyncHandle; - Identifier = identifier; - ArgsJson = argsJson; - } - - public int AsyncHandle { get; } - public string Identifier { get; } - public string ArgsJson { get; } - } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs index 360adc4b95..d77d2e6572 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs @@ -2,55 +2,33 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Ignitor; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { - public class RemoteRendererBufferLimitTest : IClassFixture, IDisposable + public class RemoteRendererBufferLimitTest : IgnitorTest { - private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500); - - private AspNetSiteServerFixture _serverFixture; - - public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture) + public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) + : base(serverFixture, output) { - serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; - _serverFixture = serverFixture; - - // Needed here for side-effects - _ = _serverFixture.RootUri; - - Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; - Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); - - Sink = _serverFixture.Host.Services.GetRequiredService(); - Sink.MessageLogged += LogMessages; } - public BlazorClient Client { get; set; } - - private IList Batches { get; set; } = new List(); - - // We use a stack so that we can search the logs in reverse order - private ConcurrentStack Logs { get; set; } = new ConcurrentStack(); - - public TestSink Sink { get; private set; } + protected override void InitializeFixture(AspNetSiteServerFixture serverFixture) + { + serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; + } [Fact] public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged() { // Arrange - var baseUri = new Uri(_serverFixture.RootUri, "/subdir"); + var baseUri = new Uri(ServerFixture.RootUri, "/subdir"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.Single(Batches); @@ -75,48 +53,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Client.ConfirmRenderBatch = true; // This will resume the render batches. - await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id)); + await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches.Last().Id)); // Assert Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); Assert.Equal(fullCount + 1, Batches.Count); } - - private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception)); - - [DebuggerDisplay("{Message,nq}")] - private class LogMessage - { - public LogMessage(LogLevel logLevel, string message, Exception exception) - { - LogLevel = logLevel; - Message = message; - Exception = exception; - } - - public LogLevel LogLevel { get; set; } - public string Message { get; set; } - public Exception Exception { get; set; } - } - - private class Batch - { - public Batch(int id, byte[] data) - { - Id = id; - Data = data; - } - - public int Id { get; } - public byte[] Data { get; } - } - - public void Dispose() - { - if (Sink != null) - { - Sink.MessageLogged -= LogMessages; - } - } } } diff --git a/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor index 8557fa95c0..13256fb7bc 100644 --- a/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor @@ -77,7 +77,7 @@ { int currentCount = 0; string errorMalformed = ""; - string errorSuccess = ""; + string errorSuccess = "(this will be cleared)"; string errorFailure = ""; bool showConstructorThrow; bool showAttachThrow; @@ -116,6 +116,10 @@ var result = await JSRuntime.InvokeAsync( "sendSuccessCallbackReturn", Array.Empty()); + + // Make sure we trigger a render when the interop call completes. + // The test uses a render to synchronize. + errorSuccess = ""; } catch (Exception e) { diff --git a/src/Components/test/testassets/Ignitor/BlazorClient.cs b/src/Components/test/testassets/Ignitor/BlazorClient.cs index 4985af2207..3f96604574 100644 --- a/src/Components/test/testassets/Ignitor/BlazorClient.cs +++ b/src/Components/test/testassets/Ignitor/BlazorClient.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; @@ -33,8 +31,20 @@ namespace Ignitor }); } - public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500); - public TimeSpan? DefaultConnectTimeout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan? DefaultConnectionTimeout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan? DefaultOperationTimeout { get; set; } = TimeSpan.FromMilliseconds(500); + + /// + /// Gets or sets a value that determines whether the client will capture data such + /// as render batches, interop calls, and errors for later inspection. + /// + public bool CaptureOperations { get; set; } + + /// + /// Gets the collections of operation results that are captured when + /// is true. + /// + public Operations Operations { get; private set; } public Func FormatError { get; set; } @@ -44,23 +54,23 @@ namespace Ignitor private TaskCompletionSource TaskCompletionSource { get; } - private CancellableOperation NextBatchReceived { get; set; } + private CancellableOperation NextBatchReceived { get; set; } - private CancellableOperation NextErrorReceived { get; set; } + private CancellableOperation NextErrorReceived { get; set; } - private CancellableOperation NextDisconnect { get; set; } + private CancellableOperation NextDisconnect { get; set; } - private CancellableOperation NextJSInteropReceived { get; set; } + private CancellableOperation NextJSInteropReceived { get; set; } - private CancellableOperation NextDotNetInteropCompletionReceived { get; set; } + private CancellableOperation NextDotNetInteropCompletionReceived { get; set; } public ILoggerProvider LoggerProvider { get; set; } public bool ConfirmRenderBatch { get; set; } = true; - public event Action JSInterop; + public event Action JSInterop; - public event Action RenderBatchReceived; + public event Action RenderBatchReceived; public event Action DotNetInteropCompletion; @@ -70,66 +80,66 @@ namespace Ignitor public ElementHive Hive { get; set; } = new ElementHive(); - public bool ImplicitWait => DefaultLatencyTimeout != null; + public bool ImplicitWait => DefaultOperationTimeout != null; public HubConnection HubConnection { get; set; } - public Task PrepareForNextBatch(TimeSpan? timeout) + public Task PrepareForNextBatch(TimeSpan? timeout) { if (NextBatchReceived?.Completion != null) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextBatchReceived = new CancellableOperation(timeout); + NextBatchReceived = new CancellableOperation(timeout); return NextBatchReceived.Completion.Task; } - public Task PrepareForNextJSInterop(TimeSpan? timeout) + public Task PrepareForNextJSInterop(TimeSpan? timeout) { if (NextJSInteropReceived?.Completion != null) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextJSInteropReceived = new CancellableOperation(timeout); + NextJSInteropReceived = new CancellableOperation(timeout); return NextJSInteropReceived.Completion.Task; } - public Task PrepareForNextDotNetInterop(TimeSpan? timeout) + public Task PrepareForNextDotNetInterop(TimeSpan? timeout) { if (NextDotNetInteropCompletionReceived?.Completion != null) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextDotNetInteropCompletionReceived = new CancellableOperation(timeout); + NextDotNetInteropCompletionReceived = new CancellableOperation(timeout); return NextDotNetInteropCompletionReceived.Completion.Task; } - public Task PrepareForNextCircuitError(TimeSpan? timeout) + public Task PrepareForNextCircuitError(TimeSpan? timeout) { if (NextErrorReceived?.Completion != null) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextErrorReceived = new CancellableOperation(timeout); + NextErrorReceived = new CancellableOperation(timeout); return NextErrorReceived.Completion.Task; } - public Task PrepareForNextDisconnect(TimeSpan? timeout) + public Task PrepareForNextDisconnect(TimeSpan? timeout) { if (NextDisconnect?.Completion != null) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextDisconnect = new CancellableOperation(timeout); + NextDisconnect = new CancellableOperation(timeout); return NextDisconnect.Completion.Task; } @@ -160,115 +170,162 @@ namespace Ignitor return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value)); } - public async Task ExpectRenderBatch(Func action, TimeSpan? timeout = null) + public async Task ExpectRenderBatch(Func action, TimeSpan? timeout = null) { var task = WaitForRenderBatch(timeout); await action(); - await task; + return await task; } - public async Task ExpectJSInterop(Func action, TimeSpan? timeout = null) + public async Task ExpectJSInterop(Func action, TimeSpan? timeout = null) { var task = WaitForJSInterop(timeout); await action(); - await task; + return await task; } - public async Task ExpectDotNetInterop(Func action, TimeSpan? timeout = null) + public async Task ExpectDotNetInterop(Func action, TimeSpan? timeout = null) { var task = WaitForDotNetInterop(timeout); await action(); - await task; + return await task; } - public async Task ExpectCircuitError(Func action, TimeSpan? timeout = null) + public async Task ExpectCircuitError(Func action, TimeSpan? timeout = null) { var task = WaitForCircuitError(timeout); await action(); - await task; + return await task; } - public async Task ExpectCircuitErrorAndDisconnect(Func action, TimeSpan? timeout = null) - { - // NOTE: timeout is used for each operation individually. - await ExpectDisconnect(async () => - { - await ExpectCircuitError(action, timeout); - }, timeout); - } - - public async Task ExpectDisconnect(Func action, TimeSpan? timeout = null) + public async Task ExpectDisconnect(Func action, TimeSpan? timeout = null) { var task = WaitForDisconnect(timeout); await action(); - await task; + return await task; } - private Task WaitForRenderBatch(TimeSpan? timeout = null) + public async Task<(string error, Exception exception)> ExpectCircuitErrorAndDisconnect(Func action, TimeSpan? timeout = null) + { + string error = null; + + // NOTE: timeout is used for each operation individually. + var exception = await ExpectDisconnect(async () => + { + error = await ExpectCircuitError(action, timeout); + }, timeout); + + return (error, exception); + } + + private async Task WaitForRenderBatch(TimeSpan? timeout = null) { if (ImplicitWait) { - if (DefaultLatencyTimeout == null && timeout == null) + if (DefaultOperationTimeout == null && timeout == null) { throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); } - return PrepareForNextBatch(timeout ?? DefaultLatencyTimeout); + try + { + return await PrepareForNextBatch(timeout ?? DefaultOperationTimeout); + } + catch (OperationCanceledException) + { + throw FormatError("Timed out while waiting for batch."); + } } - return Task.CompletedTask; + return null; } - private async Task WaitForJSInterop(TimeSpan? timeout = null) + private async Task WaitForJSInterop(TimeSpan? timeout = null) { if (ImplicitWait) { - if (DefaultLatencyTimeout == null && timeout == null) + if (DefaultOperationTimeout == null && timeout == null) { throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); } - await PrepareForNextJSInterop(timeout ?? DefaultLatencyTimeout); + try + { + return await PrepareForNextJSInterop(timeout ?? DefaultOperationTimeout); + } + catch (OperationCanceledException) + { + throw FormatError("Timed out while waiting for JS Interop."); + } } + + return null; } - private async Task WaitForDotNetInterop(TimeSpan? timeout = null) + private async Task WaitForDotNetInterop(TimeSpan? timeout = null) { if (ImplicitWait) { - if (DefaultLatencyTimeout == null && timeout == null) + if (DefaultOperationTimeout == null && timeout == null) { throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); } - await PrepareForNextDotNetInterop(timeout ?? DefaultLatencyTimeout); + try + { + return await PrepareForNextDotNetInterop(timeout ?? DefaultOperationTimeout); + } + catch (OperationCanceledException) + { + throw FormatError("Timed out while waiting for .NET interop."); + } } + + return null; } - private async Task WaitForCircuitError(TimeSpan? timeout = null) + private async Task WaitForCircuitError(TimeSpan? timeout = null) { if (ImplicitWait) { - if (DefaultLatencyTimeout == null && timeout == null) + if (DefaultOperationTimeout == null && timeout == null) { throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); } - await PrepareForNextCircuitError(timeout ?? DefaultLatencyTimeout); - } - } - - private async Task WaitForDisconnect(TimeSpan? timeout = null) - { - if (ImplicitWait) - { - if (DefaultLatencyTimeout == null && timeout == null) + try { - throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); + return await PrepareForNextCircuitError(timeout ?? DefaultOperationTimeout); + } + catch (OperationCanceledException) + { + throw FormatError("Timed out while waiting for circuit error."); } } - await PrepareForNextDisconnect(timeout ?? DefaultLatencyTimeout); + return null; + } + + private async Task WaitForDisconnect(TimeSpan? timeout = null) + { + if (ImplicitWait) + { + if (DefaultOperationTimeout == null && timeout == null) + { + throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); + } + + try + { + return await PrepareForNextDisconnect(timeout ?? DefaultOperationTimeout); + } + catch (OperationCanceledException) + { + throw FormatError("Timed out while waiting for disconnect."); + } + } + + return null; } public async Task ConnectAsync(Uri uri, bool connectAutomatically = true) @@ -294,6 +351,11 @@ namespace Ignitor HubConnection.On("JS.Error", OnError); HubConnection.Closed += OnClosedAsync; + if (CaptureOperations) + { + Operations = new Operations(); + } + if (!connectAutomatically) { return true; @@ -302,35 +364,41 @@ namespace Ignitor var descriptors = await GetPrerenderDescriptors(uri); await ExpectRenderBatch( async () => CircuitId = await HubConnection.InvokeAsync("StartCircuit", uri, uri, descriptors), - DefaultConnectTimeout); + DefaultConnectionTimeout); return CircuitId != null; } - private void OnEndInvokeDotNet(string completion) + private void OnEndInvokeDotNet(string message) { - DotNetInteropCompletion?.Invoke(completion); + Operations?.DotNetCompletions.Enqueue(message); + DotNetInteropCompletion?.Invoke(message); NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null); } private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson) { - JSInterop?.Invoke(asyncHandle, identifier, argsJson); + var call = new CapturedJSInteropCall(asyncHandle, identifier, argsJson); + Operations?.JSInteropCalls.Enqueue(call); + JSInterop?.Invoke(call); NextJSInteropReceived?.Completion?.TrySetResult(null); } - private void OnRenderBatch(int batchId, byte[] batchData) + private void OnRenderBatch(int id, byte[] data) { - RenderBatchReceived?.Invoke(batchId, batchData); + var capturedBatch = new CapturedRenderBatch(id, data); - var batch = RenderBatchReader.Read(batchData); + Operations?.Batches.Enqueue(capturedBatch); + RenderBatchReceived?.Invoke(capturedBatch); + + var batch = RenderBatchReader.Read(data); Hive.Update(batch); if (ConfirmRenderBatch) { - _ = ConfirmBatch(batchId); + _ = ConfirmBatch(id); } NextBatchReceived?.Completion?.TrySetResult(null); @@ -343,8 +411,12 @@ namespace Ignitor private void OnError(string error) { + Operations?.Errors.Enqueue(error); OnCircuitError?.Invoke(error); + // If we get an error, forcibly terminate anything else we're waiting for. These + // tests should only encounter errors in specific situations, and this ensures that + // we fail with a good message. var exception = FormatError?.Invoke(error) ?? new Exception(error); NextBatchReceived?.Completion?.TrySetException(exception); NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception); @@ -415,7 +487,7 @@ namespace Ignitor return element; } - private class CancellableOperation + private class CancellableOperation { public CancellableOperation(TimeSpan? timeout) { @@ -423,24 +495,32 @@ namespace Ignitor Initialize(); } + public TimeSpan? Timeout { get; } + + public TaskCompletionSource Completion { get; set; } + + public CancellationTokenSource Cancellation { get; set; } + + public CancellationTokenRegistration CancellationRegistration { get; set; } + private void Initialize() { - Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); Completion.Task.ContinueWith( (task, state) => { - var operation = (CancellableOperation)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) + if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue) { Cancellation = new CancellationTokenSource(Timeout.Value); CancellationRegistration = Cancellation.Token.Register( (self) => { - var operation = (CancellableOperation)self; + var operation = (CancellableOperation)self; operation.Completion.TrySetCanceled(operation.Cancellation.Token); operation.Cancellation.Dispose(); operation.CancellationRegistration.Dispose(); @@ -455,14 +535,6 @@ namespace Ignitor Cancellation.Dispose(); CancellationRegistration.Dispose(); } - - public TimeSpan? Timeout { get; } - - public TaskCompletionSource Completion { get; set; } - - public CancellationTokenSource Cancellation { get; set; } - - public CancellationTokenRegistration CancellationRegistration { get; set; } } private string[] ReadMarkers(string content) diff --git a/src/Components/test/testassets/Ignitor/CapturedJSInteropCall.cs b/src/Components/test/testassets/Ignitor/CapturedJSInteropCall.cs new file mode 100644 index 0000000000..4af491a58a --- /dev/null +++ b/src/Components/test/testassets/Ignitor/CapturedJSInteropCall.cs @@ -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 Ignitor +{ + public class CapturedJSInteropCall + { + public CapturedJSInteropCall(int asyncHandle, string identifier, string argsJson) + { + AsyncHandle = asyncHandle; + Identifier = identifier; + ArgsJson = argsJson; + } + + public int AsyncHandle { get; } + public string Identifier { get; } + public string ArgsJson { get; } + } +} diff --git a/src/Components/test/testassets/Ignitor/CapturedRenderBatch.cs b/src/Components/test/testassets/Ignitor/CapturedRenderBatch.cs new file mode 100644 index 0000000000..df2de87e9f --- /dev/null +++ b/src/Components/test/testassets/Ignitor/CapturedRenderBatch.cs @@ -0,0 +1,17 @@ +// 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 +{ + public class CapturedRenderBatch + { + public CapturedRenderBatch(int id, byte[] data) + { + Id = id; + Data = data; + } + + public int Id { get; } + public byte[] Data { get; } + } +} diff --git a/src/Components/test/testassets/Ignitor/Operations.cs b/src/Components/test/testassets/Ignitor/Operations.cs new file mode 100644 index 0000000000..4dcf8798b5 --- /dev/null +++ b/src/Components/test/testassets/Ignitor/Operations.cs @@ -0,0 +1,18 @@ +// 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.Collections.Concurrent; + +namespace Ignitor +{ + public sealed class Operations + { + public ConcurrentQueue Batches { get; } = new ConcurrentQueue(); + + public ConcurrentQueue DotNetCompletions { get; } = new ConcurrentQueue(); + + public ConcurrentQueue Errors { get; } = new ConcurrentQueue(); + + public ConcurrentQueue JSInteropCalls { get; } = new ConcurrentQueue(); + } +} diff --git a/src/Components/test/testassets/Ignitor/Program.cs b/src/Components/test/testassets/Ignitor/Program.cs index 8f5785e7d3..43486d490e 100644 --- a/src/Components/test/testassets/Ignitor/Program.cs +++ b/src/Components/test/testassets/Ignitor/Program.cs @@ -2,10 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Net.Http; -using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -38,9 +34,9 @@ namespace Ignitor var done = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // Click the counter button 1000 times - client.RenderBatchReceived += (int batchId, byte[] data) => + client.RenderBatchReceived += (batch) => { - if (batchId < 1000) + if (batch.Id < 1000) { var _ = client.ClickAsync("thecounter"); } @@ -56,8 +52,8 @@ namespace Ignitor return 0; } - private static void OnJSInterop(int callId, string identifier, string argsJson) => - Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")"); + private static void OnJSInterop(CapturedJSInteropCall call) => + Console.WriteLine("JS Invoke: " + call.Identifier + " (" + call.ArgsJson + ")"); public Program() {