From 3a93704737cceb38c4d51ae03075cf4eaad81b23 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 12 Dec 2019 15:05:12 +0000 Subject: [PATCH] Support async main (#17673) --- .../Blazor/src/Hosting/EntrypointInvoker.cs | 89 ++++++++++ .../test/Hosting/EntrypointInvokerTest.cs | 153 ++++++++++++++++++ .../Microsoft.AspNetCore.Blazor.Tests.csproj | 3 +- .../StandaloneApp/wwwroot/index.html | 8 +- src/Components/Components.sln | 15 ++ src/Components/Web.JS/src/Boot.WebAssembly.ts | 4 +- .../Web.JS/src/Platform/Mono/MonoPlatform.ts | 22 ++- .../Web.JS/src/Platform/Platform.ts | 2 +- .../ServerErrorNotificationTest.cs} | 5 +- ...ntSideTest.cs => ErrorNotificationTest.cs} | 4 +- .../Tests/StartupErrorNotificationTest.cs | 40 +++++ .../test/testassets/BasicTestApp/Index.razor | 6 - .../test/testassets/BasicTestApp/Program.cs | 24 ++- .../BasicTestApp/wwwroot/index.html | 11 ++ .../testassets/BasicTestApp/wwwroot/style.css | 14 +- .../TestServer/Pages/_ServerHost.cshtml | 6 + 16 files changed, 381 insertions(+), 25 deletions(-) create mode 100644 src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs create mode 100644 src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs rename src/Components/test/E2ETest/{Tests/ErrorNotificationServerSideTest.cs => ServerExecutionTests/ServerErrorNotificationTest.cs} (85%) rename src/Components/test/E2ETest/Tests/{ErrorNotificationClientSideTest.cs => ErrorNotificationTest.cs} (94%) create mode 100644 src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs diff --git a/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs b/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs new file mode 100644 index 0000000000..bce063bcf5 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs @@ -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 "
" + // that is marked as the assembly entrypoint. Detect this case, and instead of + // calling "", 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); + } + } +} diff --git a/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs b/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs new file mode 100644 index 0000000000..60a3d1638b --- /dev/null +++ b/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs @@ -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 ? "" : string.Empty; + var paramsDecl = hasParams ? "string[] args" : string.Empty; + var returnStatement = hasReturnValue ? "return 123;" : "return;"; + var assembly = CompileToAssembly(@" +public static TaskCompletionSource ContinueTcs { get; } = new TaskCompletionSource(); + +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)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 ContinueTcs { get; } = new TaskCompletionSource(); + +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)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null); + tcs.SetResult(null); + Assert.True(didMainExecute()); + } + + private static Assembly CompileToAssembly(string mainMethod, out Func 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; + } + } +} diff --git a/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj b/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj index a0acf4173e..be1cb3f836 100644 --- a/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj +++ b/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -6,6 +6,7 @@ + diff --git a/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html b/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html index 5da6ba26b3..fde34bd639 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html +++ b/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -11,6 +11,12 @@ Loading... +
+ An unhandled exception has occurred. See browser dev tools for details. + Reload + 🗙 +
+ diff --git a/src/Components/Components.sln b/src/Components/Components.sln index b4d1840025..91278955cf 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -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} diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index efecd016e9..1a80098301 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -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 + }); } diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts index 31fee329d6..527dcd5ab8 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts @@ -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; 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>, 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 = []; diff --git a/src/Components/Web.JS/src/Platform/Platform.ts b/src/Components/Web.JS/src/Platform/Platform.ts index 84587f943a..29eb04609e 100644 --- a/src/Components/Web.JS/src/Platform/Platform.ts +++ b/src/Components/Web.JS/src/Platform/Platform.ts @@ -1,7 +1,7 @@ export interface Platform { start(loadAssemblyUrls: string[]): Promise; - 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; diff --git a/src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs similarity index 85% rename from src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs rename to src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs index 6d2e860573..77b7da75d6 100644 --- a/src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs @@ -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 serverFixture, ITestOutputHelper output) diff --git a/src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs b/src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs similarity index 94% rename from src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs rename to src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs index 7c0705acde..883d1bc5ab 100644 --- a/src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs +++ b/src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs @@ -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> + public class ErrorNotificationTest : ServerTestBase> { - public ErrorNotificationClientSideTest( + public ErrorNotificationTest( BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) diff --git a/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs new file mode 100644 index 0000000000..75359253c0 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs @@ -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> + { + public StartupErrorNotificationTest( + BrowserFixture browserFixture, + DevHostServerFixture 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")); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 0548f382a8..eff29276a4 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -87,12 +87,6 @@ @((RenderFragment)RenderSelectedComponent) -
- An unhandled error has occurred. - Reload - 🗙 -
- @code { string SelectedComponentTypeName { get; set; } = "none"; diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index cebb226e7c..2be7d81b4e 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -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(); + + // Supports E2E tests in StartupErrorNotificationTest + private static async Task SimulateErrorsIfNeededForTest() + { + var currentUrl = new MonoWebAssemblyJSRuntime().Invoke("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"); + } + } } } diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html index 517d9d6fcc..a37c08a7d1 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html @@ -14,6 +14,13 @@ Loading... + + + @@ -27,6 +34,10 @@ function navigationManagerNavigate() { Blazor.navigateTo('/subdir/some-path'); } + + function getCurrentUrl() { + return location.href; + } diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/style.css b/src/Components/test/testassets/BasicTestApp/wwwroot/style.css index 777375d9e0..ea9900430b 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/style.css +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/style.css @@ -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 { diff --git a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml index af2f28f658..b0ba837af2 100644 --- a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml @@ -17,6 +17,12 @@ +
+ An unhandled error has occurred. + Reload + 🗙 +
+