diff --git a/src/Microsoft.AspNetCore.Blazor/Components/BindMethods.cs b/src/Microsoft.AspNetCore.Blazor/Components/BindMethods.cs index b8d8994c2a..39d8d0d90e 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/BindMethods.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/BindMethods.cs @@ -35,10 +35,10 @@ namespace Microsoft.AspNetCore.Blazor.Components /// /// Not intended to be used directly. /// - public static UIEventHandler GetEventHandlerValue(Action value) + public static MulticastDelegate GetEventHandlerValue(Action value) where T : UIEventArgs { - return e => value((T)e); + return value; } /// diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs index 73f27914c1..17fb4a7e6d 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs @@ -1,10 +1,10 @@ // 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 Microsoft.AspNetCore.Blazor.Components; -using Microsoft.AspNetCore.Blazor.Rendering; using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Blazor.Components; +using Microsoft.AspNetCore.Blazor.Rendering; namespace Microsoft.AspNetCore.Blazor.RenderTree { @@ -101,9 +101,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree => AddContent(sequence, textContent?.ToString()); /// + /// /// Appends a frame representing a bool-valued attribute. + /// + /// /// The attribute is associated with the most recently added element. If the value is false and the /// current element is not a component, the frame will be omitted. + /// /// /// An integer that represents the position of the instruction in the source code. /// The name of the attribute. @@ -124,9 +128,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } /// + /// /// Appends a frame representing a string-valued attribute. + /// + /// /// The attribute is associated with the most recently added element. If the value is null and the /// current element is not a component, the frame will be omitted. + /// /// /// An integer that represents the position of the instruction in the source code. /// The name of the attribute. @@ -141,14 +149,44 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } /// - /// Appends a frame representing an -valued attribute. + /// + /// Appends a frame representing an -valued attribute. + /// + /// /// The attribute is associated with the most recently added element. If the value is null and the /// current element is not a component, the frame will be omitted. + /// /// + /// The . /// An integer that represents the position of the instruction in the source code. /// The name of the attribute. /// The value of the attribute. public void AddAttribute(int sequence, string name, UIEventHandler value) + { + AddAttribute(sequence, name, (MulticastDelegate)value); + } + + /// + /// + /// Appends a frame representing a delegate-valued attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + /// + /// This method is provided for infrastructure purposes, and is used to be + /// to provide support for delegates of specific + /// types. For a good programming experience when using a custom delegate type, define an + /// extension method similar to + /// + /// that calls this method. + /// + public void AddAttribute(int sequence, string name, MulticastDelegate value) { AssertCanAddAttribute(); if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component) @@ -185,7 +223,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // Don't add anything for false bool value. } - else if (value is UIEventHandler eventHandler) + else if (value is MulticastDelegate) { Append(RenderTreeFrame.Attribute(sequence, name, value)); } @@ -207,8 +245,12 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } /// + /// /// Appends a frame representing an attribute. + /// + /// /// The attribute is associated with the most recently added element. + /// /// /// An integer that represents the position of the instruction in the source code. /// The name of the attribute. diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index 31828e4da2..83c25bdd7e 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -597,7 +597,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame) { - if (newFrame.AttributeValue is UIEventHandler) + // Any attribute with an event handler id will be callable via DOM events + // + // We're following a simple heuristic here that's reflected in the ts runtime + // based on the common usage of attributes for DOM events. + if (newFrame.AttributeValue is MulticastDelegate && + newFrame.AttributeName.Length >= 3 && + newFrame.AttributeName.StartsWith("on")) { diffContext.Renderer.AssignEventHandlerId(ref newFrame); } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index b758382e45..78c221f643 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -113,7 +113,30 @@ namespace Microsoft.AspNetCore.Blazor.Rendering internal void AssignEventHandlerId(ref RenderTreeFrame frame) { var id = ++_lastEventHandlerId; - _eventHandlersById.Add(id, (UIEventHandler)frame.AttributeValue); + + // The attribute value might be a more specialized type like UIKeyboardEventHandler. + // In that case, it won't be a UIEventHandler, and it will go down the MulticastDelegate + // code path (MulticastDelegate is any delegate). + // + // In order to dispatch the event, we need a UIEventHandler, so we're going weakly + // typed here. The user will get a cast exception if they map the wrong type of + // delegate to the event. + if (frame.AttributeValue is UIEventHandler wrapper) + { + _eventHandlersById.Add(id, wrapper); + } + else if (frame.AttributeValue is MulticastDelegate @delegate) + { + // IMPORTANT: we're creating an additional delegate when necessary. This is + // going to get cached in _eventHandlersById, but the render tree diff + // will operate on 'AttributeValue' which means that we'll only create a new + // wrapper delegate when the underlying delegate changes. + // + // TLDR: If the component uses a method group or a non-capturing lambda + // we don't allocate much. + _eventHandlersById.Add(id, (UIEventArgs e) => @delegate.DynamicInvoke(e)); + } + frame = frame.WithAttributeEventHandlerId(id); } diff --git a/src/Microsoft.AspNetCore.Blazor/UIEventArgs.cs b/src/Microsoft.AspNetCore.Blazor/UIEventArgs.cs index c11149579a..a333af2716 100644 --- a/src/Microsoft.AspNetCore.Blazor/UIEventArgs.cs +++ b/src/Microsoft.AspNetCore.Blazor/UIEventArgs.cs @@ -15,10 +15,15 @@ namespace Microsoft.AspNetCore.Blazor } /// - /// Supplies information about a mouse event that is being raised. + /// Supplies information about an input change event that is being raised. /// - public class UIMouseEventArgs : UIEventArgs + public class UIChangeEventArgs : UIEventArgs { + /// + /// Gets or sets the new value of the input. This may be a + /// or a . + /// + public object Value { get; set; } } /// @@ -33,14 +38,9 @@ namespace Microsoft.AspNetCore.Blazor } /// - /// Supplies information about an input change event that is being raised. + /// Supplies information about a mouse event that is being raised. /// - public class UIChangeEventArgs : UIEventArgs + public class UIMouseEventArgs : UIEventArgs { - /// - /// Gets or sets the new value of the input. This may be a - /// or a . - /// - public object Value { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Blazor/UIEventHandler.cs b/src/Microsoft.AspNetCore.Blazor/UIEventHandler.cs index 2e9b983ed4..0b51e5fc0b 100644 --- a/src/Microsoft.AspNetCore.Blazor/UIEventHandler.cs +++ b/src/Microsoft.AspNetCore.Blazor/UIEventHandler.cs @@ -4,7 +4,22 @@ namespace Microsoft.AspNetCore.Blazor { /// - /// Handles an event raised for a . + /// Handles an event raised for a . /// - public delegate void UIEventHandler(UIEventArgs eventArgs); + public delegate void UIEventHandler(UIEventArgs e); + + /// + /// Handles an event raised for a . + /// + public delegate void UIChangeEventHandler(UIChangeEventArgs e); + + /// + /// Handles an event raised for a . + /// + public delegate void UIKeyboardEventHandler(UIKeyboardEventArgs e); + + /// + /// Handles an event raised for a . + /// + public delegate void UIMouseEventHandler(UIMouseEventArgs e); } diff --git a/src/Microsoft.AspNetCore.Blazor/UIEventHandlerRenderTreeBuilderExtensions.cs b/src/Microsoft.AspNetCore.Blazor/UIEventHandlerRenderTreeBuilderExtensions.cs new file mode 100644 index 0000000000..6b84556d5b --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/UIEventHandlerRenderTreeBuilderExtensions.cs @@ -0,0 +1,109 @@ +// 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 Microsoft.AspNetCore.Blazor.RenderTree; + +namespace Microsoft.AspNetCore.Blazor +{ + /// + /// Extensions methods on for event handlers. + /// + /// + /// + /// These methods enable method-group to delegate conversion for delegates and methods that accept + /// types derived from . + /// + /// + /// This enhances the programming experience for using event handlers with the render tree builder + /// in components written in pure C#. These extension methods make it possible to write code like: + /// + /// builder.AddAttribute(0, "onkeypress", MyKeyPressHandler); + /// + /// Where void MyKeyPressHandler(UIKeyboardEventArgs e) is a method defined in the same class. + /// In this example, the author knows that the onclick event is associated with the + /// event args type. The component author is responsible for + /// providing a delegate that matches the expected event args type, an error will result in a failure + /// at runtime. + /// + /// + /// When a component is authored in Razor (.cshtml), the Razor code generator will maintain a mapping + /// between event names and event arg types that can be used to generate more strongly typed code. + /// Generated code for the same case will look like: + /// + /// builder.AddAttribute(0, "onkeypress", BindMethods.GetEventHandlerValue<UIKeyboardEventArgs>(MyKeyPressHandler)); + /// + /// + /// + public static class UIEventHandlerRenderTreeBuilderExtensions + { + /// + /// + /// Appends a frame representing an -valued attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// The . + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + public static void AddAttribute(this RenderTreeBuilder builder, int sequence, string name, UIChangeEventHandler value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddAttribute(sequence, name, (MulticastDelegate)value); + } + + /// + /// + /// Appends a frame representing an -valued attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// The . + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + public static void AddAttribute(this RenderTreeBuilder builder, int sequence, string name, UIKeyboardEventHandler value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddAttribute(sequence, name, (MulticastDelegate)value); + } + + /// + /// + /// Appends a frame representing an -valued attribute. + /// + /// + /// The attribute is associated with the most recently added element. If the value is null and the + /// current element is not a component, the frame will be omitted. + /// + /// + /// The . + /// An integer that represents the position of the instruction in the source code. + /// The name of the attribute. + /// The value of the attribute. + public static void AddAttribute(this RenderTreeBuilder builder, int sequence, string name, UIMouseEventHandler value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddAttribute(sequence, name, (MulticastDelegate)value); + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs index 3f59e323f0..cff219def8 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs @@ -174,7 +174,7 @@ namespace Test { public class MyComponent : BlazorComponent { - public UIEventHandler OnClick { get; set; } + public UIMouseEventHandler OnClick { get; set; } } } ")); @@ -186,7 +186,7 @@ namespace Test @functions {{ private int counter; - private void Increment(UIEventArgs e) {{ + private void Increment(UIMouseEventArgs e) {{ counter++; }} }}"); @@ -203,7 +203,7 @@ namespace Test AssertFrame.Attribute(frame, "OnClick", 1); // The handler will have been assigned to a lambda - var handler = Assert.IsType(frame.AttributeValue); + var handler = Assert.IsType(frame.AttributeValue); Assert.Equal("Test.TestComponent", handler.Target.GetType().FullName); }, frame => AssertFrame.Whitespace(frame, 2)); diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs index 35b48b36c4..ebf50539e4 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs @@ -519,7 +519,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { AssertFrame.Attribute(frame, "onclick", 1); - var func = Assert.IsType(frame.AttributeValue); + var func = Assert.IsType>(frame.AttributeValue); Assert.False((bool)clicked.GetValue(component)); func(new UIMouseEventArgs()); @@ -552,7 +552,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { AssertFrame.Attribute(frame, "onclick", 1); - var func = Assert.IsType(frame.AttributeValue); + var func = Assert.IsType>(frame.AttributeValue); Assert.False((bool)clicked.GetValue(component)); func(new UIMouseEventArgs()); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs index 6ef63822f6..5bd5e73875 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BasicTestAppTestBase.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { @@ -14,8 +15,11 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { public const string ServerPathBase = "/subdir"; - public BasicTestAppTestBase(BrowserFixture browserFixture, DevHostServerFixture serverFixture) - : base(browserFixture, serverFixture) + public BasicTestAppTestBase( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { serverFixture.PathBase = ServerPathBase; } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs index 3a3bc298a0..1d41aae7bf 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs @@ -5,6 +5,7 @@ using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Remote; using System; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { @@ -12,6 +13,10 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { public IWebDriver Browser { get; } + public ILogs Logs { get; } + + public ITestOutputHelper Output { get; set; } + public BrowserFixture() { var opts = new ChromeOptions(); @@ -19,6 +24,9 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure // Comment this out if you want to watch or interact with the browser (e.g., for debugging) opts.AddArgument("--headless"); + // Log errors + opts.SetLoggingPreference(LogType.Browser, LogLevel.All); + // On Windows/Linux, we don't need to set opts.BinaryLocation // But for Travis Mac builds we do var binaryLocation = Environment.GetEnvironmentVariable("TEST_CHROME_BINARY"); @@ -30,7 +38,9 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure try { - Browser = new RemoteWebDriver(opts); + var driver = new RemoteWebDriver(opts); + Browser = driver; + Logs = new RemoteLogs(driver); } catch (WebDriverException ex) { diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserTestBase.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserTestBase.cs index 063f9cf47e..855aefcd4b 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserTestBase.cs @@ -1,18 +1,31 @@ // 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.Threading; using OpenQA.Selenium; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { + [CaptureSeleniumLogs] public class BrowserTestBase : IClassFixture { - public IWebDriver Browser { get; } + private static readonly AsyncLocal _browser = new AsyncLocal(); + private static readonly AsyncLocal _logs = new AsyncLocal(); + private static readonly AsyncLocal _output = new AsyncLocal(); - public BrowserTestBase(BrowserFixture browserFixture) + public static IWebDriver Browser => _browser.Value; + + public static ILogs Logs => _logs.Value; + + public static ITestOutputHelper Output => _output.Value; + + public BrowserTestBase(BrowserFixture browserFixture, ITestOutputHelper output) { - Browser = browserFixture.Browser; + _browser.Value = browserFixture.Browser; + _logs.Value = browserFixture.Logs; + _output.Value = output; } } } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/CaptureSeleniumLogsAttribute.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/CaptureSeleniumLogsAttribute.cs new file mode 100644 index 0000000000..2ea3a34d6f --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/CaptureSeleniumLogsAttribute.cs @@ -0,0 +1,49 @@ +// 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.Linq; +using System.Reflection; +using OpenQA.Selenium; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure +{ + // This has to use BeforeAfterTestAttribute because running the log capture + // in the BrowserFixture.Dispose method is too late, and we can't add logging + // to the test. + public class CaptureSeleniumLogsAttribute : BeforeAfterTestAttribute + { + public override void Before(MethodInfo methodUnderTest) + { + if (!typeof(BrowserTestBase).IsAssignableFrom(methodUnderTest.DeclaringType)) + { + throw new InvalidOperationException("This should only be used with BrowserTestBase"); + } + } + + public override void After(MethodInfo methodUnderTest) + { + var browser = BrowserTestBase.Browser; + var logs = BrowserTestBase.Logs; + var output = BrowserTestBase.Output; + + // Put browser logs first, the test UI will truncate output after a certain length + // and the browser logs will include exceptions thrown by js in the browser. + foreach (var kind in logs.AvailableLogTypes.OrderBy(k => k == LogType.Browser ? 0 : 1)) + { + output.WriteLine($"{kind} Logs from Selenium:"); + + var entries = logs.GetLog(kind); + foreach (LogEntry entry in entries) + { + output.WriteLine($"[{entry.Timestamp}] - {entry.Level} - {entry.Message}"); + } + + output.WriteLine(""); + output.WriteLine(""); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/ServerTestBase.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/ServerTestBase.cs index 4ce3559026..deb029d89a 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/ServerTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/ServerTestBase.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using System; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { @@ -13,8 +14,8 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure { private readonly TServerFixture _serverFixture; - public ServerTestBase(BrowserFixture browserFixture, TServerFixture serverFixture) - : base(browserFixture) + public ServerTestBase(BrowserFixture browserFixture, TServerFixture serverFixture, ITestOutputHelper output) + : base(browserFixture, output) { _serverFixture = serverFixture; } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/BindTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/BindTest.cs index 1304d54b29..be32b10252 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/BindTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/BindTest.cs @@ -7,13 +7,17 @@ using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { public class BindTest : BasicTestAppTestBase { - public BindTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) - : base(browserFixture, serverFixture) + public BindTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { Navigate(ServerPathBase, noReload: true); MountTestComponent(); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs index 2bd952dd37..82897365d7 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs @@ -12,13 +12,17 @@ using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using OpenQA.Selenium; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { public class ComponentRenderingTest : BasicTestAppTestBase { - public ComponentRenderingTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) - : base(browserFixture, serverFixture) + public ComponentRenderingTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { Navigate(ServerPathBase, noReload: true); } @@ -233,7 +237,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests public void CanRenderSvgWithCorrectNamespace() { var appElement = MountTestComponent(); - + var svgElement = appElement.FindElement(By.XPath("//*[local-name()='svg' and namespace-uri()='http://www.w3.org/2000/svg']")); Assert.NotNull(svgElement); @@ -245,7 +249,7 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests public void CanRenderSvgChildComponentWithCorrectNamespace() { var appElement = MountTestComponent(); - + var svgElement = appElement.FindElement(By.XPath("//*[local-name()='svg' and namespace-uri()='http://www.w3.org/2000/svg']")); Assert.NotNull(svgElement); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HostedInAspNetTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HostedInAspNetTest.cs index af552df1a7..b9f3a454a5 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HostedInAspNetTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HostedInAspNetTest.cs @@ -7,13 +7,17 @@ using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { public class HostedInAspNetTest : ServerTestBase { - public HostedInAspNetTest(BrowserFixture browserFixture, AspNetSiteServerFixture serverFixture) - : base(browserFixture, serverFixture) + public HostedInAspNetTest( + BrowserFixture browserFixture, + AspNetSiteServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { serverFixture.BuildWebHostMethod = HostedInAspNet.Server.Program.BuildWebHost; serverFixture.Environment = AspNetEnvironment.Development; diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs index b18bcf8455..4d6101ebb4 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs @@ -10,6 +10,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { @@ -24,8 +25,9 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests public HttpClientTest( BrowserFixture browserFixture, DevHostServerFixture devHostServerFixture, - AspNetSiteServerFixture apiServerFixture) - : base(browserFixture, devHostServerFixture) + AspNetSiteServerFixture apiServerFixture, + ITestOutputHelper output) + : base(browserFixture, devHostServerFixture, output) { apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; _apiServerFixture = apiServerFixture; diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/MonoSanityTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/MonoSanityTest.cs index ee4d78351c..513a0a85c5 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/MonoSanityTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/MonoSanityTest.cs @@ -7,13 +7,17 @@ using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { public class MonoSanityTest : ServerTestBase { - public MonoSanityTest(BrowserFixture browserFixture, AspNetSiteServerFixture serverFixture) - : base(browserFixture, serverFixture) + public MonoSanityTest( + BrowserFixture browserFixture, + AspNetSiteServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { serverFixture.BuildWebHostMethod = MonoSanity.Program.BuildWebHost; Navigate("/", noReload: true); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs index 7d28b3dd03..edb8fc288f 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using OpenQA.Selenium; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { @@ -16,8 +17,11 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { private readonly ServerFixture _server; - public RoutingTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) - : base(browserFixture, serverFixture) + public RoutingTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { _server = serverFixture; Navigate(ServerPathBase, noReload: true); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/StandaloneAppTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/StandaloneAppTest.cs index 217fdaaba5..c0093f0dfc 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/StandaloneAppTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/StandaloneAppTest.cs @@ -8,6 +8,7 @@ using OpenQA.Selenium.Support.UI; using System; using System.Linq; using Xunit; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { @@ -16,8 +17,11 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests { private readonly ServerFixture _serverFixture; - public StandaloneAppTest(BrowserFixture browserFixture, DevHostServerFixture serverFixture) - : base(browserFixture, serverFixture) + public StandaloneAppTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) { _serverFixture = serverFixture; Navigate("/", noReload: true); diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs index 61b3992fdf..f9a708f242 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs @@ -1156,10 +1156,10 @@ namespace Microsoft.AspNetCore.Blazor.Test // Arrange UIEventHandler retainedHandler = _ => { }; oldTree.OpenElement(0, "My element"); - oldTree.AddAttribute(1, "will remain", retainedHandler); + oldTree.AddAttribute(1, "ontest", retainedHandler); oldTree.CloseElement(); newTree.OpenElement(0, "My element"); - newTree.AddAttribute(1, "will remain", retainedHandler); + newTree.AddAttribute(1, "ontest", retainedHandler); newTree.CloseElement(); // Act @@ -1169,8 +1169,8 @@ namespace Microsoft.AspNetCore.Blazor.Test // Assert Assert.Empty(result.Edits); - AssertFrame.Attribute(oldAttributeFrame, "will remain", retainedHandler); - AssertFrame.Attribute(newAttributeFrame, "will remain", retainedHandler); + AssertFrame.Attribute(oldAttributeFrame, "ontest", retainedHandler); + AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler); Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId); Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId); } @@ -1181,11 +1181,11 @@ namespace Microsoft.AspNetCore.Blazor.Test // Arrange UIEventHandler retainedHandler = _ => { }; oldTree.OpenElement(0, "My element"); - oldTree.AddAttribute(0, "will remain", retainedHandler); + oldTree.AddAttribute(0, "ontest", retainedHandler); oldTree.CloseElement(); newTree.OpenElement(0, "My element"); newTree.AddAttribute(0, "another-attribute", "go down the slow path please"); - newTree.AddAttribute(0, "will remain", retainedHandler); + newTree.AddAttribute(0, "ontest", retainedHandler); newTree.CloseElement(); // Act @@ -1195,8 +1195,8 @@ namespace Microsoft.AspNetCore.Blazor.Test // Assert Assert.Single(result.Edits); - AssertFrame.Attribute(oldAttributeFrame, "will remain", retainedHandler); - AssertFrame.Attribute(newAttributeFrame, "will remain", retainedHandler); + AssertFrame.Attribute(oldAttributeFrame, "ontest", retainedHandler); + AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler); Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId); Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId); } diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 8db5dd9eec..019f6f988b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var component = new EventComponent { - Handler = args => { receivedArgs = args; } + OnTest = args => { receivedArgs = args; } }; var componentId = renderer.AssignComponentId(component); component.TriggerRender(); @@ -189,6 +189,34 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Same(eventArgs, receivedArgs); } + [Fact] + public void CanDispatchTypedEventsToTopLevelComponents() + { + // Arrange: Render a component with an event handler + var renderer = new TestRenderer(); + UIMouseEventArgs receivedArgs = null; + + var component = new EventComponent + { + OnClick = args => { receivedArgs = args; } + }; + var componentId = renderer.AssignComponentId(component); + component.TriggerRender(); + + var eventHandlerId = renderer.Batches.Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; + + // Assert: Event not yet fired + Assert.Null(receivedArgs); + + // Act/Assert: Event can be fired + var eventArgs = new UIMouseEventArgs(); + renderer.DispatchEvent(componentId, eventHandlerId, eventArgs); + Assert.Same(eventArgs, receivedArgs); + } + [Fact] public void CanDispatchEventsToNestedComponents() { @@ -209,7 +237,7 @@ namespace Microsoft.AspNetCore.Blazor.Test .ReferenceFrames .Single(frame => frame.FrameType == RenderTreeFrameType.Component); var nestedComponent = (EventComponent)nestedComponentFrame.Component; - nestedComponent.Handler = args => { receivedArgs = args; }; + nestedComponent.OnTest = args => { receivedArgs = args; }; var nestedComponentId = nestedComponentFrame.ComponentId; nestedComponent.TriggerRender(); @@ -237,7 +265,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var component = new TestComponent(builder => { builder.OpenElement(0, "mybutton"); - builder.AddAttribute(1, "my click event", handler); + builder.AddAttribute(1, "onclick", handler); builder.CloseElement(); }); @@ -476,7 +504,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var renderer = new TestRenderer(); var eventCount = 0; UIEventHandler origEventHandler = args => { eventCount++; }; - var component = new EventComponent { Handler = origEventHandler }; + var component = new EventComponent { OnTest = origEventHandler }; var componentId = renderer.AssignComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() @@ -492,7 +520,7 @@ namespace Microsoft.AspNetCore.Blazor.Test // Now change the attribute value var newEventCount = 0; - component.Handler = args => { newEventCount++; }; + component.OnTest = args => { newEventCount++; }; component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event, but can fire the new event @@ -513,7 +541,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var renderer = new TestRenderer(); var eventCount = 0; UIEventHandler origEventHandler = args => { eventCount++; }; - var component = new EventComponent { Handler = origEventHandler }; + var component = new EventComponent { OnTest = origEventHandler }; var componentId = renderer.AssignComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() @@ -528,7 +556,7 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(1, eventCount); // Now remove the event attribute - component.Handler = null; + component.OnTest = null; component.TriggerRender(); // Act/Assert 2: Can no longer fire the original event @@ -551,7 +579,7 @@ namespace Microsoft.AspNetCore.Blazor.Test IncludeChild = true, ChildParameters = new Dictionary { - { nameof(EventComponent.Handler), origEventHandler } + { nameof(EventComponent.OnTest), origEventHandler } } }; var rootComponentId = renderer.AssignComponentId(component); @@ -595,7 +623,7 @@ namespace Microsoft.AspNetCore.Blazor.Test var renderer = new TestRenderer(); var eventCount = 0; UIEventHandler origEventHandler = args => { eventCount++; }; - var component = new EventComponent { Handler = origEventHandler }; + var component = new EventComponent { OnTest = origEventHandler }; var componentId = renderer.AssignComponentId(component); component.TriggerRender(); var origEventHandlerId = renderer.Batches.Single() @@ -634,7 +662,7 @@ namespace Microsoft.AspNetCore.Blazor.Test { builder.AddContent(0, "Child event count: " + eventCount); builder.OpenComponent(1); - builder.AddAttribute(2, nameof(EventComponent.Handler), args => + builder.AddAttribute(2, nameof(EventComponent.OnTest), args => { eventCount++; rootComponent.TriggerRender(); @@ -822,7 +850,7 @@ namespace Microsoft.AspNetCore.Blazor.Test if (shouldRenderChild) { builder.OpenComponent(1); - builder.AddAttribute(2, nameof(RendersSelfAfterEventComponent.OnClick), (Action)(() => + builder.AddAttribute(2, "onclick", (Action)((object obj) => { // First we queue (1) a re-render of the root component, then the child component // will queue (2) its own re-render. But by the time (1) completes, the child will @@ -843,7 +871,7 @@ namespace Microsoft.AspNetCore.Blazor.Test .ComponentId; var origEventHandlerId = renderer.Batches.Single() .ReferenceFrames - .Where(f => f.FrameType == RenderTreeFrameType.Attribute) + .Where(f => f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == "onclick") .Single(f => f.AttributeEventHandlerId != 0) .AttributeEventHandlerId; @@ -925,7 +953,9 @@ namespace Microsoft.AspNetCore.Blazor.Test private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent { - public UIEventHandler Handler { get; set; } + public UIEventHandler OnTest { get; set; } + public UIMouseEventHandler OnClick { get; set; } + public bool SkipElement { get; set; } private int renderCount = 0; @@ -936,15 +966,19 @@ namespace Microsoft.AspNetCore.Blazor.Test { builder.OpenElement(1, "parent"); builder.OpenElement(2, "some element"); - if (Handler != null) + if (OnTest != null) { - builder.AddAttribute(3, "some event", Handler); + builder.AddAttribute(3, "ontest", OnTest); + } + if (OnClick != null) + { + builder.AddAttribute(4, "onclick", OnClick); } builder.CloseElement(); builder.CloseElement(); } builder.CloseElement(); - builder.AddContent(4, $"Render count: {++renderCount}"); + builder.AddContent(5, $"Render count: {++renderCount}"); } public void HandleEvent(UIEventHandler handler, UIEventArgs args) @@ -995,7 +1029,7 @@ namespace Microsoft.AspNetCore.Blazor.Test private class RendersSelfAfterEventComponent : IComponent, IHandleEvent { - public Action OnClick { get; set; } + public Action OnClick { get; set; } private RenderHandle _renderHandle; @@ -1018,7 +1052,7 @@ namespace Microsoft.AspNetCore.Blazor.Test => _renderHandle.Render(builder => { builder.OpenElement(0, "my button"); - builder.AddAttribute(1, "my click handler", eventArgs => OnClick()); + builder.AddAttribute(1, "my click handler", eventArgs => OnClick(eventArgs)); builder.CloseElement(); }); } diff --git a/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml b/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml index 92e4e0aa3e..31b5e4dd57 100644 --- a/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml +++ b/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml @@ -11,8 +11,8 @@ Type here: @functions { List keysPressed = new List(); - void OnKeyPressed(UIEventArgs eventArgs) + void OnKeyPressed(UIKeyboardEventArgs eventArgs) { - keysPressed.Add(((UIKeyboardEventArgs)eventArgs).Key); + keysPressed.Add(eventArgs.Key); } }