Support async main (#17673)
This commit is contained in:
parent
3fba107522
commit
3a93704737
|
|
@ -0,0 +1,89 @@
|
|||
// 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 System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
internal static class EntrypointInvoker
|
||||
{
|
||||
// This method returns void because currently the JS side is not listening to any result,
|
||||
// nor will it handle any exceptions. We handle all exceptions internally to this method.
|
||||
// In the future we may want Blazor.start to return something that exposes the possibly-async
|
||||
// entrypoint result to the JS caller. There's no requirement to do that today, and if we
|
||||
// do change this it will be non-breaking.
|
||||
public static void InvokeEntrypoint(string assemblyName, string[] args)
|
||||
{
|
||||
object entrypointResult;
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.Load(assemblyName);
|
||||
var entrypoint = FindUnderlyingEntrypoint(assembly);
|
||||
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args } : new object[] { };
|
||||
entrypointResult = entrypoint.Invoke(null, @params);
|
||||
}
|
||||
catch (Exception syncException)
|
||||
{
|
||||
HandleStartupException(syncException);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the entrypoint is async, handle async exceptions in the same way that we would
|
||||
// have handled sync ones
|
||||
if (entrypointResult is Task entrypointTask)
|
||||
{
|
||||
entrypointTask.ContinueWith(task =>
|
||||
{
|
||||
if (task.Exception != null)
|
||||
{
|
||||
HandleStartupException(task.Exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodBase FindUnderlyingEntrypoint(Assembly assembly)
|
||||
{
|
||||
// This is the entrypoint declared in .NET metadata. In the case of async main, it's the
|
||||
// compiler-generated wrapper method. Otherwise it's the developer-defined method.
|
||||
var metadataEntrypointMethodBase = assembly.EntryPoint;
|
||||
|
||||
// For "async Task Main", the C# compiler generates a method called "<Main>"
|
||||
// that is marked as the assembly entrypoint. Detect this case, and instead of
|
||||
// calling "<Whatever>", call the sibling "Whatever".
|
||||
if (metadataEntrypointMethodBase.IsSpecialName)
|
||||
{
|
||||
var origName = metadataEntrypointMethodBase.Name;
|
||||
var origNameLength = origName.Length;
|
||||
if (origNameLength > 2)
|
||||
{
|
||||
var candidateMethodName = origName.Substring(1, origNameLength - 2);
|
||||
var candidateMethod = metadataEntrypointMethodBase.DeclaringType.GetMethod(
|
||||
candidateMethodName,
|
||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
null,
|
||||
metadataEntrypointMethodBase.GetParameters().Select(p => p.ParameterType).ToArray(),
|
||||
null);
|
||||
|
||||
if (candidateMethod != null)
|
||||
{
|
||||
return candidateMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either it's not async main, or for some reason we couldn't locate the underlying entrypoint,
|
||||
// so use the one from assembly metadata.
|
||||
return metadataEntrypointMethodBase;
|
||||
}
|
||||
|
||||
private static void HandleStartupException(Exception exception)
|
||||
{
|
||||
// Logs to console, and causes the error UI to appear
|
||||
Console.Error.WriteLine(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// 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.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
public class EntrypointInvokerTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void InvokesEntrypoint_Sync_Success(bool hasReturnValue, bool hasParams)
|
||||
{
|
||||
// Arrange
|
||||
var returnType = hasReturnValue ? "int" : "void";
|
||||
var paramsDecl = hasParams ? "string[] args" : string.Empty;
|
||||
var returnStatement = hasReturnValue ? "return 123;" : "return;";
|
||||
var assembly = CompileToAssembly(@"
|
||||
static " + returnType + @" Main(" + paramsDecl + @")
|
||||
{
|
||||
DidMainExecute = true;
|
||||
" + returnStatement + @"
|
||||
}", out var didMainExecute);
|
||||
|
||||
// Act
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
|
||||
// Assert
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void InvokesEntrypoint_Async_Success(bool hasReturnValue, bool hasParams)
|
||||
{
|
||||
// Arrange
|
||||
var returnTypeGenericParam = hasReturnValue ? "<int>" : string.Empty;
|
||||
var paramsDecl = hasParams ? "string[] args" : string.Empty;
|
||||
var returnStatement = hasReturnValue ? "return 123;" : "return;";
|
||||
var assembly = CompileToAssembly(@"
|
||||
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
|
||||
|
||||
static async Task" + returnTypeGenericParam + @" Main(" + paramsDecl + @")
|
||||
{
|
||||
await ContinueTcs.Task;
|
||||
DidMainExecute = true;
|
||||
" + returnStatement + @"
|
||||
}", out var didMainExecute);
|
||||
|
||||
// Act/Assert 1: Waits for task
|
||||
// The fact that we're not blocking here proves that we're not executing the
|
||||
// metadata-declared entrypoint, as that would block
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
|
||||
tcs.SetResult(null);
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokesEntrypoint_Sync_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = CompileToAssembly(@"
|
||||
public static void Main()
|
||||
{
|
||||
DidMainExecute = true;
|
||||
throw new InvalidTimeZoneException(""Test message"");
|
||||
}", out var didMainExecute);
|
||||
|
||||
// Act/Assert
|
||||
// The fact that this doesn't throw shows that EntrypointInvoker is doing something
|
||||
// to handle the exception. We can't assert about what it does here, because that
|
||||
// would involve capturing console output, which isn't safe in unit tests. Instead
|
||||
// we'll check this in E2E tests.
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokesEntrypoint_Async_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = CompileToAssembly(@"
|
||||
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
await ContinueTcs.Task;
|
||||
DidMainExecute = true;
|
||||
throw new InvalidTimeZoneException(""Test message"");
|
||||
}", out var didMainExecute);
|
||||
|
||||
// Act/Assert 1: Waits for task
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
// As above, we can't directly observe the exception handling behavior here,
|
||||
// so this is covered in E2E tests instead.
|
||||
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
|
||||
tcs.SetResult(null);
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
private static Assembly CompileToAssembly(string mainMethod, out Func<bool> didMainExecute)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SomeApp
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static bool DidMainExecute { get; private set; }
|
||||
|
||||
" + mainMethod + @"
|
||||
}
|
||||
}");
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
$"TestAssembly-{Guid.NewGuid().ToString("D")}",
|
||||
new[] { syntaxTree },
|
||||
new[] { MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) },
|
||||
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
|
||||
using var ms = new MemoryStream();
|
||||
var compilationResult = compilation.Emit(ms);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
|
||||
|
||||
var didMainExecuteProp = assembly.GetType("SomeApp.Program").GetProperty("DidMainExecute");
|
||||
didMainExecute = () => (bool)didMainExecuteProp.GetValue(null);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
||||
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
|
@ -11,6 +11,12 @@
|
|||
<body>
|
||||
<app>Loading...</app>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -248,6 +248,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mono.WebAssembly.Interop",
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj", "{D141CFEE-D10A-406B-8963-F86FA13732E3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{F2E27E1C-2E47-42C1-9AC7-36265A381717}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1518,6 +1520,18 @@ Global
|
|||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1632,6 +1646,7 @@ Global
|
|||
{A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||
{21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||
{D141CFEE-D10A-406B-8963-F86FA13732E3} = {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05}
|
||||
{F2E27E1C-2E47-42C1-9AC7-36265A381717} = {44E0D4F3-4430-4175-B482-0D1AEE4BB699}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}
|
||||
|
|
|
|||
|
|
@ -73,5 +73,7 @@ interface BootJsonData {
|
|||
|
||||
window['Blazor'].start = boot;
|
||||
if (shouldAutoStart()) {
|
||||
boot();
|
||||
boot().catch(error => {
|
||||
Module.printErr(error); // Logs it, and causes the error UI to appear
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ let assembly_load: (assemblyName: string) => number;
|
|||
let find_class: (assemblyHandle: number, namespace: string, className: string) => number;
|
||||
let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle;
|
||||
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object;
|
||||
let mono_call_assembly_entry_point: (assemblyName: string, args: System_Object[]) => System_Object;
|
||||
let mono_obj_array_new: (length: number) => System_Object;
|
||||
let mono_string_array_new: (length: number) => System_Array<System_String>;
|
||||
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
|
||||
let mono_string: (jsString: string) => System_String;
|
||||
const appBinDirName = 'appBinDir';
|
||||
|
|
@ -41,9 +40,18 @@ export const monoPlatform: Platform = {
|
|||
|
||||
findMethod: findMethod,
|
||||
|
||||
callEntryPoint: function callEntryPoint(assemblyName: string): System_Object {
|
||||
const empty_array = mono_obj_array_new(0);
|
||||
return mono_call_assembly_entry_point(assemblyName, [empty_array]);
|
||||
callEntryPoint: function callEntryPoint(assemblyName: string) {
|
||||
// Instead of using Module.mono_call_assembly_entry_point, we have our own logic for invoking
|
||||
// the entrypoint which adds support for async main.
|
||||
// Currently we disregard the return value from the entrypoint, whether it's sync or async.
|
||||
// In the future, we might want Blazor.start to return a Promise<Promise<value>>, where the
|
||||
// outer promise reflects the startup process, and the inner one reflects the possibly-async
|
||||
// .NET entrypoint method.
|
||||
const invokeEntrypoint = findMethod('Microsoft.AspNetCore.Blazor', 'Microsoft.AspNetCore.Blazor.Hosting', 'EntrypointInvoker', 'InvokeEntrypoint');
|
||||
this.callMethod(invokeEntrypoint, null, [
|
||||
this.toDotNetString(assemblyName),
|
||||
mono_string_array_new(0) // In the future, we may have a way of supplying arg strings. For now, we always supply an empty string[].
|
||||
]);
|
||||
},
|
||||
|
||||
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object {
|
||||
|
|
@ -263,11 +271,9 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
|
|||
'number',
|
||||
]);
|
||||
|
||||
mono_call_assembly_entry_point = Module.mono_call_assembly_entry_point;
|
||||
|
||||
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
|
||||
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']);
|
||||
mono_obj_array_new = Module.cwrap ('mono_wasm_obj_array_new', 'number', ['number']);
|
||||
mono_string_array_new = Module.cwrap('mono_wasm_string_array_new', 'number', ['number']);
|
||||
|
||||
MONO.loaded_files = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export interface Platform {
|
||||
start(loadAssemblyUrls: string[]): Promise<void>;
|
||||
|
||||
callEntryPoint(assemblyName: string): System_Object;
|
||||
callEntryPoint(assemblyName: string): void;
|
||||
findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
|
||||
callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ using BasicTestApp;
|
|||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
||||
public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest
|
||||
public class ServerErrorNotificationTest : ErrorNotificationTest
|
||||
{
|
||||
public ErrorNotificationServerSideTest(
|
||||
public ServerErrorNotificationTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
|
|
@ -13,9 +13,9 @@ using Xunit.Abstractions;
|
|||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
||||
public class ErrorNotificationClientSideTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
public class ErrorNotificationTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public ErrorNotificationClientSideTest(
|
||||
public ErrorNotificationTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 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 BasicTestApp;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class StartupErrorNotificationTest : ServerTestBase<DevHostServerFixture<Program>>
|
||||
{
|
||||
public StartupErrorNotificationTest(
|
||||
BrowserFixture browserFixture,
|
||||
DevHostServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
_serverFixture.PathBase = ServerPathBase;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DisplaysNotificationForStartupException(bool errorIsAsync)
|
||||
{
|
||||
var url = $"{ServerPathBase}?error={(errorIsAsync ? "async" : "sync")}";
|
||||
|
||||
Navigate(url, noReload: true);
|
||||
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
|
||||
Assert.NotNull(errorUiElem);
|
||||
|
||||
Browser.Equal("block", () => errorUiElem.GetCssValue("display"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,12 +87,6 @@
|
|||
@((RenderFragment)RenderSelectedComponent)
|
||||
</app>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href class='reload'>Reload</a>
|
||||
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string SelectedComponentTypeName { get; set; } = "none";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
// 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.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Blazor.Hosting;
|
||||
using Mono.WebAssembly.Interop;
|
||||
|
||||
namespace BasicTestApp
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
await SimulateErrorsIfNeededForTest();
|
||||
|
||||
// We want the culture to be en-US so that the tests for bind can work consistently.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-US");
|
||||
|
||||
|
|
@ -19,5 +24,22 @@ namespace BasicTestApp
|
|||
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
|
||||
BlazorWebAssemblyHost.CreateDefaultBuilder()
|
||||
.UseBlazorStartup<Startup>();
|
||||
|
||||
// Supports E2E tests in StartupErrorNotificationTest
|
||||
private static async Task SimulateErrorsIfNeededForTest()
|
||||
{
|
||||
var currentUrl = new MonoWebAssemblyJSRuntime().Invoke<string>("getCurrentUrl");
|
||||
if (currentUrl.Contains("error=sync"))
|
||||
{
|
||||
throw new InvalidTimeZoneException("This is a synchronous startup exception");
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
|
||||
if (currentUrl.Contains("error=async"))
|
||||
{
|
||||
throw new InvalidTimeZoneException("This is an asynchronous startup exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@
|
|||
<body>
|
||||
<root>Loading...</root>
|
||||
|
||||
<!-- Explicit display:none required so StartupErrorNotificationTest can observe it change -->
|
||||
<div id="blazor-error-ui" style="display: none;">
|
||||
An unhandled error has occurred.
|
||||
<a href class='reload'>Reload</a>
|
||||
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
||||
</div>
|
||||
|
||||
<!-- Used for testing interop scenarios between JS and .NET -->
|
||||
<script src="js/jsinteroptests.js"></script>
|
||||
|
||||
|
|
@ -27,6 +34,10 @@
|
|||
function navigationManagerNavigate() {
|
||||
Blazor.navigateTo('/subdir/some-path');
|
||||
}
|
||||
|
||||
function getCurrentUrl() {
|
||||
return location.href;
|
||||
}
|
||||
</script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,23 @@
|
|||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#blazor-error-ui dismiss {
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
<!-- Used for testing interop scenarios between JS and .NET -->
|
||||
<script src="js/jsinteroptests.js"></script>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href class='reload'>Reload</a>
|
||||
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Used by ElementRefComponent
|
||||
function setElementValue(element, newValue) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue