[Blazor] Adds E2E reliability tests for JS interop (#11958)
* Adds tests using ignitor to cover a variety of JS interop scenarios. * Validates that these errors don't bring down the circuit.
This commit is contained in:
parent
840c226e28
commit
6e53dac454
|
|
@ -38,6 +38,7 @@
|
|||
<ProjectReference Include="..\testassets\ComponentsApp.App\ComponentsApp.App.csproj" />
|
||||
<ProjectReference Include="..\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj" />
|
||||
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
|
||||
<ProjectReference Include="..\testassets\Ignitor\Ignitor.csproj" />
|
||||
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,337 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Ignitor;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
|
||||
private readonly AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_serverFixture = serverFixture;
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; } = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeNonJSInvokableMethods()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"System.IO.FileSystem",
|
||||
"WriteAllText",
|
||||
null,
|
||||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeNonExistingMethods()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
"MadeUpMethod",
|
||||
null,
|
||||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWithWrongNumberOfArguments()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"NotifyLocationChanged",
|
||||
null,
|
||||
JsonSerializer.Serialize(new[] { _serverFixture.RootUri }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsEmptyAssemblyName()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"",
|
||||
"NotifyLocationChanged",
|
||||
null,
|
||||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsEmptyMethodName()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"",
|
||||
null,
|
||||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWithWrongReferenceId()
|
||||
{
|
||||
// Arrange
|
||||
var expectedDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]";
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027Reverse\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
"CreateImportant",
|
||||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedDotNetObjectRef));
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
null,
|
||||
"Reverse",
|
||||
1,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", "[\"1\",true,\"tnatropmI\"]"));
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
null,
|
||||
"Reverse",
|
||||
3, // non existing ref
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWrongReferenceIdType()
|
||||
{
|
||||
// Arrange
|
||||
var expectedImportantDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]";
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
"CreateImportant",
|
||||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedImportantDotNetObjectRef));
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
"ReceiveTrivial",
|
||||
null,
|
||||
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details.";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop");
|
||||
|
||||
Assert.Single(interopCalls, (4, "sendMalformedCallbackReturn", (string)null));
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
0,
|
||||
"Microsoft.JSInterop",
|
||||
"DotNetDispatcher.EndInvoke",
|
||||
null,
|
||||
"[4, true, \"{\"]");
|
||||
|
||||
var text = Assert.Single(
|
||||
Client.FindElementById("errormessage").Children.OfType<TextNode>(),
|
||||
e => expectedError == e.TextContent);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWithInvalidArgumentsPayload()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"NotifyLocationChanged",
|
||||
null,
|
||||
"[ \"invalidPayload\"}");
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWithMalformedArgumentPayload()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.JSInteropDetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
"ReceiveTrivial",
|
||||
null,
|
||||
"[ { \"data\": {\"}} ]");
|
||||
|
||||
// Assert
|
||||
Assert.Single(interopCalls, (0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", expectedError));
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) =>
|
||||
ValidateClientKeepsWorking(Client, () => batches.Count);
|
||||
|
||||
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
|
||||
{
|
||||
var currentBatches = countAccessor();
|
||||
await Client.ClickAsync("thecounter");
|
||||
|
||||
Assert.Equal(currentBatches + 1, countAccessor());
|
||||
}
|
||||
|
||||
private async Task GoToTestComponent(List<(int, int, byte[])> batches)
|
||||
{
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
|
||||
Assert.Equal(2, batches.Count);
|
||||
}
|
||||
|
||||
private (List<(int, string, string)>, List<(int, int, byte[])>) ConfigureClient()
|
||||
{
|
||||
var interopCalls = new List<(int, string, string)>();
|
||||
Client.JSInterop += (int arg1, string arg2, string arg3) => interopCalls.Add((arg1, arg2, arg3));
|
||||
var batches = new List<(int, int, byte[])>();
|
||||
Client.RenderBatchReceived += (id, renderer, data) => batches.Add((id, renderer, data));
|
||||
return (interopCalls, batches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,69 @@
|
|||
@using Microsoft.AspNetCore.Components.RenderTree
|
||||
<div id="test-selector">
|
||||
Select test:
|
||||
<select @bind=SelectedComponentTypeName>
|
||||
Select test:
|
||||
<select id="test-selector-select" @bind=SelectedComponentTypeName>
|
||||
<option value="none">Choose...</option>
|
||||
<option value="BasicTestApp.InteropComponent">Interop component</option>
|
||||
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
|
||||
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
|
||||
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
|
||||
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
|
||||
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
|
||||
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
|
||||
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
|
||||
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
|
||||
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
|
||||
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
|
||||
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
|
||||
<option value="BasicTestApp.CounterComponent">Counter</option>
|
||||
<option value="BasicTestApp.CounterComponentUsingChild">Counter using child component</option>
|
||||
<option value="BasicTestApp.CounterComponentWrapper">Counter wrapped in parent</option>
|
||||
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
|
||||
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
||||
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
|
||||
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
|
||||
<option value="BasicTestApp.InputEventComponent">Input events</option>
|
||||
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
|
||||
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
|
||||
<option value="BasicTestApp.RedTextComponent">Red text</option>
|
||||
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
|
||||
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
|
||||
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
|
||||
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
||||
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
|
||||
<option value="BasicTestApp.CulturePicker">Culture Picker</option>
|
||||
<option value="BasicTestApp.DataDashComponent">data-* attribute rendering</option>
|
||||
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
|
||||
<option value="BasicTestApp.SvgComponent">SVG</option>
|
||||
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
|
||||
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
||||
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
|
||||
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
|
||||
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
|
||||
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
|
||||
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
|
||||
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithoutNotFoundContent">Router without NotFoundContent</option>
|
||||
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
|
||||
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
|
||||
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
|
||||
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
|
||||
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
||||
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
|
||||
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
|
||||
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
|
||||
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
|
||||
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
|
||||
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
|
||||
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
|
||||
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
|
||||
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
|
||||
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
|
||||
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
|
||||
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
|
||||
<option value="BasicTestApp.FormsTest.SimpleValidationComponent">Simple validation</option>
|
||||
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
|
||||
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
|
||||
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
|
||||
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
|
||||
<option value="BasicTestApp.HtmlEncodedChildContent">ChildContent HTML Encoded Block</option>
|
||||
<option value="BasicTestApp.HtmlMixedChildContent">ChildContent Mixed Block</option>
|
||||
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
|
||||
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||
<option value="BasicTestApp.InputEventComponent">Input events</option>
|
||||
<option value="BasicTestApp.InteropComponent">Interop component</option>
|
||||
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
|
||||
<option value="BasicTestApp.KeyCasesComponent">Key cases</option>
|
||||
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
|
||||
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
|
||||
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
|
||||
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
|
||||
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
|
||||
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
|
||||
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
||||
<option value="BasicTestApp.LaggyTypingComponent">Laggy typing</option>
|
||||
<option value="BasicTestApp.CulturePicker">Culture Picker</option>
|
||||
<option value="BasicTestApp.LocalizedText">Localized Text</option>
|
||||
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
|
||||
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
||||
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
|
||||
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
|
||||
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
|
||||
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
|
||||
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
||||
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
|
||||
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
|
||||
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
|
||||
<option value="BasicTestApp.RedTextComponent">Red text</option>
|
||||
<option value="BasicTestApp.ReliabilityComponent">Server reliability component</option>
|
||||
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
|
||||
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithoutNotFoundContent">Router without NotFoundContent</option>
|
||||
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
|
||||
<option value="BasicTestApp.SvgComponent">SVG</option>
|
||||
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
|
||||
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
|
||||
</select>
|
||||
|
||||
<span id="runtime-info"><code><tt>@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription</tt></code></span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class JSInterop
|
||||
{
|
||||
[JSInvokable]
|
||||
public static DotNetObjectRef<ImportantInformation> CreateImportant()
|
||||
{
|
||||
return DotNetObjectRef.Create(new ImportantInformation());
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public static string ReceiveTrivial(DotNetObjectRef<TrivialInformation> information)
|
||||
{
|
||||
return information.Value.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public class ImportantInformation
|
||||
{
|
||||
public string Message { get; set; } = "Important";
|
||||
|
||||
[JSInvokable]
|
||||
public string Reverse()
|
||||
{
|
||||
var messageChars = Message.ToCharArray();
|
||||
Array.Reverse(messageChars);
|
||||
return new string(messageChars);
|
||||
}
|
||||
}
|
||||
|
||||
public class TrivialInformation
|
||||
{
|
||||
public string Message { get; set; } = "Trivial";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
@inject Microsoft.JSInterop.IJSRuntime JSRuntime
|
||||
@namespace BasicTestApp
|
||||
<h1>Server reliability</h1>
|
||||
<p>This component is used on the server-side execution model to validate that the circuit is resilient to failures, intentional or not.
|
||||
The tests that use this component trigger invalid .NET calls to which the server replies with proper JS interop messages indicating a
|
||||
failure to perform the call.
|
||||
|
||||
We include a counter to ensure that the circuit is still alive and working fine after the error.
|
||||
</p>
|
||||
|
||||
<p>Current count: @currentCount</p>
|
||||
<p id="errormessage">Error = @error</p>
|
||||
<button id="triggerjsinterop" @onclick="@TriggerJSInterop"></button>
|
||||
|
||||
<button id="thecounter" @onclick="@IncrementCount">Click me</button>
|
||||
|
||||
@code
|
||||
{
|
||||
int currentCount = 0;
|
||||
string error = "";
|
||||
|
||||
void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
|
||||
async Task TriggerJSInterop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await JSRuntime.InvokeAsync<int>(
|
||||
"sendMalformedCallbackReturn",
|
||||
Array.Empty<object>());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
error = e.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ using System.Text.Json;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -27,44 +26,133 @@ namespace Ignitor
|
|||
{
|
||||
TaskCompletionSource.TrySetCanceled();
|
||||
});
|
||||
|
||||
ImplicitWait = DefaultLatencyTimeout != null;
|
||||
}
|
||||
|
||||
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private CancellationTokenSource CancellationTokenSource { get; }
|
||||
|
||||
private CancellationToken CancellationToken => CancellationTokenSource.Token;
|
||||
|
||||
private TaskCompletionSource<object> TaskCompletionSource { get; }
|
||||
|
||||
private CancellableOperation NextBatchReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextJSInteropReceived { get; set; }
|
||||
|
||||
public bool ConfirmRenderBatch { get; set; } = true;
|
||||
|
||||
public event Action<int, string, string> JSInterop;
|
||||
|
||||
public event Action<int, int, byte[]> RenderBatchReceived;
|
||||
|
||||
public event Action<Error> OnCircuitError;
|
||||
|
||||
public string CircuitId { get; set; }
|
||||
|
||||
public ElementHive Hive { get; set; } = new ElementHive();
|
||||
|
||||
public bool ImplicitWait { get; set; }
|
||||
|
||||
public HubConnection HubConnection { get; set; }
|
||||
|
||||
public Task ClickAsync(string elementId)
|
||||
public Task PrepareForNextBatch(TimeSpan? timeout)
|
||||
{
|
||||
if (NextBatchReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextBatchReceived = new CancellableOperation(timeout);
|
||||
|
||||
return NextBatchReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextJSInterop()
|
||||
{
|
||||
if (NextJSInteropReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextJSInteropReceived = new CancellableOperation(DefaultLatencyTimeout);
|
||||
|
||||
return NextJSInteropReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public async Task ClickAsync(string elementId)
|
||||
{
|
||||
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
||||
}
|
||||
|
||||
return elementNode.ClickAsync(HubConnection);
|
||||
await ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
|
||||
}
|
||||
|
||||
public ElementHive Hive { get; set; }
|
||||
public async Task SelectAsync(string elementId, string value)
|
||||
{
|
||||
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
||||
}
|
||||
|
||||
await ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
|
||||
}
|
||||
|
||||
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForRenderBatch(timeout);
|
||||
await action();
|
||||
await task;
|
||||
}
|
||||
|
||||
public async Task ExpectJSInterop(Func<Task> action)
|
||||
{
|
||||
var task = WaitForJSInterop();
|
||||
await action();
|
||||
await task;
|
||||
}
|
||||
|
||||
private Task WaitForRenderBatch(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
return PrepareForNextBatch(timeout ?? DefaultLatencyTimeout);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WaitForJSInterop()
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
await PrepareForNextJSInterop();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync(Uri uri, bool prerendered)
|
||||
{
|
||||
var builder = new HubConnectionBuilder();
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
|
||||
builder.WithUrl(new Uri(uri, "_blazor/"));
|
||||
builder.WithUrl(GetHubUrl(uri));
|
||||
builder.ConfigureLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Trace));
|
||||
var hive = new ElementHive();
|
||||
|
||||
HubConnection = builder.Build();
|
||||
await HubConnection.StartAsync(CancellationToken);
|
||||
Console.WriteLine("Connected");
|
||||
|
||||
HubConnection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
|
||||
HubConnection.On<int, int, byte[]>("JS.RenderBatch", OnRenderBatch);
|
||||
|
|
@ -75,55 +163,92 @@ namespace Ignitor
|
|||
if (prerendered)
|
||||
{
|
||||
CircuitId = await GetPrerenderedCircuitIdAsync(uri);
|
||||
return await HubConnection.InvokeAsync<bool>("ConnectCircuit", CircuitId);
|
||||
var result = false;
|
||||
await ExpectRenderBatch(async () => result = await HubConnection.InvokeAsync<bool>("ConnectCircuit", CircuitId));
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri);
|
||||
await ExpectRenderBatch(
|
||||
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", new Uri(uri.GetLeftPart(UriPartial.Authority)), uri),
|
||||
TimeSpan.FromSeconds(10));
|
||||
return CircuitId != null;
|
||||
}
|
||||
}
|
||||
|
||||
void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
|
||||
private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
JSInterop?.Invoke(asyncHandle, identifier, argsJson);
|
||||
}
|
||||
|
||||
void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
|
||||
NextJSInteropReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NextJSInteropReceived?.Completion?.TrySetException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderBatchReceived?.Invoke(browserRendererId, batchId, batchData);
|
||||
|
||||
var batch = RenderBatchReader.Read(batchData);
|
||||
hive.Update(batch);
|
||||
|
||||
Hive.Update(batch);
|
||||
|
||||
if (ConfirmRenderBatch)
|
||||
{
|
||||
HubConnection.InvokeAsync("OnRenderCompleted", batchId, /* error */ null);
|
||||
}
|
||||
|
||||
RenderBatchReceived?.Invoke(browserRendererId, batchId, batchData);
|
||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
void OnError(Error error)
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("ERROR: " + error.Stack);
|
||||
}
|
||||
|
||||
Task OnClosedAsync(Exception ex)
|
||||
{
|
||||
if (ex == null)
|
||||
{
|
||||
TaskCompletionSource.TrySetResult(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
NextBatchReceived?.Completion?.TrySetResult(e);
|
||||
}
|
||||
}
|
||||
|
||||
void InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson)
|
||||
private void OnError(Error error)
|
||||
{
|
||||
HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson);
|
||||
OnCircuitError?.Invoke(error);
|
||||
}
|
||||
|
||||
private Task OnClosedAsync(Exception ex)
|
||||
{
|
||||
if (ex == null)
|
||||
{
|
||||
TaskCompletionSource.TrySetResult(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Uri GetHubUrl(Uri uri)
|
||||
{
|
||||
if (uri.Segments.Length == 1)
|
||||
{
|
||||
return new Uri(uri, "_blazor");
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = new UriBuilder(uri);
|
||||
builder.Path += builder.Path.EndsWith("/") ? "_blazor" : "/_blazor";
|
||||
return builder.Uri;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson)
|
||||
{
|
||||
await ExpectJSInterop(() => HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson));
|
||||
}
|
||||
|
||||
private static async Task<string> GetPrerenderedCircuitIdAsync(Uri uri)
|
||||
|
|
@ -144,5 +269,65 @@ namespace Ignitor
|
|||
CancellationTokenSource.Cancel();
|
||||
CancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
public ElementNode FindElementById(string id)
|
||||
{
|
||||
if (!Hive.TryFindElementById(id, out var element))
|
||||
{
|
||||
throw new InvalidOperationException("Element not found.");
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private class CancellableOperation
|
||||
{
|
||||
public CancellableOperation(TimeSpan? timeout)
|
||||
{
|
||||
Timeout = timeout;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
Completion.Task.ContinueWith(
|
||||
(task, state) =>
|
||||
{
|
||||
var operation = (CancellableOperation)state;
|
||||
operation.Dispose();
|
||||
},
|
||||
this,
|
||||
TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
|
||||
if (Timeout != null)
|
||||
{
|
||||
Cancellation = new CancellationTokenSource(Timeout.Value);
|
||||
CancellationRegistration = Cancellation.Token.Register(
|
||||
(self) =>
|
||||
{
|
||||
var operation = (CancellableOperation)self;
|
||||
operation.Completion.TrySetCanceled(operation.Cancellation.Token);
|
||||
operation.Cancellation.Dispose();
|
||||
operation.CancellationRegistration.Dispose();
|
||||
},
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose()
|
||||
{
|
||||
Completion = null;
|
||||
Cancellation.Dispose();
|
||||
CancellationRegistration.Dispose();
|
||||
}
|
||||
|
||||
public TimeSpan? Timeout { get; }
|
||||
|
||||
public TaskCompletionSource<object> Completion { get; set; }
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
|
|
@ -62,6 +63,42 @@ namespace Ignitor
|
|||
_events[eventName] = descriptor;
|
||||
}
|
||||
|
||||
internal async Task SelectAsync(HubConnection connection, string value)
|
||||
{
|
||||
if (!Events.TryGetValue("change", out var changeEventDescriptor))
|
||||
{
|
||||
throw new InvalidOperationException("Element does not have a click event.");
|
||||
}
|
||||
|
||||
var mouseEventArgs = new UIChangeEventArgs()
|
||||
{
|
||||
Type = changeEventDescriptor.EventName,
|
||||
Value = value
|
||||
};
|
||||
|
||||
var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor()
|
||||
{
|
||||
BrowserRendererId = 0,
|
||||
EventHandlerId = changeEventDescriptor.EventId,
|
||||
EventArgsType = "change",
|
||||
EventFieldInfo = new EventFieldInfo
|
||||
{
|
||||
ComponentId = 0,
|
||||
FieldValue = value
|
||||
}
|
||||
};
|
||||
|
||||
var serializedJson = JsonSerializer.Serialize(mouseEventArgs, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
var argsObject = new object[] { browserDescriptor, serializedJson };
|
||||
var callId = "0";
|
||||
var assemblyName = "Microsoft.AspNetCore.Components.Web";
|
||||
var methodIdentifier = "DispatchEvent";
|
||||
var dotNetObjectId = 0;
|
||||
var clickArgs = JsonSerializer.Serialize(argsObject, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
await connection.InvokeAsync("BeginInvokeDotNetFromJS", callId, assemblyName, methodIdentifier, dotNetObjectId, clickArgs);
|
||||
|
||||
}
|
||||
|
||||
public class ElementEventDescriptor
|
||||
{
|
||||
public ElementEventDescriptor(string eventName, int eventId)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Ignitor
|
||||
{
|
||||
internal class Error
|
||||
public class Error
|
||||
{
|
||||
public string Stack { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ namespace Ignitor
|
|||
{
|
||||
if (batchId < 1000)
|
||||
{
|
||||
client.ClickAsync("thecounter");
|
||||
var _ = client.ClickAsync("thecounter");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue