Skip to content

Commit 4ae47fd

Browse files
committed
E2E tests
1 parent 6017636 commit 4ae47fd

File tree

12 files changed

+365
-26
lines changed

12 files changed

+365
-26
lines changed

src/Components/Samples/BlazorUnitedApp/App.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
<body>
1717
<Routes @rendermode="InteractiveServer" />
1818
<script src="@Assets["_framework/blazor.web.js"]"></script>
19+
<ReconnectModal />
1920
</body>
2021
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script type="module" src="@Assets["Shared/ReconnectModal.razor.js"]"></script>
2+
3+
<dialog id="components-reconnect-modal" data-nosnippet>
4+
<div class="components-reconnect-container">
5+
<div class="components-rejoining-animation" aria-hidden="true">
6+
<div></div>
7+
<div></div>
8+
</div>
9+
<p class="components-reconnect-first-attempt-visible">
10+
Rejoining the server...
11+
</p>
12+
<p class="components-reconnect-repeated-attempt-visible">
13+
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
14+
</p>
15+
<p class="components-reconnect-failed-visible">
16+
Failed to rejoin.<br />Please retry or reload the page.
17+
</p>
18+
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
19+
Retry
20+
</button>
21+
<p class="components-pause-visible">
22+
The session has been paused by the server.
23+
</p>
24+
<button id="components-resume-button" class="components-pause-visible">
25+
Resume
26+
</button>
27+
<p class="components-resume-failed-visible">
28+
Failed to resume the session.<br />Please reload the page.
29+
</p>
30+
</div>
31+
</dialog>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/* Comment */
2+
3+
.components-reconnect-first-attempt-visible,
4+
.components-reconnect-repeated-attempt-visible,
5+
.components-reconnect-failed-visible,
6+
.components-pause-visible,
7+
.components-resume-failed-visible,
8+
.components-rejoining-animation {
9+
display: none;
10+
}
11+
12+
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
13+
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
14+
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
15+
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
16+
#components-reconnect-modal.components-reconnect-retrying,
17+
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
18+
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
19+
#components-reconnect-modal.components-reconnect-failed,
20+
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
21+
display: block;
22+
}
23+
24+
25+
#components-reconnect-modal {
26+
background-color: white;
27+
width: 20rem;
28+
margin: 20vh auto;
29+
padding: 2rem;
30+
border: 0;
31+
border-radius: 0.5rem;
32+
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
33+
opacity: 0;
34+
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
35+
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
36+
&[open]
37+
38+
{
39+
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;
40+
animation-fill-mode: both;
41+
}
42+
43+
}
44+
45+
#components-reconnect-modal::backdrop {
46+
background-color: rgba(0, 0, 0, 0.4);
47+
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
48+
opacity: 1;
49+
}
50+
51+
@keyframes components-reconnect-modal-slideUp {
52+
0% {
53+
transform: translateY(30px) scale(0.95);
54+
}
55+
56+
100% {
57+
transform: translateY(0);
58+
}
59+
}
60+
61+
@keyframes components-reconnect-modal-fadeInOpacity {
62+
0% {
63+
opacity: 0;
64+
}
65+
66+
100% {
67+
opacity: 1;
68+
}
69+
}
70+
71+
@keyframes components-reconnect-modal-fadeOutOpacity {
72+
0% {
73+
opacity: 1;
74+
}
75+
76+
100% {
77+
opacity: 0;
78+
}
79+
}
80+
81+
.components-reconnect-container {
82+
display: flex;
83+
flex-direction: column;
84+
align-items: center;
85+
gap: 1rem;
86+
}
87+
88+
#components-reconnect-modal p {
89+
margin: 0;
90+
text-align: center;
91+
}
92+
93+
#components-reconnect-modal button {
94+
border: 0;
95+
background-color: #6b9ed2;
96+
color: white;
97+
padding: 4px 24px;
98+
border-radius: 4px;
99+
}
100+
101+
#components-reconnect-modal button:hover {
102+
background-color: #3b6ea2;
103+
}
104+
105+
#components-reconnect-modal button:active {
106+
background-color: #6b9ed2;
107+
}
108+
109+
.components-rejoining-animation {
110+
position: relative;
111+
width: 80px;
112+
height: 80px;
113+
}
114+
115+
.components-rejoining-animation div {
116+
position: absolute;
117+
border: 3px solid #0087ff;
118+
opacity: 1;
119+
border-radius: 50%;
120+
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
121+
}
122+
123+
.components-rejoining-animation div:nth-child(2) {
124+
animation-delay: -0.5s;
125+
}
126+
127+
@keyframes components-rejoining-animation {
128+
0% {
129+
top: 40px;
130+
left: 40px;
131+
width: 0;
132+
height: 0;
133+
opacity: 0;
134+
}
135+
136+
4.9% {
137+
top: 40px;
138+
left: 40px;
139+
width: 0;
140+
height: 0;
141+
opacity: 0;
142+
}
143+
144+
5% {
145+
top: 40px;
146+
left: 40px;
147+
width: 0;
148+
height: 0;
149+
opacity: 1;
150+
}
151+
152+
100% {
153+
top: 0px;
154+
left: 0px;
155+
width: 80px;
156+
height: 80px;
157+
opacity: 0;
158+
}
159+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Set up event handlers
2+
const reconnectModal = document.getElementById("components-reconnect-modal");
3+
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
4+
5+
const retryButton = document.getElementById("components-reconnect-button");
6+
retryButton.addEventListener("click", retry);
7+
8+
const resumeButton = document.getElementById("components-resume-button");
9+
resumeButton.addEventListener("click", resume);
10+
11+
function handleReconnectStateChanged(event) {
12+
if (event.detail.state === "show") {
13+
reconnectModal.showModal();
14+
} else if (event.detail.state === "hide") {
15+
reconnectModal.close();
16+
} else if (event.detail.state === "failed") {
17+
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
18+
} else if (event.detail.state === "rejected") {
19+
location.reload();
20+
}
21+
}
22+
23+
async function retry() {
24+
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
25+
26+
try {
27+
// Reconnect will asynchronously return:
28+
// - true to mean success
29+
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
30+
// - exception to mean we didn't reach the server (this can be sync or async)
31+
const successful = await Blazor.reconnect();
32+
if (!successful) {
33+
// We have been able to reach the server, but the circuit is no longer available.
34+
// We'll reload the page so the user can continue using the app as quickly as possible.
35+
const resumeSuccessful = await Blazor.resume();
36+
if (!resumeSuccessful) {
37+
location.reload();
38+
} else {
39+
reconnectModal.close();
40+
}
41+
}
42+
} catch (err) {
43+
// We got an exception, server is currently unavailable
44+
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
45+
}
46+
}
47+
48+
async function resume() {
49+
try {
50+
const successful = await Blazor.resume();
51+
if (!successful) {
52+
location.reload();
53+
}
54+
} catch {
55+
location.reload();
56+
}
57+
}
58+
59+
async function retryWhenDocumentBecomesVisible() {
60+
if (document.visibilityState === "visible") {
61+
await retry();
62+
}
63+
}

src/Components/Server/test/Circuits/CircuitRegistryTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Licensed to the .NET Foundation under one or more agreements.
2-
// The ..NET Foundation licenses this file to you under the MIT license.
2+
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Components.Infrastructure;
55
using Microsoft.AspNetCore.DataProtection;

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { Blazor } from '../../GlobalExports';
55
import { LogLevel, Logger } from '../Logging/Logger';
6-
import { ReconnectDisplay, ReconnectDisplayUpdateOptions } from './ReconnectDisplay';
6+
import { ReconnectDisplay, ReconnectDisplayUpdateOptions, ReconnectOptions } from './ReconnectDisplay';
77

88
export class DefaultReconnectDisplay implements ReconnectDisplay {
99
static readonly ReconnectOverlayClassName = 'components-reconnect-overlay';
@@ -89,11 +89,13 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
8989
};
9090
}
9191

92-
show(): void {
92+
show(options?: ReconnectDisplayUpdateOptions): void {
9393
if (!this.document.contains(this.host)) {
9494
this.document.body.appendChild(this.host);
9595
}
9696

97+
this.reconnect = options?.type === 'reconnect';
98+
9799
this.reloadButton.style.display = 'none';
98100
this.rejoiningAnimation.style.display = 'block';
99101
this.status.innerHTML = 'Rejoining the server...';
@@ -102,25 +104,20 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
102104
}
103105

104106
update(options: ReconnectDisplayUpdateOptions): void {
105-
if (options.type === 'reconnect') {
106-
this.reconnect = true;
107-
const { currentAttempt, secondsToNextAttempt } = options;
107+
this.reconnect = options.type === 'reconnect';
108+
if (this.reconnect) {
109+
const { currentAttempt, secondsToNextAttempt } = options as ReconnectOptions;
108110
if (currentAttempt === 1 || secondsToNextAttempt === 0) {
109111
this.status.innerHTML = 'Rejoining the server...';
110112
} else {
111113
const unitText = secondsToNextAttempt === 1 ? 'second' : 'seconds';
112114
this.status.innerHTML = `Rejoin failed... trying again in ${secondsToNextAttempt} ${unitText}`;
113115
}
114-
}
115-
if (options.type === 'pause') {
116-
this.reconnect = false;
117-
this.remote = options.remote;
116+
} else {
118117
this.reloadButton.style.display = 'none';
119-
if (options.remote) {
120-
this.rejoiningAnimation.style.display = 'none';
121-
this.status.innerHTML = 'The session has been paused by the server.';
122-
this.resumeButton.style.display = 'block';
123-
}
118+
this.rejoiningAnimation.style.display = 'none';
119+
this.status.innerHTML = 'The session has been paused by the server.';
120+
this.resumeButton.style.display = 'block';
124121
}
125122
}
126123

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectionHandler.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
import { ReconnectionHandler, ReconnectionOptions } from './CircuitStartOptions';
5-
import { ReconnectDisplay } from './ReconnectDisplay';
5+
import { ReconnectDisplay, ReconnectDisplayUpdateOptions } from './ReconnectDisplay';
66
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
77
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
88
import { Logger, LogLevel } from '../Logging/Logger';
@@ -68,12 +68,18 @@ class ReconnectionProcess {
6868
private reconnectCallback: () => Promise<boolean>,
6969
private resumeCallback: () => Promise<boolean>,
7070
display: ReconnectDisplay,
71-
private isClientPause?: boolean,
71+
private isGracefulPause?: boolean,
7272
private isRemote: boolean = false,
7373
) {
7474
this.reconnectDisplay = display;
75-
this.reconnectDisplay.show();
76-
if (!this.isClientPause) {
75+
const displayOptions: ReconnectDisplayUpdateOptions = {
76+
type: isGracefulPause ? 'pause' : 'reconnect',
77+
remote: this.isRemote,
78+
currentAttempt: 0,
79+
secondsToNextAttempt: 0,
80+
};
81+
this.reconnectDisplay.show(displayOptions);
82+
if (!this.isGracefulPause) {
7783
this.attemptPeriodicReconnection(options);
7884
} else {
7985
this.reconnectDisplay.update({

src/Components/Web.JS/src/Platform/Circuits/ReconnectDisplay.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
export interface ReconnectDisplay {
5-
show(): void;
5+
show(options: ReconnectDisplayUpdateOptions): void;
66
update(options: ReconnectDisplayUpdateOptions): void;
77
hide(): void;
88
failed(): void;
@@ -11,12 +11,12 @@ export interface ReconnectDisplay {
1111

1212
export type ReconnectDisplayUpdateOptions = ReconnectOptions | PauseOptions;
1313

14-
type PauseOptions = {
14+
export type PauseOptions = {
1515
type: 'pause',
1616
remote: boolean
1717
};
1818

19-
type ReconnectOptions = {
19+
export type ReconnectOptions = {
2020
type: 'reconnect',
2121
currentAttempt: number,
2222
secondsToNextAttempt: number

src/Components/Web.JS/src/Platform/Circuits/ReconnectStateChangedEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface ReconnectStateChangedEvent {
2-
state: "show" | "hide" | "retrying" | "failed" | "paused" | "rejected";
2+
state: "show" | "hide" | "retrying" | "failed" | "resume-failed" | "paused" | "rejected";
33
currentAttempt?: number;
44
secondsToNextAttempt?: number;
55
remote?: boolean;

0 commit comments

Comments
 (0)