Fix components event dispatch ordering + E2E test fixes (#10112)
This commit is contained in:
parent
dcf49f2575
commit
4db8260c6c
|
|
@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
public WebAssemblyRenderer(System.IServiceProvider serviceProvider) : base (default(System.IServiceProvider)) { }
|
public WebAssemblyRenderer(System.IServiceProvider serviceProvider) : base (default(System.IServiceProvider)) { }
|
||||||
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
|
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
|
||||||
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||||
|
public override System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
||||||
protected override void Dispose(bool disposing) { }
|
protected override void Dispose(bool disposing) { }
|
||||||
protected override void HandleException(System.Exception exception) { }
|
protected override void HandleException(System.Exception exception) { }
|
||||||
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch batch) { throw null; }
|
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch batch) { throw null; }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Blazor.Services;
|
using Microsoft.AspNetCore.Blazor.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
@ -18,6 +19,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
{
|
{
|
||||||
private readonly int _webAssemblyRendererId;
|
private readonly int _webAssemblyRendererId;
|
||||||
|
|
||||||
|
private bool isDispatchingEvent;
|
||||||
|
private Queue<IncomingEventInfo> deferredIncomingEvents = new Queue<IncomingEventInfo>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
|
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -110,5 +114,78 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
|
||||||
Console.Error.WriteLine(exception);
|
Console.Error.WriteLine(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
// Be sure we only run one event handler at once. Although they couldn't run
|
||||||
|
// simultaneously anyway (there's only one thread), they could run nested on
|
||||||
|
// the stack if somehow one event handler triggers another event synchronously.
|
||||||
|
// We need event handlers not to overlap because (a) that's consistent with
|
||||||
|
// server-side Blazor which uses a sync context, and (b) the rendering logic
|
||||||
|
// relies completely on the idea that within a given scope it's only building
|
||||||
|
// or processing one batch at a time.
|
||||||
|
//
|
||||||
|
// The only currently known case where this makes a difference is in the E2E
|
||||||
|
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
|
||||||
|
// where mutating the DOM cause an element's "change" to fire while its "input"
|
||||||
|
// handler is still running (i.e., nested on the stack) -- this doesn't happen
|
||||||
|
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
|
||||||
|
// it's conceivable that DOM mutation events could trigger this too.
|
||||||
|
|
||||||
|
if (isDispatchingEvent)
|
||||||
|
{
|
||||||
|
var info = new IncomingEventInfo(eventHandlerId, eventArgs);
|
||||||
|
deferredIncomingEvents.Enqueue(info);
|
||||||
|
return info.TaskCompletionSource.Task;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isDispatchingEvent = true;
|
||||||
|
return base.DispatchEventAsync(eventHandlerId, eventArgs);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isDispatchingEvent = false;
|
||||||
|
|
||||||
|
if (deferredIncomingEvents.Count > 0)
|
||||||
|
{
|
||||||
|
ProcessNextDeferredEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ProcessNextDeferredEvent()
|
||||||
|
{
|
||||||
|
var info = deferredIncomingEvents.Dequeue();
|
||||||
|
var taskCompletionSource = info.TaskCompletionSource;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DispatchEventAsync(info.EventHandlerId, info.EventArgs);
|
||||||
|
taskCompletionSource.SetResult(null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
taskCompletionSource.SetException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly struct IncomingEventInfo
|
||||||
|
{
|
||||||
|
public readonly int EventHandlerId;
|
||||||
|
public readonly UIEventArgs EventArgs;
|
||||||
|
public readonly TaskCompletionSource<object> TaskCompletionSource;
|
||||||
|
|
||||||
|
public IncomingEventInfo(int eventHandlerId, UIEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
EventHandlerId = eventHandlerId;
|
||||||
|
EventArgs = eventArgs;
|
||||||
|
TaskCompletionSource = new TaskCompletionSource<object>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
@ -14,10 +13,6 @@ namespace Microsoft.AspNetCore.Components.Browser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RendererRegistryEventDispatcher
|
public static class RendererRegistryEventDispatcher
|
||||||
{
|
{
|
||||||
private static bool isDispatchingEvent;
|
|
||||||
private static Queue<IncomingEventInfo> deferredIncomingEvents
|
|
||||||
= new Queue<IncomingEventInfo>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For framework use only.
|
/// For framework use only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -25,62 +20,9 @@ namespace Microsoft.AspNetCore.Components.Browser
|
||||||
public static Task DispatchEvent(
|
public static Task DispatchEvent(
|
||||||
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
||||||
{
|
{
|
||||||
// Be sure we only run one event handler at once. Although they couldn't run
|
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
||||||
// simultaneously anyway (there's only one thread), they could run nested on
|
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
|
||||||
// the stack if somehow one event handler triggers another event synchronously.
|
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
|
||||||
// We need event handlers not to overlap because (a) that's consistent with
|
|
||||||
// server-side Blazor which uses a sync context, and (b) the rendering logic
|
|
||||||
// relies completely on the idea that within a given scope it's only building
|
|
||||||
// or processing one batch at a time.
|
|
||||||
//
|
|
||||||
// The only currently known case where this makes a difference is in the E2E
|
|
||||||
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
|
|
||||||
// where mutating the DOM cause an element's "change" to fire while its "input"
|
|
||||||
// handler is still running (i.e., nested on the stack) -- this doesn't happen
|
|
||||||
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
|
|
||||||
// it's conceivable that DOM mutation events could trigger this too.
|
|
||||||
|
|
||||||
if (isDispatchingEvent)
|
|
||||||
{
|
|
||||||
var info = new IncomingEventInfo(eventDescriptor, eventArgsJson);
|
|
||||||
deferredIncomingEvents.Enqueue(info);
|
|
||||||
return info.TaskCompletionSource.Task;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isDispatchingEvent = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
|
|
||||||
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
|
|
||||||
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
isDispatchingEvent = false;
|
|
||||||
if (deferredIncomingEvents.Count > 0)
|
|
||||||
{
|
|
||||||
ProcessNextDeferredEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ProcessNextDeferredEvent()
|
|
||||||
{
|
|
||||||
var info = deferredIncomingEvents.Dequeue();
|
|
||||||
var task = DispatchEvent(info.EventDescriptor, info.EventArgsJson);
|
|
||||||
task.ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
if (task.Exception != null)
|
|
||||||
{
|
|
||||||
info.TaskCompletionSource.SetException(task.Exception);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
info.TaskCompletionSource.SetResult(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
|
private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
|
||||||
|
|
@ -136,19 +78,5 @@ namespace Microsoft.AspNetCore.Components.Browser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string EventArgsType { get; set; }
|
public string EventArgsType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly struct IncomingEventInfo
|
|
||||||
{
|
|
||||||
public readonly BrowserEventDescriptor EventDescriptor;
|
|
||||||
public readonly string EventArgsJson;
|
|
||||||
public readonly TaskCompletionSource<object> TaskCompletionSource;
|
|
||||||
|
|
||||||
public IncomingEventInfo(BrowserEventDescriptor eventDescriptor, string eventArgsJson)
|
|
||||||
{
|
|
||||||
EventDescriptor = eventDescriptor;
|
|
||||||
EventArgsJson = eventArgsJson;
|
|
||||||
TaskCompletionSource = new TaskCompletionSource<object>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -710,7 +710,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
|
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
|
||||||
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
|
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
|
||||||
public static Microsoft.AspNetCore.Components.Rendering.IDispatcher CreateDefaultDispatcher() { throw null; }
|
public static Microsoft.AspNetCore.Components.Rendering.IDispatcher CreateDefaultDispatcher() { throw null; }
|
||||||
public System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
protected virtual void Dispose(bool disposing) { }
|
protected virtual void Dispose(bool disposing) { }
|
||||||
protected abstract void HandleException(System.Exception exception);
|
protected abstract void HandleException(System.Exception exception);
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
static bool TryParseFloat(string value, out T result)
|
static bool TryParseFloat(string value, out T result)
|
||||||
{
|
{
|
||||||
var success = float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
var success = float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
||||||
if (success)
|
if (success && !float.IsInfinity(parsedValue))
|
||||||
{
|
{
|
||||||
result = (T)(object)parsedValue;
|
result = (T)(object)parsedValue;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
static bool TryParseDouble(string value, out T result)
|
static bool TryParseDouble(string value, out T result)
|
||||||
{
|
{
|
||||||
var success = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
var success = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue);
|
||||||
if (success)
|
if (success && !double.IsInfinity(parsedValue))
|
||||||
{
|
{
|
||||||
result = (T)(object)parsedValue;
|
result = (T)(object)parsedValue;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
|
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
|
||||||
/// has completed.
|
/// has completed.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
public virtual Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
|
||||||
{
|
{
|
||||||
EnsureSynchronizationContext();
|
EnsureSynchronizationContext();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,28 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ServerEventCallbackTest : EventCallbackTest
|
||||||
|
{
|
||||||
|
public ServerEventCallbackTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServerFormsTest : FormsTest
|
||||||
|
{
|
||||||
|
public ServerFormsTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServerKeyTest : KeyTest
|
||||||
|
{
|
||||||
|
public ServerKeyTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture.WithServerExecution(), output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
|
Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
|
||||||
|
|
||||||
// Can become invalid
|
// Can become invalid
|
||||||
|
expiryDateInput.Clear();
|
||||||
expiryDateInput.SendKeys("111111111");
|
expiryDateInput.SendKeys("111111111");
|
||||||
Browser.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
|
Browser.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
|
||||||
Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
|
Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
var appElem = MountTestComponent<ReorderingFocusComponent>();
|
var appElem = MountTestComponent<ReorderingFocusComponent>();
|
||||||
Func<IWebElement> textboxFinder = () => appElem.FindElement(By.CssSelector(".incomplete-items .item-1 input[type=text]"));
|
Func<IWebElement> textboxFinder = () => appElem.FindElement(By.CssSelector(".incomplete-items .item-1 input[type=text]"));
|
||||||
var textToType = "Hello this is a long string that should be typed";
|
var textToType = "Hello there!";
|
||||||
var expectedTextTyped = "";
|
var expectedTextTyped = "";
|
||||||
|
|
||||||
textboxFinder().Clear();
|
textboxFinder().Clear();
|
||||||
|
|
@ -221,17 +221,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
textboxFinder().Click();
|
textboxFinder().Click();
|
||||||
while (textToType.Length > 0)
|
while (textToType.Length > 0)
|
||||||
{
|
{
|
||||||
var nextBlockLength = Math.Min(5, textToType.Length);
|
var nextChar = textToType.Substring(0, 1);
|
||||||
var nextBlock = textToType.Substring(0, nextBlockLength);
|
textToType = textToType.Substring(1);
|
||||||
textToType = textToType.Substring(nextBlockLength);
|
expectedTextTyped += nextChar;
|
||||||
expectedTextTyped += nextBlock;
|
|
||||||
|
|
||||||
// Send keys to whatever has focus
|
// Send keys to whatever has focus
|
||||||
new Actions(Browser).SendKeys(nextBlock).Perform();
|
new Actions(Browser).SendKeys(nextChar).Perform();
|
||||||
Browser.Equal(expectedTextTyped, () => textboxFinder().GetAttribute("value"));
|
Browser.Equal(expectedTextTyped, () => textboxFinder().GetAttribute("value"));
|
||||||
|
|
||||||
// We delay between typings to ensure the events aren't all collapsed into one.
|
// We delay between typings to ensure the events aren't all collapsed into one.
|
||||||
await Task.Delay(100);
|
await Task.Delay(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that after all this, we can still move the edited item
|
// Verify that after all this, we can still move the edited item
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue