Skip to content

Commit a68c961

Browse files
Dev exception notifications (#14636)
Blazor dev exception notification
1 parent f3b0fbe commit a68c961

File tree

18 files changed

+256
-6
lines changed

18 files changed

+256
-6
lines changed

src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/wwwroot/css/site.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ app {
9696
color: red;
9797
}
9898

99+
#error-ui {
100+
background: lightyellow;
101+
bottom: 0;
102+
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
103+
display: none;
104+
left: 0;
105+
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
106+
position: fixed;
107+
width: 100%;
108+
z-index: 1000;
109+
}
110+
111+
#error-ui .dismiss {
112+
cursor: pointer;
113+
position: absolute;
114+
right: 0.75rem;
115+
top: 0.5rem;
116+
}
117+
99118
@media (max-width: 767.98px) {
100119
.main .top-row {
101120
display: none;

src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/wwwroot/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!DOCTYPE html>
22
<html>
3+
34
<head>
45
<meta charset="utf-8" />
56
<meta name="viewport" content="width=device-width" />
@@ -8,9 +9,16 @@
89
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
910
<link href="css/site.css" rel="stylesheet" />
1011
</head>
12+
1113
<body>
1214
<app>Loading...</app>
1315

16+
<div id="error-ui">
17+
An unhandled error has occurred.
18+
<a href class="reload">Reload</a>
19+
<a class="dismiss">🗙</a>
20+
</div>
1421
<script src="_framework/blazor.webassembly.js"></script>
1522
</body>
23+
1624
</html>

src/Components/Samples/BlazorServerApp/Shared/MainLayout.razor

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@
1212
<div class="content px-4">
1313
@Body
1414
</div>
15+
16+
</div>
17+
<div id="error-ui">
18+
An unhandled error has occurred.
19+
<a class="reload">Reload</a>
20+
<a class="dismiss">X</a>
1521
</div>

src/Components/Samples/BlazorServerApp/wwwroot/css/site.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,23 @@ app {
111111
color: red;
112112
}
113113

114+
#error-ui {
115+
background: lightyellow;
116+
position: fixed;
117+
border: "1px solid";
118+
border-color: black;
119+
width: 100%;
120+
bottom: 0;
121+
left: 0;
122+
z-index: 1000;
123+
}
124+
125+
#error-ui .dismiss {
126+
position: absolute;
127+
right: 5px;
128+
top: 5px;
129+
}
130+
114131
@media (max-width: 767.98px) {
115132
.main .top-row {
116133
display: none;

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import '@dotnet/jsinterop';
22
import './GlobalExports';
33
import * as signalR from '@aspnet/signalr';
44
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
5+
import { showErrorNotification } from './BootErrors';
56
import { shouldAutoStart } from './BootCommon';
67
import { RenderQueue } from './Platform/Circuits/RenderQueue';
78
import { ConsoleLogger } from './Platform/Logging/Loggers';
@@ -106,6 +107,7 @@ async function initializeConnection(options: BlazorOptions, logger: Logger, circ
106107
connection.on('JS.Error', error => {
107108
renderingFailed = true;
108109
unhandledError(connection, error, logger);
110+
showErrorNotification();
109111
});
110112

111113
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
let hasFailed = false;
2+
3+
export async function showErrorNotification() {
4+
let errorUi = document.querySelector('#error-ui') as HTMLElement;
5+
if (errorUi) {
6+
errorUi.style.display = 'block';
7+
}
8+
9+
if (!hasFailed) {
10+
hasFailed = true;
11+
const errorUiReloads = document.querySelectorAll<HTMLElement>('#error-ui .reload');
12+
errorUiReloads.forEach(reload => {
13+
reload.onclick = function (e) {
14+
location.reload();
15+
e.preventDefault();
16+
};
17+
});
18+
19+
let errorUiDismiss = document.querySelectorAll<HTMLElement>('#error-ui .dismiss');
20+
errorUiDismiss.forEach(dismiss => {
21+
dismiss.onclick = function (e) {
22+
const errorUi = document.querySelector<HTMLElement>('#error-ui');
23+
if (errorUi) {
24+
errorUi.style.display = 'none';
25+
}
26+
e.preventDefault();
27+
};
28+
});
29+
}
30+
}

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
22
import { getFileNameFromUrl } from '../Url';
33
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
4+
import { showErrorNotification } from '../../BootErrors';
45

56
const assemblyHandleCache: { [assemblyName: string]: number } = {};
67
const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {};
@@ -232,7 +233,11 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
232233
const suppressMessages = ['DEBUGGING ENABLED'];
233234

234235
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`));
235-
module.printErr = line => console.error(`WASM: ${line}`);
236+
237+
module.printErr = line => {
238+
console.error(`WASM: ${line}`);
239+
showErrorNotification();
240+
};
236241
module.preRun = [];
237242
module.postRun = [];
238243
module.preloadPlugins = [];
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using BasicTestApp;
6+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
7+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
8+
using Microsoft.AspNetCore.E2ETesting;
9+
using OpenQA.Selenium;
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
13+
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
14+
{
15+
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
16+
public class ErrorNotificationClientSideTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
17+
{
18+
public ErrorNotificationClientSideTest(
19+
BrowserFixture browserFixture,
20+
ToggleExecutionModeServerFixture<Program> serverFixture,
21+
ITestOutputHelper output)
22+
: base(browserFixture, serverFixture, output)
23+
{
24+
}
25+
26+
protected override void InitializeAsyncCore()
27+
{
28+
// On WebAssembly, page reloads are expensive so skip if possible
29+
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
30+
Browser.MountTestComponent<ErrorComponent>();
31+
Browser.Exists(By.Id("error-ui"));
32+
Browser.Exists(By.TagName("button"));
33+
}
34+
35+
[Fact]
36+
public void ShowsErrorNotification_OnError_Dismiss()
37+
{
38+
var errorUi = Browser.FindElement(By.Id("error-ui"));
39+
Assert.Equal("none", errorUi.GetCssValue("display"));
40+
41+
var causeErrorButton = Browser.FindElement(By.TagName("button"));
42+
causeErrorButton.Click();
43+
44+
Browser.Exists(By.CssSelector("#error-ui[style='display: block;']"), TimeSpan.FromSeconds(10));
45+
46+
var reload = Browser.FindElement(By.ClassName("reload"));
47+
reload.Click();
48+
49+
Browser.DoesNotExist(By.TagName("button"));
50+
}
51+
52+
[Fact]
53+
public void ShowsErrorNotification_OnError_Reload()
54+
{
55+
var causeErrorButton = Browser.Exists(By.TagName("button"));
56+
var errorUi = Browser.FindElement(By.Id("error-ui"));
57+
Assert.Equal("none", errorUi.GetCssValue("display"));
58+
59+
causeErrorButton.Click();
60+
Browser.Exists(By.CssSelector("#error-ui[style='display: block;']"));
61+
62+
var dismiss = Browser.FindElement(By.ClassName("dismiss"));
63+
dismiss.Click();
64+
Browser.Exists(By.CssSelector("#error-ui[style='display: none;']"));
65+
}
66+
}
67+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using BasicTestApp;
5+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
6+
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
7+
using Microsoft.AspNetCore.E2ETesting;
8+
using OpenQA.Selenium;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
13+
{
14+
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
15+
public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest
16+
{
17+
public ErrorNotificationServerSideTest(
18+
BrowserFixture browserFixture,
19+
ToggleExecutionModeServerFixture<Program> serverFixture,
20+
ITestOutputHelper output)
21+
: base(browserFixture, serverFixture.WithServerExecution(), output)
22+
{
23+
}
24+
}
25+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div>
2+
<h2>Error throwing button</h2>
3+
<p>
4+
<button @onclick="@(IncrementCount)">Click me</button>
5+
</p>
6+
</div>
7+
8+
@code {
9+
int currentCount = 0;
10+
11+
void IncrementCount()
12+
{
13+
currentCount++;
14+
throw new NotImplementedException("Doing crazy things!");
15+
}
16+
}

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@using Microsoft.AspNetCore.Components.Rendering
22
<div id="test-selector">
3-
Select test:
3+
Select test:
44
<select id="test-selector-select" @bind=SelectedComponentTypeName>
55
<option value="none">Choose...</option>
66
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
@@ -20,6 +20,7 @@
2020
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
2121
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
2222
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
23+
<option value="BasicTestApp.ErrorComponent">Error throwing</option>
2324
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
2425
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
2526
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
@@ -82,6 +83,12 @@
8283
@((RenderFragment)RenderSelectedComponent)
8384
</app>
8485

86+
<div id="error-ui">
87+
An unhandled error has occurred.
88+
<a href class='reload'>Reload</a>
89+
<a class='dismiss' style="cursor: pointer;">🗙</a>
90+
</div>
91+
8592
@code {
8693
string SelectedComponentTypeName { get; set; } = "none";
8794

src/Components/test/testassets/BasicTestApp/wwwroot/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!DOCTYPE html>
22
<html>
3+
34
<head>
45
<meta charset="utf-8" />
56
<title>Basic test app</title>
@@ -9,6 +10,7 @@
910
<!-- Used by ExternalContentPackage -->
1011
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
1112
</head>
13+
1214
<body>
1315
<root>Loading...</root>
1416

@@ -31,4 +33,5 @@
3133
<!-- Used by ExternalContentPackage -->
3234
<script src="_content/TestContentPackage/prompt.js"></script>
3335
</body>
36+
3437
</html>

src/Components/test/testassets/BasicTestApp/wwwroot/style.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
outline: 1px solid red;
77
}
88

9+
#error-ui {
10+
display: none;
11+
}
12+
13+
#error-ui dismiss {
14+
cursor: pointer;
15+
}
16+
917
.validation-message {
1018
color: red;
1119
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
1818
</app>
1919

20+
<div id="error-ui">
21+
<environment include="Staging,Production">
22+
An error has occurred. This application may no longer respond until reloaded.
23+
</environment>
24+
<environment include="Development">
25+
An unhandled exception has occurred. See browser dev tools for details.
26+
</environment>
27+
<a href class="reload">Reload</a>
28+
<a class="dismiss">🗙</a>
29+
</div>
30+
2031
<script src="_framework/blazor.server.js"></script>
2132
</body>
2233
</html>

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/wwwroot/css/site.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ app {
111111
color: red;
112112
}
113113

114+
#error-ui {
115+
background: lightyellow;
116+
bottom: 0;
117+
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
118+
display: none;
119+
left: 0;
120+
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
121+
position: fixed;
122+
width: 100%;
123+
z-index: 1000;
124+
}
125+
126+
#error-ui .dismiss {
127+
cursor: pointer;
128+
position: absolute;
129+
right: 0.75rem;
130+
top: 0.5rem;
131+
}
132+
114133
@media (max-width: 767.98px) {
115134
.main .top-row {
116135
display: none;

src/Shared/E2ETesting/WaitAssert.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public static void Single(this IWebDriver driver, Func<IEnumerable> actualValues
4747
public static IWebElement Exists(this IWebDriver driver, By finder)
4848
=> Exists(driver, finder, default);
4949

50+
public static void DoesNotExist(this IWebDriver driver, By finder, TimeSpan timeout = default)
51+
=> WaitAssertCore(driver, () =>
52+
{
53+
var elements = driver.FindElements(finder);
54+
Assert.Empty(elements);
55+
}, timeout);
56+
5057
public static IWebElement Exists(this IWebDriver driver, By finder, TimeSpan timeout)
5158
=> WaitAssertCore(driver, () =>
5259
{

0 commit comments

Comments
 (0)