Throw unhandled exceptions during prerendering
Fixes: #8609 Currently exceptions thrown during prerendering are simply logged. This change uses the existing *unhandled exception* mechanism of the renderer/circuit to throw these. The result is that the developer exception page just works for prerendering.
This commit is contained in:
parent
2a08c6e54d
commit
b743ba2f66
|
|
@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float?> setter, float? existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<float> setter, float existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<string> setter, string existingValue) { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) where T : System.Enum { throw null; }
|
||||
public static Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIChangeEventArgs> CreateBinder<T>(this Microsoft.AspNetCore.Components.EventCallbackFactory factory, object receiver, System.Action<T> setter, T existingValue) where T : struct, System.Enum { throw null; }
|
||||
}
|
||||
public static partial class EventCallbackFactoryUIEventArgsExtensions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,74 +11,263 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
public static class EventCallbackFactoryBinderExtensions
|
||||
{
|
||||
private delegate bool BindConverter<T>(object obj, out T value);
|
||||
|
||||
// Perf: conversion delegates are written as static funcs so we can prevent
|
||||
// allocations for these simple cases.
|
||||
private static Func<object, string> ConvertToString = (obj) => (string)obj;
|
||||
private readonly static BindConverter<string> ConvertToString = ConvertToStringCore;
|
||||
|
||||
private static Func<object, bool> ConvertToBool = (obj) => (bool)obj;
|
||||
private static Func<object, bool?> ConvertToNullableBool = (obj) => (bool?)obj;
|
||||
|
||||
private static Func<object, int> ConvertToInt = (obj) => int.Parse((string)obj);
|
||||
private static Func<object, int?> ConvertToNullableInt = (obj) =>
|
||||
private static bool ConvertToStringCore(object obj, out string value)
|
||||
{
|
||||
if (int.TryParse((string)obj, out var value))
|
||||
// We expect the input to already be a string.
|
||||
value = (string)obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BindConverter<bool> ConvertToBool = ConvertToBoolCore;
|
||||
private static BindConverter<bool?> ConvertToNullableBool = ConvertToNullableBoolCore;
|
||||
|
||||
private static bool ConvertToBoolCore(object obj, out bool value)
|
||||
{
|
||||
// We expect the input to already be a bool.
|
||||
value = (bool)obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableBoolCore(object obj, out bool? value)
|
||||
{
|
||||
// We expect the input to already be a bool.
|
||||
value = (bool?)obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BindConverter<int> ConvertToInt = ConvertToIntCore;
|
||||
private static BindConverter<int?> ConvertToNullableInt = ConvertToNullableIntCore;
|
||||
|
||||
private static bool ConvertToIntCore(object obj, out int value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return value;
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private static Func<object, long> ConvertToLong = (obj) => long.Parse((string)obj);
|
||||
private static Func<object, long?> ConvertToNullableLong = (obj) =>
|
||||
{
|
||||
if (long.TryParse((string)obj, out var value))
|
||||
if (!int.TryParse(text, out var converted))
|
||||
{
|
||||
return value;
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Func<object, float> ConvertToFloat = (obj) => float.Parse((string)obj);
|
||||
private static Func<object, float?> ConvertToNullableFloat = (obj) =>
|
||||
private static bool ConvertToNullableIntCore(object obj, out int? value)
|
||||
{
|
||||
if (float.TryParse((string)obj, out var value))
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return value;
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private static Func<object, double> ConvertToDouble = (obj) => double.Parse((string)obj);
|
||||
private static Func<object, double?> ConvertToNullableDouble = (obj) =>
|
||||
{
|
||||
if (double.TryParse((string)obj, out var value))
|
||||
if (!int.TryParse(text, out var converted))
|
||||
{
|
||||
return value;
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Func<object, decimal> ConvertToDecimal = (obj) => decimal.Parse((string)obj);
|
||||
private static Func<object, decimal?> ConvertToNullableDecimal = (obj) =>
|
||||
private static BindConverter<long> ConvertToLong = ConvertToLongCore;
|
||||
private static BindConverter<long?> ConvertToNullableLong = ConvertToNullableLongCore;
|
||||
|
||||
private static bool ConvertToLongCore(object obj, out long value)
|
||||
{
|
||||
if (decimal.TryParse((string)obj, out var value))
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return value;
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private static class EnumConverter<T> where T : Enum
|
||||
{
|
||||
public static Func<object, T> Convert = (obj) =>
|
||||
if (!long.TryParse(text, out var converted))
|
||||
{
|
||||
return (T)Enum.Parse(typeof(T), (string)obj);
|
||||
};
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableLongCore(object obj, out long? value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!long.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BindConverter<float> ConvertToFloat = ConvertToFloatCore;
|
||||
private static BindConverter<float?> ConvertToNullableFloat = ConvertToNullableFloatCore;
|
||||
|
||||
private static bool ConvertToFloatCore(object obj, out float value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!float.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableFloatCore(object obj, out float? value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!float.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BindConverter<double> ConvertToDouble = ConvertToDoubleCore;
|
||||
private static BindConverter<double?> ConvertToNullableDouble = ConvertToNullableDoubleCore;
|
||||
|
||||
private static bool ConvertToDoubleCore(object obj, out double value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!double.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableDoubleCore(object obj, out double? value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!double.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BindConverter<decimal> ConvertToDecimal = ConvertToDecimalCore;
|
||||
private static BindConverter<decimal?> ConvertToNullableDecimal = ConvertToNullableDecimalCore;
|
||||
|
||||
private static bool ConvertToDecimalCore(object obj, out decimal value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertToNullableDecimalCore(object obj, out decimal? value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class EnumConverter<T> where T : struct, Enum
|
||||
{
|
||||
public static readonly BindConverter<T> Convert = ConvertCore;
|
||||
|
||||
public static bool ConvertCore(object obj, out T value)
|
||||
{
|
||||
var text = (string)obj;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<T>(text, out var converted))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = converted;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -330,7 +519,22 @@ namespace Microsoft.AspNetCore.Components
|
|||
// when a format is used.
|
||||
Action<UIChangeEventArgs> callback = (e) =>
|
||||
{
|
||||
setter(ConvertDateTime(e.Value, format: null));
|
||||
DateTime value = default;
|
||||
var converted = false;
|
||||
try
|
||||
{
|
||||
value = ConvertDateTime(e.Value, format: null);
|
||||
converted = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// See comments in CreateBinderCore
|
||||
if (converted)
|
||||
{
|
||||
setter(value);
|
||||
}
|
||||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
}
|
||||
|
|
@ -355,7 +559,22 @@ namespace Microsoft.AspNetCore.Components
|
|||
// when a format is used.
|
||||
Action<UIChangeEventArgs> callback = (e) =>
|
||||
{
|
||||
setter(ConvertDateTime(e.Value, format));
|
||||
DateTime value = default;
|
||||
var converted = false;
|
||||
try
|
||||
{
|
||||
value = ConvertDateTime(e.Value, format);
|
||||
converted = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// See comments in CreateBinderCore
|
||||
if (converted)
|
||||
{
|
||||
setter(value);
|
||||
}
|
||||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
}
|
||||
|
|
@ -373,7 +592,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
this EventCallbackFactory factory,
|
||||
object receiver,
|
||||
Action<T> setter,
|
||||
T existingValue) where T : Enum
|
||||
T existingValue) where T : struct, Enum
|
||||
{
|
||||
return CreateBinderCore<T>(factory, receiver, setter, EnumConverter<T>.Convert);
|
||||
}
|
||||
|
|
@ -399,11 +618,27 @@ namespace Microsoft.AspNetCore.Components
|
|||
this EventCallbackFactory factory,
|
||||
object receiver,
|
||||
Action<T> setter,
|
||||
Func<object, T> converter)
|
||||
BindConverter<T> converter)
|
||||
{
|
||||
Action<UIChangeEventArgs> callback = e =>
|
||||
{
|
||||
setter(converter(e.Value));
|
||||
T value = default;
|
||||
var converted = false;
|
||||
try
|
||||
{
|
||||
converted = converter(e.Value, out value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// We only invoke the setter if the conversion didn't throw. This is valuable because it allows us to attempt
|
||||
// to process invalid input but avoid dirtying the state of the component if can't be converted. Imagine if
|
||||
// we assigned default(T) on failure - this would result in trouncing the user's typed in value.
|
||||
if (converted)
|
||||
{
|
||||
setter(value);
|
||||
}
|
||||
};
|
||||
return factory.Create<UIChangeEventArgs>(receiver, callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
public class EventCallbackFactoryBinderExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateBinder_ThrowsConversionException()
|
||||
public async Task CreateBinder_SwallowsConversionException()
|
||||
{
|
||||
// Arrange
|
||||
var value = 17;
|
||||
|
|
@ -20,12 +20,61 @@ namespace Microsoft.AspNetCore.Components
|
|||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<FormatException>(() =>
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
|
||||
|
||||
Assert.Equal(17, value); // Setter not called
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_ThrowsSetterException()
|
||||
{
|
||||
// Arrange
|
||||
var component = new EventCountingComponent();
|
||||
Action<int> setter = (_) => { throw new InvalidTimeZoneException(); };
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, 17);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(() =>
|
||||
{
|
||||
return binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
|
||||
return binder.InvokeAsync(new UIChangeEventArgs() { Value = "18", });
|
||||
});
|
||||
|
||||
Assert.Equal(17, value);
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_BindsEmpty_DoesNotCallSetter()
|
||||
{
|
||||
// Arrange
|
||||
var value = 17;
|
||||
var component = new EventCountingComponent();
|
||||
Action<int> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "not-an-integer!", });
|
||||
|
||||
Assert.Equal(17, value); // Setter not called
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateBinder_BindsEmpty_CallsSetterForNullable()
|
||||
{
|
||||
// Arrange
|
||||
var value = (int?)17;
|
||||
var component = new EventCountingComponent();
|
||||
Action<int?> setter = (_) => value = _;
|
||||
|
||||
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
|
||||
|
||||
// Act
|
||||
await binder.InvokeAsync(new UIChangeEventArgs() { Value = "", });
|
||||
|
||||
Assert.Null(value); // Setter called
|
||||
Assert.Equal(1, component.Count);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// 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.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
|
@ -26,8 +28,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
GetFullUri(context.Request),
|
||||
GetFullBaseUri(context.Request));
|
||||
|
||||
// We don't need to unsubscribe because the circuit host object is scoped to this call.
|
||||
circuitHost.UnhandledException += CircuitHost_UnhandledException;
|
||||
|
||||
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
|
||||
// reconnect to it from the ComponentsHub.
|
||||
// reconnect to it from the ComponentsHub. If we keep the circuit/renderer we also need to unsubscribe this error
|
||||
// handler.
|
||||
try
|
||||
{
|
||||
return await circuitHost.PrerenderComponentAsync(
|
||||
|
|
@ -40,6 +46,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
}
|
||||
|
||||
private void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
// Throw all exceptions encountered during pre-rendering so the default developer
|
||||
// error page can respond.
|
||||
ExceptionDispatchInfo.Capture((Exception)e.ExceptionObject).Throw();
|
||||
}
|
||||
|
||||
private string GetFullUri(HttpRequest request)
|
||||
{
|
||||
return UriHelper.BuildAbsolute(
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
{
|
||||
Log.UnhandledExceptionRenderingComponent(_logger, exception);
|
||||
}
|
||||
|
||||
UnhandledException?.Invoke(this, exception);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -199,8 +199,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.Equal("-42", boundValue.Text);
|
||||
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("-42", () => boundValue.Text);
|
||||
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("42\t");
|
||||
Browser.Equal("42", () => boundValue.Text);
|
||||
Assert.Equal("42", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -218,6 +222,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
Browser.Equal("", () => boundValue.Text);
|
||||
Assert.Equal("", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("-42\t");
|
||||
Browser.Equal("-42", () => boundValue.Text);
|
||||
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -245,8 +253,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.Equal("3000000000", boundValue.Text);
|
||||
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("3000000000", () => boundValue.Text);
|
||||
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("-3000000000\t");
|
||||
Browser.Equal("-3000000000", () => boundValue.Text);
|
||||
Assert.Equal("-3000000000", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -264,6 +276,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
Browser.Equal("", () => boundValue.Text);
|
||||
Assert.Equal("", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("3000000000\t");
|
||||
Browser.Equal("3000000000", () => boundValue.Text);
|
||||
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -291,8 +307,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.Equal("3.141", boundValue.Text);
|
||||
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("3.141", () => boundValue.Text);
|
||||
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("-3.141\t");
|
||||
Browser.Equal("-3.141", () => boundValue.Text);
|
||||
Assert.Equal("-3.141", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -310,6 +330,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
Browser.Equal("", () => boundValue.Text);
|
||||
Assert.Equal("", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("3.141\t");
|
||||
Browser.Equal("3.141", () => boundValue.Text);
|
||||
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -337,8 +361,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.Equal("3.14159265359", boundValue.Text);
|
||||
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("3.14159265359", () => boundValue.Text);
|
||||
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("-3.14159265359\t");
|
||||
Browser.Equal("-3.14159265359", () => boundValue.Text);
|
||||
Assert.Equal("-3.14159265359", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -363,6 +391,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
Browser.Equal("", () => boundValue.Text);
|
||||
Assert.Equal("", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("3.14159265359\t");
|
||||
Browser.Equal("3.14159265359", () => boundValue.Text);
|
||||
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -397,9 +429,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Assert.Equal("0.0000000000000000000000000001", boundValue.Text);
|
||||
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; value is not updated because it's not convertable.
|
||||
target.Clear();
|
||||
Browser.Equal("0.0000000000000000000000000001", () => boundValue.Text);
|
||||
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
// Decimal should preserve trailing zeros
|
||||
target.Clear();
|
||||
target.SendKeys("0.010\t");
|
||||
Browser.Equal("0.010", () => boundValue.Text);
|
||||
Assert.Equal("0.010", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -417,6 +453,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
Browser.Equal("", () => boundValue.Text);
|
||||
Assert.Equal("", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.SendKeys("0.0000000000000000000000000001\t");
|
||||
Browser.Equal("0.0000000000000000000000000001", () => boundValue.Text);
|
||||
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
|
||||
|
|
@ -434,5 +474,69 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal(string.Empty, () => boundValue.Text);
|
||||
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
|
||||
// This tests what happens you put invalid (unconvertable) input in. This is separate from the
|
||||
// other tests because it requires type="text" - the other tests use type="number"
|
||||
[Fact]
|
||||
public void CanBindTextbox_Decimal_InvalidInput()
|
||||
{
|
||||
var target = Browser.FindElement(By.Id("textbox-decimal-invalid"));
|
||||
var boundValue = Browser.FindElement(By.Id("textbox-decimal-invalid-value"));
|
||||
var mirrorValue = Browser.FindElement(By.Id("textbox-decimal-invalid-mirror"));
|
||||
Assert.Equal("0.0000000000000000000000000001", target.GetAttribute("value"));
|
||||
Assert.Equal("0.0000000000000000000000000001", boundValue.Text);
|
||||
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
target.SendKeys("0.01\t");
|
||||
Browser.Equal("0.01", () => boundValue.Text);
|
||||
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target to something invalid - the invalid value is preserved in the input, the other displays
|
||||
// don't change and still have the last value valid.
|
||||
target.SendKeys("A\t");
|
||||
Browser.Equal("0.01", () => boundValue.Text);
|
||||
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
|
||||
Assert.Equal("0.01A", target.GetAttribute("value"));
|
||||
|
||||
// Modify target to something valid.
|
||||
target.SendKeys(Keys.Backspace);
|
||||
target.SendKeys("1\t");
|
||||
Browser.Equal("0.011", () => boundValue.Text);
|
||||
Assert.Equal("0.011", mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
|
||||
// This tests what happens you put invalid (unconvertable) input in. This is separate from the
|
||||
// other tests because it requires type="text" - the other tests use type="number"
|
||||
[Fact]
|
||||
public void CanBindTextbox_NullableDecimal_InvalidInput()
|
||||
{
|
||||
var target = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid"));
|
||||
var boundValue = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid-value"));
|
||||
var mirrorValue = Browser.FindElement(By.Id("textbox-nullable-decimal-invalid-mirror"));
|
||||
Assert.Equal(string.Empty, target.GetAttribute("value"));
|
||||
Assert.Equal(string.Empty, boundValue.Text);
|
||||
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target; verify value is updated and that textboxes linked to the same data are updated
|
||||
target.Clear();
|
||||
target.SendKeys("0.01\t");
|
||||
Browser.Equal("0.01", () => boundValue.Text);
|
||||
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
|
||||
|
||||
// Modify target to something invalid - the invalid value is preserved in the input, the other displays
|
||||
// don't change and still have the last value valid.
|
||||
target.SendKeys("A\t");
|
||||
Browser.Equal("0.01", () => boundValue.Text);
|
||||
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
|
||||
Assert.Equal("0.01A", target.GetAttribute("value"));
|
||||
|
||||
// Modify target to something valid.
|
||||
target.SendKeys(Keys.Backspace);
|
||||
target.SendKeys("1\t");
|
||||
Browser.Equal("0.011", () => boundValue.Text);
|
||||
Assert.Equal("0.011", mirrorValue.GetAttribute("value"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,18 @@
|
|||
<span id="textbox-nullable-decimal-value">@textboxNullableDecimalValue</span>
|
||||
<input id="textbox-nullable-decimal-mirror" bind="textboxNullableDecimalValue" readonly />
|
||||
</p>
|
||||
<p>
|
||||
decimal (invalid-input):
|
||||
<input id="textbox-decimal-invalid" bind="textboxDecimalInvalidValue" />
|
||||
<span id="textbox-decimal-invalid-value">@textboxDecimalInvalidValue</span>
|
||||
<input id="textbox-decimal-invalid-mirror" bind="textboxDecimalInvalidValue" readonly />
|
||||
</p>
|
||||
<p>
|
||||
Nullable decimal (invalid-input):
|
||||
<input id="textbox-nullable-decimal-invalid" bind="textboxNullableDecimalInvalidValue" />
|
||||
<span id="textbox-nullable-decimal-invalid-value">@textboxNullableDecimalInvalidValue</span>
|
||||
<input id="textbox-nullable-decimal-invalid-mirror" bind="textboxNullableDecimalInvalidValue" readonly />
|
||||
</p>
|
||||
|
||||
<h2>Text Area</h2>
|
||||
<p>
|
||||
|
|
@ -116,7 +128,7 @@
|
|||
<option value=@SelectableValue.Third>Third choice</option>
|
||||
@if (includeFourthOption)
|
||||
{
|
||||
<option value=@SelectableValue.Fourth>Fourth choice</option>
|
||||
<option value=@SelectableValue.Fourth>Fourth choice</option>
|
||||
}
|
||||
</select>
|
||||
<span id="select-box-value">@selectValue</span>
|
||||
|
|
@ -144,6 +156,8 @@
|
|||
double? textboxNullableDoubleValue = null;
|
||||
decimal textboxDecimalValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalValue = null;
|
||||
decimal textboxDecimalInvalidValue = 0.0000000000000000000000000001M;
|
||||
decimal? textboxNullableDecimalInvalidValue = null;
|
||||
|
||||
bool includeFourthOption = false;
|
||||
enum SelectableValue { First, Second, Third, Fourth }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Parser.Html;
|
||||
using BasicWebSite;
|
||||
using BasicWebSite.Services;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -17,18 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||
{
|
||||
Factory = fixture;
|
||||
Client = Client ?? CreateClient(fixture);
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
public MvcTestFixture<StartupWithoutEndpointRouting> Factory { get; }
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_BasicComponent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var response = await Client.GetAsync("http://localhost/components");
|
||||
var client = CreateClient(Factory);
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
|
@ -41,9 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
|
||||
.CreateClient();
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components");
|
||||
|
||||
|
|
@ -58,7 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task Renders_RoutingComponent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var response = await Client.GetAsync("http://localhost/components/routable");
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/routable");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
|
@ -71,9 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
|
||||
.CreateClient();
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/routable");
|
||||
|
||||
|
|
@ -84,6 +83,21 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/throws");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Contains("InvalidTimeZoneException: test", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_AsyncComponent()
|
||||
{
|
||||
|
|
@ -138,8 +152,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
</table>
|
||||
|
||||
";
|
||||
|
||||
var response = await Client.GetAsync("http://localhost/components");
|
||||
var client = CreateClient(Factory);
|
||||
var response = await client.GetAsync("http://localhost/components");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
|
@ -164,12 +178,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
}
|
||||
|
||||
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture, Action<IWebHostBuilder> configure = null)
|
||||
{
|
||||
var loopHandler = new LoopHttpHandler();
|
||||
|
||||
var client = fixture
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(ConfigureTestWeatherForecastService))
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
configure?.Invoke(builder);
|
||||
builder.ConfigureServices(ConfigureTestWeatherForecastService);
|
||||
})
|
||||
.CreateClient();
|
||||
|
||||
// We configure the inner handler with a handler to this TestServer instance so that calls to the
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ namespace BasicWebSite.Controllers
|
|||
};
|
||||
|
||||
[HttpGet("/components")]
|
||||
[HttpGet("/components/routable")]
|
||||
[HttpGet("/components/{component}")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
@page "/components/throws"
|
||||
|
||||
@* This is expected to throw and result in a 500 *@
|
||||
@functions {
|
||||
protected override async Task OnInitAsync()
|
||||
{
|
||||
await base.OnInitAsync();
|
||||
throw new InvalidTimeZoneException("test");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue