From 34f8b635cfc92e091078baecf5292337d29be4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 7 Feb 2025 16:59:02 +0100 Subject: [PATCH 01/13] Add prototype to sample app --- .../Samples/BlazorUnitedApp/App.razor | 13 +- .../Shared/ReconnectModal.razor | 22 +++ .../Shared/ReconnectModal.razor.css | 151 ++++++++++++++++++ .../Shared/ReconnectModal.razor.js | 30 ++++ .../Platform/Circuits/UserSpecifiedDisplay.ts | 21 ++- 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor create mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css create mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor index e04f7fc9e8e4..9dcfb356cd89 100644 --- a/src/Components/Samples/BlazorUnitedApp/App.razor +++ b/src/Components/Samples/BlazorUnitedApp/App.razor @@ -15,6 +15,17 @@ - + + + diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor new file mode 100644 index 000000000000..6e3c5b96f632 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor @@ -0,0 +1,22 @@ + + + +
+
+
+
+
+

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +
+
diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css new file mode 100644 index 000000000000..e2277d5f132d --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css @@ -0,0 +1,151 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] { + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; + } +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js new file mode 100644 index 000000000000..868c46e7d4e6 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js @@ -0,0 +1,30 @@ +// Set up event handlers +const retryButton = document.getElementById("components-reconnect-button"); + +retryButton.addEventListener('click', retry); + +async function retry() { + document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + location.reload(); + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === 'visible') { + await retry(); + } +} diff --git a/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts b/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts index e179d921cf3c..c99af7b9bf3c 100644 --- a/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts +++ b/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts @@ -7,6 +7,8 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { static readonly HideClassName = 'components-reconnect-hide'; + static readonly RetryingClassName = 'components-reconnect-retrying'; + static readonly FailedClassName = 'components-reconnect-failed'; static readonly RejectedClassName = 'components-reconnect-rejected'; @@ -32,6 +34,10 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { show(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName); + + if ((this.dialog as HTMLDialogElement).showModal) { + (this.dialog as HTMLDialogElement).showModal(); + } } update(currentAttempt: number, secondsToNextAttempt: number): void { @@ -46,11 +52,19 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { if (secondsToNextAttemptElement) { secondsToNextAttemptElement.innerText = secondsToNextAttempt.toString(); } + + if (currentAttempt > 1 && secondsToNextAttempt > 0) { + this.dialog.classList.add(UserSpecifiedDisplay.RetryingClassName); + } } hide(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.HideClassName); + + if ((this.dialog as HTMLDialogElement).close) { + (this.dialog as HTMLDialogElement).close(); + } } failed(): void { @@ -64,6 +78,11 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { } private removeClasses() { - this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RejectedClassName); + this.dialog.classList.remove( + UserSpecifiedDisplay.ShowClassName, + UserSpecifiedDisplay.HideClassName, + UserSpecifiedDisplay.RetryingClassName, + UserSpecifiedDisplay.FailedClassName, + UserSpecifiedDisplay.RejectedClassName); } } From 84ba9ad1943fd2d244be76938e6f55f59f074a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Wed, 12 Feb 2025 19:23:19 +0100 Subject: [PATCH 02/13] Add e2e test for the new reconnect UI --- .../ServerReconnectionCustomUITest.cs | 53 ++++++ .../Reconnection/ReconnectModal.razor | 22 +++ .../Reconnection/ReconnectModal.razor.css | 151 ++++++++++++++++++ .../Reconnection/ReconnectModal.razor.js | 30 ++++ .../Reconnection/ReconnectionComponent.razor | 13 ++ 5 files changed, 269 insertions(+) create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor create mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css create mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs new file mode 100644 index 000000000000..6529c44ec4d8 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp.Reconnection; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; + +public class ServerReconnectionCustomUITest : ServerReconnectionTest +{ + public ServerReconnectionCustomUITest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + Navigate($"{ServerPathBase}?useCustomReconnectModal=true"); + Browser.MountTestComponent(); + Browser.Exists(By.Id("count")); + } + + [Fact] + public void CustomReconnectUI() + { + Browser.Exists(By.Id("increment")).Click(); + + Browser.Equal("dialog", () => Browser.Exists(By.Id("components-reconnect-modal")).TagName); + + var javascript = (IJavaScriptExecutor)Browser; + javascript.ExecuteScript("Blazor._internal.forceCloseConnection()"); + + // We should see the 'reconnecting' UI appear + Browser.NotEqual(null, () => Browser.Exists(By.Id("components-reconnect-modal")).GetAttribute("open")); + + // Then it should disappear + Browser.Equal(null, () => Browser.Exists(By.Id("components-reconnect-modal")).GetAttribute("open")); + + Browser.Exists(By.Id("increment")).Click(); + + // Can dispatch events after reconnect + Browser.Equal("2", () => Browser.Exists(By.Id("count")).Text); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor new file mode 100644 index 000000000000..6e3c5b96f632 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor @@ -0,0 +1,22 @@ + + + +
+
+
+
+
+

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +
+
diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css new file mode 100644 index 000000000000..e2277d5f132d --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css @@ -0,0 +1,151 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] { + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; + } +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js new file mode 100644 index 000000000000..868c46e7d4e6 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js @@ -0,0 +1,30 @@ +// Set up event handlers +const retryButton = document.getElementById("components-reconnect-button"); + +retryButton.addEventListener('click', retry); + +async function retry() { + document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + location.reload(); + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === 'visible') { + await retry(); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor index ec5f659fa20b..e645145c6432 100644 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor @@ -1,4 +1,6 @@ @using System.Timers +@using System.Web +@inject NavigationManager Navigation @implements IDisposable

Reconnection Component

@@ -20,16 +22,27 @@ + +@if (useCustomReconnectModal) +{ + +} + @code { int count; void Increment() => count++; int tickCount = 0; Timer timer; bool causeError = false; + bool useCustomReconnectModal = false; protected override void OnInitialized() { timer = StartTimer(); + + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + var query = HttpUtility.ParseQueryString(uri.Query); + useCustomReconnectModal = query["useCustomReconnectModal"] == "true"; } private Timer StartTimer() From a2391d4ee1482b99f167ce54e1cd8026a1452cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 13 Feb 2025 16:42:41 +0100 Subject: [PATCH 03/13] Add E2E test for CSP violation --- .../ServerReconnectionCustomUITest.cs | 36 +++++++++++++++---- .../ServerStartupWithCsp.cs | 22 ++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs index 6529c44ec4d8..41979fb93c92 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs @@ -2,21 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. using BasicTestApp.Reconnection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.AspNetCore.Hosting; using OpenQA.Selenium; using TestServer; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; -public class ServerReconnectionCustomUITest : ServerReconnectionTest +public class ServerReconnectionCustomUITest : ServerTestBase> { public ServerReconnectionCustomUITest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture serverFixture, + BasicTestAppServerSiteFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { @@ -30,19 +32,22 @@ protected override void InitializeAsyncCore() } [Fact] - public void CustomReconnectUI() + public void CustomReconnectUIIsDisplayed() { Browser.Exists(By.Id("increment")).Click(); - Browser.Equal("dialog", () => Browser.Exists(By.Id("components-reconnect-modal")).TagName); - - var javascript = (IJavaScriptExecutor)Browser; - javascript.ExecuteScript("Blazor._internal.forceCloseConnection()"); + var js = (IJavaScriptExecutor)Browser; + js.ExecuteScript("Blazor._internal.forceCloseConnection()"); // We should see the 'reconnecting' UI appear + Browser.Equal("block", () => Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display")); Browser.NotEqual(null, () => Browser.Exists(By.Id("components-reconnect-modal")).GetAttribute("open")); + // The reconnect modal should not be a 'div' element created by the fallback JS code + Browser.Equal("dialog", () => Browser.Exists(By.Id("components-reconnect-modal")).TagName); + // Then it should disappear + Browser.Equal("none", () => Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display")); Browser.Equal(null, () => Browser.Exists(By.Id("components-reconnect-modal")).GetAttribute("open")); Browser.Exists(By.Id("increment")).Click(); @@ -50,4 +55,21 @@ public void CustomReconnectUI() // Can dispatch events after reconnect Browser.Equal("2", () => Browser.Exists(By.Id("count")).Text); } + + [Fact] + public void StyleSrcCSPIsNotViolated() + { + var js = (IJavaScriptExecutor)Browser; + js.ExecuteScript("Blazor._internal.forceCloseConnection()"); + + // We should see the 'reconnecting' UI appear + Browser.Equal("block", () => Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display")); + + // Check that there is no CSP-related error in the browser console + var logs = Browser.Manage().Logs.GetLog(LogType.Browser); + var styleErrors = logs.Where( + log => log.Message.Contains("Refused to apply inline style because it violates the following Content Security Policy directive")); + + Assert.Empty(styleErrors); + } } diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs b/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs new file mode 100644 index 000000000000..823429b30814 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace TestServer; + +public class ServerStartupWithCsp : ServerStartup +{ + public ServerStartupWithCsp(IConfiguration configuration) : base(configuration) + { + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, ResourceRequestLog resourceRequestLog) + { + app.Use(async (context, next) => + { + context.Response.Headers.ContentSecurityPolicy = "style-src 'self';"; + await next(); + }); + + base.Configure(app, env, resourceRequestLog); + } +} From ba714dfa66a83519a87436c899453df96ea683b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 13 Feb 2025 17:51:52 +0100 Subject: [PATCH 04/13] Add HTML attributes to ReconnectModal --- .../Samples/BlazorUnitedApp/Shared/ReconnectModal.razor | 4 ++-- .../testassets/BasicTestApp/Reconnection/ReconnectModal.razor | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor index 6e3c5b96f632..86e92652595f 100644 --- a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor +++ b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor @@ -1,8 +1,8 @@  - +
-
+ diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor index 6e3c5b96f632..86e92652595f 100644 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor @@ -1,8 +1,8 @@  - +
-
+ From 8840fc85bdb916518aa26aafdfa0f45a289a7ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 13 Feb 2025 17:52:07 +0100 Subject: [PATCH 05/13] Add ReconnectModal to the BlazorWeb-CSharp project template --- .../.template.config/template.json | 8 + .../Components/Layout/MainLayout.razor | 3 + .../Components/Layout/ReconnectModal.razor | 22 +++ .../Layout/ReconnectModal.razor.css | 154 ++++++++++++++++++ .../Components/Layout/ReconnectModal.razor.js | 30 ++++ 5 files changed, 217 insertions(+) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index 717a2d42f7b7..8ef0b63f24d7 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -93,6 +93,14 @@ "BlazorWeb-CSharp/Components/Pages/Counter.razor" ] }, + { + "condition": "(!UseServer)", + "exclude": [ + "BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor", + "BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css", + "BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js" + ] + }, { "condition": "(ExcludeLaunchSettings)", "exclude": [ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/MainLayout.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/MainLayout.razor index 998fa68bafd4..dff2f7ff2e66 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/MainLayout.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/MainLayout.razor @@ -28,3 +28,6 @@ 🗙
##endif*@ +@*#if (UseServer) --> + +##endif*@ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor new file mode 100644 index 000000000000..86e92652595f --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor @@ -0,0 +1,22 @@ + + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +
+
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 000000000000..873056727e60 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,154 @@ +.components-reconnect-first-attempt-s, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 000000000000..868c46e7d4e6 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,30 @@ +// Set up event handlers +const retryButton = document.getElementById("components-reconnect-button"); + +retryButton.addEventListener('click', retry); + +async function retry() { + document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + location.reload(); + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === 'visible') { + await retry(); + } +} From e96766517179b42c5951c3706aec78dfe712a29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 13 Feb 2025 18:13:17 +0100 Subject: [PATCH 06/13] Add comments, simplify test component, clean up sample app --- .../Samples/BlazorUnitedApp/App.razor | 13 +- .../Shared/ReconnectModal.razor | 22 --- .../Shared/ReconnectModal.razor.css | 151 ------------------ .../Shared/ReconnectModal.razor.js | 30 ---- .../ServerReconnectionCustomUITest.cs | 13 +- .../Reconnection/ReconnectModal.razor | 22 --- .../Reconnection/ReconnectModal.razor.css | 151 ------------------ .../Reconnection/ReconnectModal.razor.js | 30 ---- .../Reconnection/ReconnectionComponent.razor | 6 +- .../ServerStartupWithCsp.cs | 3 + .../Components/Layout/ReconnectModal.razor | 2 +- 11 files changed, 20 insertions(+), 423 deletions(-) delete mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor delete mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css delete mode 100644 src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js delete mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor delete mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css delete mode 100644 src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor index 9dcfb356cd89..e04f7fc9e8e4 100644 --- a/src/Components/Samples/BlazorUnitedApp/App.razor +++ b/src/Components/Samples/BlazorUnitedApp/App.razor @@ -15,17 +15,6 @@ - - - + diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor deleted file mode 100644 index 86e92652595f..000000000000 --- a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor +++ /dev/null @@ -1,22 +0,0 @@ - - - -
- -

- Rejoining the server... -

-

- Rejoin failed... trying again in seconds. -

-

- Failed to rejoin.
Please retry or reload the page. -

- -
-
diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css deleted file mode 100644 index e2277d5f132d..000000000000 --- a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.css +++ /dev/null @@ -1,151 +0,0 @@ -.components-reconnect-first-attempt-visible, -.components-reconnect-repeated-attempt-visible, -.components-reconnect-failed-visible, -.components-rejoining-animation { - display: none; -} - -#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, -#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-retrying, -#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, -#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-failed, -#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { - display: block; -} - - -#components-reconnect-modal { - background-color: white; - width: 20rem; - margin: 20vh auto; - padding: 2rem; - border: 0; - border-radius: 0.5rem; - box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); - opacity: 0; - transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; - animation: components-reconnect-modal-fadeOutOpacity 0.5s both; - &[open] { - animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; - animation-fill-mode: both; - } -} - -#components-reconnect-modal::backdrop { - background-color: rgba(0, 0, 0, 0.4); - animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; - opacity: 1; -} - -@keyframes components-reconnect-modal-slideUp { - 0% { - transform: translateY(30px) scale(0.95); - } - - 100% { - transform: translateY(0); - } -} - -@keyframes components-reconnect-modal-fadeInOpacity { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@keyframes components-reconnect-modal-fadeOutOpacity { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.components-reconnect-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -#components-reconnect-modal p { - margin: 0; - text-align: center; -} - -#components-reconnect-modal button { - border: 0; - background-color: #6b9ed2; - color: white; - padding: 4px 24px; - border-radius: 4px; -} - - #components-reconnect-modal button:hover { - background-color: #3b6ea2; - } - - #components-reconnect-modal button:active { - background-color: #6b9ed2; - } - -.components-rejoining-animation { - display: block; - position: relative; - width: 80px; - height: 80px; -} - - .components-rejoining-animation div { - position: absolute; - border: 3px solid #0087ff; - opacity: 1; - border-radius: 50%; - animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; - } - - .components-rejoining-animation div:nth-child(2) { - animation-delay: -0.5s; - } - -@keyframes components-rejoining-animation { - 0% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 4.9% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 5% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 1; - } - - 100% { - top: 0px; - left: 0px; - width: 80px; - height: 80px; - opacity: 0; - } -} diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js b/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js deleted file mode 100644 index 868c46e7d4e6..000000000000 --- a/src/Components/Samples/BlazorUnitedApp/Shared/ReconnectModal.razor.js +++ /dev/null @@ -1,30 +0,0 @@ -// Set up event handlers -const retryButton = document.getElementById("components-reconnect-button"); - -retryButton.addEventListener('click', retry); - -async function retry() { - document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); - - try { - // Reconnect will asynchronously return: - // - true to mean success - // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) - // - exception to mean we didn't reach the server (this can be sync or async) - const successful = await Blazor.reconnect(); - if (!successful) { - // We have been able to reach the server, but the circuit is no longer available. - // We'll reload the page so the user can continue using the app as quickly as possible. - location.reload(); - } - } catch (err) { - // We got an exception, server is currently unavailable - document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); - } -} - -async function retryWhenDocumentBecomesVisible() { - if (document.visibilityState === 'visible') { - await retry(); - } -} diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs index 41979fb93c92..622c492ef75d 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs @@ -26,11 +26,17 @@ public ServerReconnectionCustomUITest( protected override void InitializeAsyncCore() { + /// Setting this query parameter causes to include the custom reconnect dialog. Navigate($"{ServerPathBase}?useCustomReconnectModal=true"); Browser.MountTestComponent(); Browser.Exists(By.Id("count")); } + /// + /// Tests that the custom reconnect is displayed when the server circuit is disconnected. + /// This UI is provided statically by a Razor component instead being generated by the default + /// JS fallback code (see 'DefaultReconnectDisplay.ts'). + /// [Fact] public void CustomReconnectUIIsDisplayed() { @@ -56,6 +62,9 @@ public void CustomReconnectUIIsDisplayed() Browser.Equal("2", () => Browser.Exists(By.Id("count")).Text); } + /// + /// Tests that when the custom reconnect UI is used, there are no style-related CSP errors. + /// [Fact] public void StyleSrcCSPIsNotViolated() { @@ -66,9 +75,9 @@ public void StyleSrcCSPIsNotViolated() Browser.Equal("block", () => Browser.Exists(By.Id("components-reconnect-modal")).GetCssValue("display")); // Check that there is no CSP-related error in the browser console + var cspErrorMessage = "violates the following Content Security Policy directive: \"style-src"; var logs = Browser.Manage().Logs.GetLog(LogType.Browser); - var styleErrors = logs.Where( - log => log.Message.Contains("Refused to apply inline style because it violates the following Content Security Policy directive")); + var styleErrors = logs.Where(log => log.Message.Contains(cspErrorMessage)); Assert.Empty(styleErrors); } diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor deleted file mode 100644 index 86e92652595f..000000000000 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor +++ /dev/null @@ -1,22 +0,0 @@ - - - -
- -

- Rejoining the server... -

-

- Rejoin failed... trying again in seconds. -

-

- Failed to rejoin.
Please retry or reload the page. -

- -
-
diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css deleted file mode 100644 index e2277d5f132d..000000000000 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.css +++ /dev/null @@ -1,151 +0,0 @@ -.components-reconnect-first-attempt-visible, -.components-reconnect-repeated-attempt-visible, -.components-reconnect-failed-visible, -.components-rejoining-animation { - display: none; -} - -#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, -#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-retrying, -#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, -#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-failed, -#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { - display: block; -} - - -#components-reconnect-modal { - background-color: white; - width: 20rem; - margin: 20vh auto; - padding: 2rem; - border: 0; - border-radius: 0.5rem; - box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); - opacity: 0; - transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; - animation: components-reconnect-modal-fadeOutOpacity 0.5s both; - &[open] { - animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; - animation-fill-mode: both; - } -} - -#components-reconnect-modal::backdrop { - background-color: rgba(0, 0, 0, 0.4); - animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; - opacity: 1; -} - -@keyframes components-reconnect-modal-slideUp { - 0% { - transform: translateY(30px) scale(0.95); - } - - 100% { - transform: translateY(0); - } -} - -@keyframes components-reconnect-modal-fadeInOpacity { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@keyframes components-reconnect-modal-fadeOutOpacity { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.components-reconnect-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -#components-reconnect-modal p { - margin: 0; - text-align: center; -} - -#components-reconnect-modal button { - border: 0; - background-color: #6b9ed2; - color: white; - padding: 4px 24px; - border-radius: 4px; -} - - #components-reconnect-modal button:hover { - background-color: #3b6ea2; - } - - #components-reconnect-modal button:active { - background-color: #6b9ed2; - } - -.components-rejoining-animation { - display: block; - position: relative; - width: 80px; - height: 80px; -} - - .components-rejoining-animation div { - position: absolute; - border: 3px solid #0087ff; - opacity: 1; - border-radius: 50%; - animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; - } - - .components-rejoining-animation div:nth-child(2) { - animation-delay: -0.5s; - } - -@keyframes components-rejoining-animation { - 0% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 4.9% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 5% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 1; - } - - 100% { - top: 0px; - left: 0px; - width: 80px; - height: 80px; - opacity: 0; - } -} diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js deleted file mode 100644 index 868c46e7d4e6..000000000000 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectModal.razor.js +++ /dev/null @@ -1,30 +0,0 @@ -// Set up event handlers -const retryButton = document.getElementById("components-reconnect-button"); - -retryButton.addEventListener('click', retry); - -async function retry() { - document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); - - try { - // Reconnect will asynchronously return: - // - true to mean success - // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) - // - exception to mean we didn't reach the server (this can be sync or async) - const successful = await Blazor.reconnect(); - if (!successful) { - // We have been able to reach the server, but the circuit is no longer available. - // We'll reload the page so the user can continue using the app as quickly as possible. - location.reload(); - } - } catch (err) { - // We got an exception, server is currently unavailable - document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); - } -} - -async function retryWhenDocumentBecomesVisible() { - if (document.visibilityState === 'visible') { - await retry(); - } -} diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor index e645145c6432..392b5f9cee24 100644 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor @@ -25,7 +25,9 @@ @if (useCustomReconnectModal) { - + + Rejoining the server... + } @code { @@ -36,7 +38,7 @@ bool causeError = false; bool useCustomReconnectModal = false; - protected override void OnInitialized() + protected override void OnInitialized() { timer = StartTimer(); diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs b/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs index 823429b30814..4147b1a5d521 100644 --- a/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs +++ b/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs @@ -3,6 +3,9 @@ namespace TestServer; +/// +/// Implementation of intended for tests that check behavior with Content-Security-Policy headers. +/// public class ServerStartupWithCsp : ServerStartup { public ServerStartupWithCsp(IConfiguration configuration) : base(configuration) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor index 86e92652595f..1965d2c88991 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor @@ -1,4 +1,4 @@ - +
From 03a54f4c079c4528de7047d23f49f49367565ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 14 Feb 2025 11:22:03 +0100 Subject: [PATCH 07/13] Add new project template files to template-baselines.json --- .../test/Templates.Tests/template-baselines.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index c70e18b926d7..48c31072b532 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -721,6 +721,9 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", @@ -828,6 +831,9 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", @@ -946,6 +952,9 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", From 34d85ca5a2f2f0b099c59e3215ba92ae9c5e5f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 14 Feb 2025 14:46:54 +0100 Subject: [PATCH 08/13] Add missing cases to template-baselines.json --- .../test/Templates.Tests/template-baselines.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 48c31072b532..2ad8f0a3dde6 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1233,6 +1233,9 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor", + "{ProjectName}/Components/Layout/ReconnectModal.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", @@ -1353,6 +1356,9 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor", + "{ProjectName}/Components/Layout/ReconnectModal.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/Weather.razor", @@ -1427,6 +1433,9 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", From 5ec46ab72042f5dbc1f97759b59de42c3fdec320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 14 Feb 2025 15:58:52 +0100 Subject: [PATCH 09/13] Add HeadOutlet to TestServer app, use meta tag for setting CSP in reconnect test --- .../ServerReconnectionCustomUITest.cs | 8 +++--- .../Reconnection/ReconnectionComponent.razor | 7 ++++++ .../Pages/_ServerHost.cshtml | 1 + .../ServerStartupWithCsp.cs | 25 ------------------- 4 files changed, 12 insertions(+), 29 deletions(-) delete mode 100644 src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs index 622c492ef75d..b7e127ebd83a 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerReconnectionCustomUITest.cs @@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; -public class ServerReconnectionCustomUITest : ServerTestBase> +public class ServerReconnectionCustomUITest : ServerTestBase> { public ServerReconnectionCustomUITest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture serverFixture, + BasicTestAppServerSiteFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { @@ -38,7 +38,7 @@ protected override void InitializeAsyncCore() /// JS fallback code (see 'DefaultReconnectDisplay.ts'). /// [Fact] - public void CustomReconnectUIIsDisplayed() + public void ReconnectionUI_CustomDialog_IsDisplayed() { Browser.Exists(By.Id("increment")).Click(); @@ -66,7 +66,7 @@ public void CustomReconnectUIIsDisplayed() /// Tests that when the custom reconnect UI is used, there are no style-related CSP errors. /// [Fact] - public void StyleSrcCSPIsNotViolated() + public void ReconnectionUI_WorksWith_StrictStyleCspPolicy() { var js = (IJavaScriptExecutor)Browser; js.ExecuteScript("Blazor._internal.forceCloseConnection()"); diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor index 392b5f9cee24..0aabaa1d4d00 100644 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor @@ -25,6 +25,13 @@ @if (useCustomReconnectModal) { + // We set stricter CSP for styles as we want to check that the application is CSP compliant + // when using a custom reconnect modal. + // (We know that it is not compliant with the JS-created default reconnect UI.) + + + + Rejoining the server... diff --git a/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml index 13c09111b590..9de8ea472840 100644 --- a/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml +++ b/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml @@ -12,6 +12,7 @@ + diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs b/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs deleted file mode 100644 index 4147b1a5d521..000000000000 --- a/src/Components/test/testassets/Components.TestServer/ServerStartupWithCsp.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace TestServer; - -/// -/// Implementation of intended for tests that check behavior with Content-Security-Policy headers. -/// -public class ServerStartupWithCsp : ServerStartup -{ - public ServerStartupWithCsp(IConfiguration configuration) : base(configuration) - { - } - - public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, ResourceRequestLog resourceRequestLog) - { - app.Use(async (context, next) => - { - context.Response.Headers.ContentSecurityPolicy = "style-src 'self';"; - await next(); - }); - - base.Configure(app, env, resourceRequestLog); - } -} From 9fd6ca399dcecea9dcf82d89dd4a85f7191dcee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 14 Feb 2025 17:57:40 +0100 Subject: [PATCH 10/13] Dispatch reconnection state change events in UserSpecifiedDisplay --- .../Circuits/ReconnectStateChangedEvent.ts | 6 +++++ .../Platform/Circuits/UserSpecifiedDisplay.ts | 23 +++++++++++------- .../Reconnection/ReconnectionComponent.razor | 13 ++++++++++ .../Layout/ReconnectModal.razor.css | 2 +- .../Components/Layout/ReconnectModal.razor.js | 24 ++++++++++++++----- 5 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts diff --git a/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts b/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts new file mode 100644 index 000000000000..dda1bc2bad43 --- /dev/null +++ b/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts @@ -0,0 +1,6 @@ + +export interface ReconnectStateChangedEvent { + state: "show" | "hide" | "retrying" | "failed" | "rejected"; + currentAttempt?: number; + secondsToNextAttempt?: number; +} diff --git a/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts b/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts index c99af7b9bf3c..db91f49dcc90 100644 --- a/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts +++ b/src/Components/Web.JS/src/Platform/Circuits/UserSpecifiedDisplay.ts @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import { ReconnectDisplay } from './ReconnectDisplay'; +import { ReconnectStateChangedEvent } from './ReconnectStateChangedEvent'; + export class UserSpecifiedDisplay implements ReconnectDisplay { static readonly ShowClassName = 'components-reconnect-show'; @@ -19,6 +21,8 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { static readonly SecondsToNextAttemptId = 'components-seconds-to-next-attempt'; + static readonly ReconnectStateChangedEventName = 'components-reconnect-state-changed'; + constructor(private dialog: HTMLElement, private readonly document: Document, maxRetries?: number) { this.document = document; @@ -34,10 +38,7 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { show(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName); - - if ((this.dialog as HTMLDialogElement).showModal) { - (this.dialog as HTMLDialogElement).showModal(); - } + this.dispatchReconnectStateChangedEvent({ state: 'show' }); } update(currentAttempt: number, secondsToNextAttempt: number): void { @@ -56,25 +57,26 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { if (currentAttempt > 1 && secondsToNextAttempt > 0) { this.dialog.classList.add(UserSpecifiedDisplay.RetryingClassName); } + + this.dispatchReconnectStateChangedEvent({ state: 'retrying', currentAttempt, secondsToNextAttempt }); } hide(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.HideClassName); - - if ((this.dialog as HTMLDialogElement).close) { - (this.dialog as HTMLDialogElement).close(); - } + this.dispatchReconnectStateChangedEvent({ state: 'hide' }); } failed(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName); + this.dispatchReconnectStateChangedEvent({ state: 'failed' }); } rejected(): void { this.removeClasses(); this.dialog.classList.add(UserSpecifiedDisplay.RejectedClassName); + this.dispatchReconnectStateChangedEvent({ state: 'rejected' }); } private removeClasses() { @@ -85,4 +87,9 @@ export class UserSpecifiedDisplay implements ReconnectDisplay { UserSpecifiedDisplay.FailedClassName, UserSpecifiedDisplay.RejectedClassName); } + + private dispatchReconnectStateChangedEvent(eventData: ReconnectStateChangedEvent) { + const event = new CustomEvent(UserSpecifiedDisplay.ReconnectStateChangedEventName, { detail: eventData }); + this.dialog.dispatchEvent(event); + } } diff --git a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor index 0aabaa1d4d00..2a113db37bb4 100644 --- a/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/Reconnection/ReconnectionComponent.razor @@ -35,6 +35,19 @@ Rejoining the server... + + } @code { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css index 873056727e60..cb5426abe0b2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.css @@ -1,4 +1,4 @@ -.components-reconnect-first-attempt-s, +.components-reconnect-first-attempt-visible, .components-reconnect-repeated-attempt-visible, .components-reconnect-failed-visible, .components-rejoining-animation { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js index 868c46e7d4e6..c7a148cb6f26 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/ReconnectModal.razor.js @@ -1,30 +1,42 @@ // Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); -retryButton.addEventListener('click', retry); +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} async function retry() { - document.removeEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); try { // Reconnect will asynchronously return: // - true to mean success // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) - // - exception to mean we didn't reach the server (this can be sync or async) + // - exception to mean we didn"t reach the server (this can be sync or async) const successful = await Blazor.reconnect(); if (!successful) { // We have been able to reach the server, but the circuit is no longer available. - // We'll reload the page so the user can continue using the app as quickly as possible. + // We"ll reload the page so the user can continue using the app as quickly as possible. location.reload(); } } catch (err) { // We got an exception, server is currently unavailable - document.addEventListener('visibilitychange', retryWhenDocumentBecomesVisible); + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); } } async function retryWhenDocumentBecomesVisible() { - if (document.visibilityState === 'visible') { + if (document.visibilityState === "visible") { await retry(); } } From 51d1008526c462a879cc3eb07b0fb62657fda248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 14 Feb 2025 17:58:54 +0100 Subject: [PATCH 11/13] Fix code style --- .../Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts b/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts index dda1bc2bad43..89bc593057e4 100644 --- a/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts +++ b/src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts @@ -1,4 +1,3 @@ - export interface ReconnectStateChangedEvent { state: "show" | "hide" | "retrying" | "failed" | "rejected"; currentAttempt?: number; From 71d0efd5d921000b7edc89daac56b8c3a3762d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Sat, 15 Feb 2025 08:27:28 +0100 Subject: [PATCH 12/13] Add missing Add missing cases to template-baselines.json --- .../test/Templates.Tests/template-baselines.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 2ad8f0a3dde6..fc2c641e050f 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1677,6 +1677,9 @@ "Components/_Imports.razor", "Components/Layout/MainLayout.razor", "Components/Layout/MainLayout.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Properties/launchSettings.json", @@ -1722,6 +1725,9 @@ "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor", + "{ProjectName}/Components/Layout/ReconnectModal.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Properties/launchSettings.json", @@ -1787,6 +1793,9 @@ "{ProjectName}/Components/App.razor", "{ProjectName}/Components/Layout/MainLayout.razor", "{ProjectName}/Components/Layout/MainLayout.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor", + "{ProjectName}/Components/Layout/ReconnectModal.razor.css", + "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Routes.razor", @@ -1857,6 +1866,9 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Layout/ReconnectModal.razor", + "Components/Layout/ReconnectModal.razor.css", + "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", @@ -2054,6 +2066,9 @@ "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", + "{ProjectName}.Client/Layout/ReconnectModal.razor", + "{ProjectName}.Client/Layout/ReconnectModal.razor.css", + "{ProjectName}.Client/Layout/ReconnectModal.razor.js", "{ProjectName}.Client/{ProjectName}.Client.csproj", "{ProjectName}.Client/Pages/Auth.razor", "{ProjectName}.Client/Pages/Counter.razor", From 25f93fa6f182b6f2cb59869d2fe4e6bd2798d52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Sat, 15 Feb 2025 10:32:08 +0100 Subject: [PATCH 13/13] Add missing cases to template-baselines.json --- .../test/Templates.Tests/template-baselines.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index fc2c641e050f..dd5755ff3bbe 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1577,6 +1577,9 @@ "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", + "{ProjectName}.Client/Layout/ReconnectModal.razor", + "{ProjectName}.Client/Layout/ReconnectModal.razor.css", + "{ProjectName}.Client/Layout/ReconnectModal.razor.js", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/Weather.razor",