Merge pull request #13226 from aspnet/rynowak/interop-reliability
Improve reliability of reliability tests
This commit is contained in:
commit
733218c652
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<AspNetSiteServerFixture>
|
||||
{
|
||||
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>(T browserEventDescriptor) =>
|
||||
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AspNetSiteServerFixture>, IDisposable
|
||||
public class ComponentHubReliabilityTest : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
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<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
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>();
|
||||
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<Batch> 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>(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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TFixture> : IClassFixture<TFixture>, 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<LogMessage> Logs { get; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
protected ITestOutputHelper Output { get; }
|
||||
|
||||
protected TFixture ServerFixture { get; }
|
||||
|
||||
protected TimeSpan Timeout { get; set; } = DefaultTimeout;
|
||||
|
||||
private TestSink TestSink { get; set; }
|
||||
|
||||
protected IReadOnlyCollection<CapturedRenderBatch> Batches => Client?.Operations?.Batches;
|
||||
|
||||
protected IReadOnlyCollection<string> DotNetCompletions => Client?.Operations?.DotNetCompletions;
|
||||
|
||||
protected IReadOnlyCollection<string> Errors => Client?.Operations?.Errors;
|
||||
|
||||
protected IReadOnlyCollection<CapturedJSInteropCall> 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>();
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AspNetSiteServerFixture>, IDisposable
|
||||
public class InteropReliabilityTests : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
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<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private List<DotNetCompletion> DotNetCompletions = new List<DotNetCompletion>();
|
||||
private List<JSInteropCall> JSInteropCalls = new List<JSInteropCall>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
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>();
|
||||
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<object>()));
|
||||
|
||||
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<object>()));
|
||||
|
||||
// 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<object>()));
|
||||
|
||||
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<object>()));
|
||||
|
||||
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<TextNode>(),
|
||||
|
|
@ -295,10 +252,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task JSInteropCompletionSuccess()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
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<TextNode>(),
|
||||
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<TestSink>();
|
||||
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<TextNode>(),
|
||||
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<TestSink>();
|
||||
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<TextNode>(),
|
||||
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<TestSink>();
|
||||
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<TestSink>();
|
||||
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<TestSink>();
|
||||
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<TestSink>();
|
||||
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<TestSink>();
|
||||
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<Batch> batches) =>
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection<CapturedRenderBatch> batches) =>
|
||||
ValidateClientKeepsWorking(Client, () => batches.Count);
|
||||
|
||||
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
|
||||
|
|
@ -615,75 +541,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
Assert.Equal(currentBatches + 1, countAccessor());
|
||||
}
|
||||
|
||||
private async Task GoToTestComponent(IList<Batch> 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AspNetSiteServerFixture>, IDisposable
|
||||
public class RemoteRendererBufferLimitTest : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
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<TestSink>();
|
||||
Sink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
|
||||
// We use a stack so that we can search the logs in reverse order
|
||||
private ConcurrentStack<LogMessage> Logs { get; set; } = new ConcurrentStack<LogMessage>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<object>(
|
||||
"sendSuccessCallbackReturn",
|
||||
Array.Empty<object>());
|
||||
|
||||
// Make sure we trigger a render when the interop call completes.
|
||||
// The test uses a render to synchronize.
|
||||
errorSuccess = "";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines whether the client will capture data such
|
||||
/// as render batches, interop calls, and errors for later inspection.
|
||||
/// </summary>
|
||||
public bool CaptureOperations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collections of operation results that are captured when <see cref="CaptureOperations"/>
|
||||
/// is true.
|
||||
/// </summary>
|
||||
public Operations Operations { get; private set; }
|
||||
|
||||
public Func<string, Exception> FormatError { get; set; }
|
||||
|
||||
|
|
@ -44,23 +54,23 @@ namespace Ignitor
|
|||
|
||||
private TaskCompletionSource<object> TaskCompletionSource { get; }
|
||||
|
||||
private CancellableOperation NextBatchReceived { get; set; }
|
||||
private CancellableOperation<CapturedRenderBatch> NextBatchReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextErrorReceived { get; set; }
|
||||
private CancellableOperation<string> NextErrorReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDisconnect { get; set; }
|
||||
private CancellableOperation<Exception> NextDisconnect { get; set; }
|
||||
|
||||
private CancellableOperation NextJSInteropReceived { get; set; }
|
||||
private CancellableOperation<CapturedJSInteropCall> NextJSInteropReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDotNetInteropCompletionReceived { get; set; }
|
||||
private CancellableOperation<string> NextDotNetInteropCompletionReceived { get; set; }
|
||||
|
||||
public ILoggerProvider LoggerProvider { get; set; }
|
||||
|
||||
public bool ConfirmRenderBatch { get; set; } = true;
|
||||
|
||||
public event Action<int, string, string> JSInterop;
|
||||
public event Action<CapturedJSInteropCall> JSInterop;
|
||||
|
||||
public event Action<int, byte[]> RenderBatchReceived;
|
||||
public event Action<CapturedRenderBatch> RenderBatchReceived;
|
||||
|
||||
public event Action<string> 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<CapturedRenderBatch> PrepareForNextBatch(TimeSpan? timeout)
|
||||
{
|
||||
if (NextBatchReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextBatchReceived = new CancellableOperation(timeout);
|
||||
NextBatchReceived = new CancellableOperation<CapturedRenderBatch>(timeout);
|
||||
|
||||
return NextBatchReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextJSInterop(TimeSpan? timeout)
|
||||
public Task<CapturedJSInteropCall> PrepareForNextJSInterop(TimeSpan? timeout)
|
||||
{
|
||||
if (NextJSInteropReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextJSInteropReceived = new CancellableOperation(timeout);
|
||||
NextJSInteropReceived = new CancellableOperation<CapturedJSInteropCall>(timeout);
|
||||
|
||||
return NextJSInteropReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextDotNetInterop(TimeSpan? timeout)
|
||||
public Task<string> PrepareForNextDotNetInterop(TimeSpan? timeout)
|
||||
{
|
||||
if (NextDotNetInteropCompletionReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextDotNetInteropCompletionReceived = new CancellableOperation(timeout);
|
||||
NextDotNetInteropCompletionReceived = new CancellableOperation<string>(timeout);
|
||||
|
||||
return NextDotNetInteropCompletionReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextCircuitError(TimeSpan? timeout)
|
||||
public Task<string> PrepareForNextCircuitError(TimeSpan? timeout)
|
||||
{
|
||||
if (NextErrorReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextErrorReceived = new CancellableOperation(timeout);
|
||||
NextErrorReceived = new CancellableOperation<string>(timeout);
|
||||
|
||||
return NextErrorReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextDisconnect(TimeSpan? timeout)
|
||||
public Task<Exception> PrepareForNextDisconnect(TimeSpan? timeout)
|
||||
{
|
||||
if (NextDisconnect?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextDisconnect = new CancellableOperation(timeout);
|
||||
NextDisconnect = new CancellableOperation<Exception>(timeout);
|
||||
|
||||
return NextDisconnect.Completion.Task;
|
||||
}
|
||||
|
|
@ -160,115 +170,162 @@ namespace Ignitor
|
|||
return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
|
||||
}
|
||||
|
||||
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<CapturedRenderBatch> ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForRenderBatch(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<CapturedJSInteropCall> ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForJSInterop(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<string> ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForDotNetInterop(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<string> ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForCircuitError(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectCircuitErrorAndDisconnect(Func<Task> 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<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<Exception> ExpectDisconnect(Func<Task> 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<Task> 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<CapturedRenderBatch> 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<CapturedJSInteropCall> 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<string> 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<string> 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<Exception> 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<bool> ConnectAsync(Uri uri, bool connectAutomatically = true)
|
||||
|
|
@ -294,6 +351,11 @@ namespace Ignitor
|
|||
HubConnection.On<string>("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<string>("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<TResult>
|
||||
{
|
||||
public CancellableOperation(TimeSpan? timeout)
|
||||
{
|
||||
|
|
@ -423,24 +495,32 @@ namespace Ignitor
|
|||
Initialize();
|
||||
}
|
||||
|
||||
public TimeSpan? Timeout { get; }
|
||||
|
||||
public TaskCompletionSource<TResult> Completion { get; set; }
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
Completion = new TaskCompletionSource<TResult>(TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
Completion.Task.ContinueWith(
|
||||
(task, state) =>
|
||||
{
|
||||
var operation = (CancellableOperation)state;
|
||||
var operation = (CancellableOperation<TResult>)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<TResult>)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<object> Completion { get; set; }
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
||||
}
|
||||
|
||||
private string[] ReadMarkers(string content)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CapturedRenderBatch> Batches { get; } = new ConcurrentQueue<CapturedRenderBatch>();
|
||||
|
||||
public ConcurrentQueue<string> DotNetCompletions { get; } = new ConcurrentQueue<string>();
|
||||
|
||||
public ConcurrentQueue<string> Errors { get; } = new ConcurrentQueue<string>();
|
||||
|
||||
public ConcurrentQueue<CapturedJSInteropCall> JSInteropCalls { get; } = new ConcurrentQueue<CapturedJSInteropCall>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<bool>(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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue