Skip to content

Commit 1078811

Browse files
authored
Merge pull request #25084 from dotnet-maestro-bot/merge/release/5.0-to-master
[automated] Merge branch 'release/5.0' => 'master'
2 parents 4ecde5f + 63cad45 commit 1078811

File tree

57 files changed

+4439
-858
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4439
-858
lines changed

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/src/Platform/Circuits/CircuitStartOptions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ const defaultOptions: CircuitStartOptions = {
3333
configureSignalR: (_) => { },
3434
logLevel: LogLevel.Warning,
3535
reconnectionOptions: {
36-
maxRetries: 5,
37-
retryIntervalMilliseconds: 3000,
36+
maxRetries: 8,
37+
retryIntervalMilliseconds: 20000,
3838
dialogId: 'components-reconnect-modal',
3939
},
4040
};

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
1212

1313
reloadParagraph: HTMLParagraphElement;
1414

15-
constructor(dialogId: string, private readonly document: Document, private readonly logger: Logger) {
15+
loader: HTMLDivElement;
16+
17+
constructor(dialogId: string, private readonly maxRetries: number, private readonly document: Document, private readonly logger: Logger) {
1618
this.modal = this.document.createElement('div');
1719
this.modal.id = dialogId;
20+
this.maxRetries = maxRetries;
1821

1922
const modalStyles = [
2023
'position: fixed',
@@ -37,6 +40,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
3740
this.message = this.modal.querySelector('h5')!;
3841
this.button = this.modal.querySelector('button')!;
3942
this.reloadParagraph = this.modal.querySelector('p')!;
43+
this.loader = this.getLoader();
44+
45+
this.message.after(this.loader);
4046

4147
this.button.addEventListener('click', async () => {
4248
this.show();
@@ -65,6 +71,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
6571
this.document.body.appendChild(this.modal);
6672
}
6773
this.modal.style.display = 'block';
74+
this.loader.style.display = 'inline-block';
6875
this.button.style.display = 'none';
6976
this.reloadParagraph.style.display = 'none';
7077
this.message.textContent = 'Attempting to reconnect to the server...';
@@ -78,21 +85,51 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
7885
}, 0);
7986
}
8087

88+
update(currentAttempt: number): void {
89+
this.message.textContent = `Attempting to reconnect to the server: ${currentAttempt} of ${this.maxRetries}`;
90+
}
91+
8192
hide(): void {
8293
this.modal.style.display = 'none';
8394
}
8495

8596
failed(): void {
8697
this.button.style.display = 'block';
8798
this.reloadParagraph.style.display = 'none';
99+
this.loader.style.display = 'none';
88100
this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
89101
this.message.querySelector('a')!.addEventListener('click', () => location.reload());
90102
}
91103

92104
rejected(): void {
93105
this.button.style.display = 'none';
94106
this.reloadParagraph.style.display = 'none';
107+
this.loader.style.display = 'none';
95108
this.message.innerHTML = 'Could not reconnect to the server. <a href>Reload</a> the page to restore functionality.';
96109
this.message.querySelector('a')!.addEventListener('click', () => location.reload());
97110
}
111+
112+
private getLoader(): HTMLDivElement {
113+
const loader = this.document.createElement('div');
114+
115+
const loaderStyles = [
116+
'border: 0.3em solid #f3f3f3',
117+
'border-top: 0.3em solid #3498db',
118+
'border-radius: 50%',
119+
'width: 2em',
120+
'height: 2em',
121+
'display: inline-block'
122+
];
123+
124+
loader.style.cssText = loaderStyles.join(';');
125+
loader.animate([
126+
{ transform: 'rotate(0deg)' },
127+
{ transform: 'rotate(360deg)' }
128+
], {
129+
duration: 2000,
130+
iterations: Infinity
131+
});
132+
133+
return loader;
134+
}
98135
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
2020
if (!this._reconnectionDisplay) {
2121
const modal = document.getElementById(options.dialogId);
2222
this._reconnectionDisplay = modal
23-
? new UserSpecifiedDisplay(modal)
24-
: new DefaultReconnectDisplay(options.dialogId, document, this._logger);
23+
? new UserSpecifiedDisplay(modal, options.maxRetries, document)
24+
: new DefaultReconnectDisplay(options.dialogId, options.maxRetries, document, this._logger);
2525
}
2626

2727
if (!this._currentReconnectionProcess) {
@@ -38,6 +38,8 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
3838
};
3939

4040
class ReconnectionProcess {
41+
static readonly MaximumFirstRetryInterval = 3000;
42+
4143
readonly reconnectDisplay: ReconnectDisplay;
4244
isDisposed = false;
4345

@@ -54,7 +56,13 @@ class ReconnectionProcess {
5456

5557
async attemptPeriodicReconnection(options: ReconnectionOptions) {
5658
for (let i = 0; i < options.maxRetries; i++) {
57-
await this.delay(options.retryIntervalMilliseconds);
59+
this.reconnectDisplay.update(i + 1);
60+
61+
const delayDuration = i == 0 && options.retryIntervalMilliseconds > ReconnectionProcess.MaximumFirstRetryInterval
62+
? ReconnectionProcess.MaximumFirstRetryInterval
63+
: options.retryIntervalMilliseconds;
64+
await this.delay(delayDuration);
65+
5866
if (this.isDisposed) {
5967
break;
6068
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface ReconnectDisplay {
22
show(): void;
3+
update(currentAttempt: number): void;
34
hide(): void;
45
failed(): void;
56
rejected(): void;

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,33 @@ export class UserSpecifiedDisplay implements ReconnectDisplay {
88

99
static readonly RejectedClassName = 'components-reconnect-rejected';
1010

11-
constructor(private dialog: HTMLElement) {
11+
static readonly MaxRetriesId = 'components-reconnect-max-retries';
12+
13+
static readonly CurrentAttemptId = 'components-reconnect-current-attempt';
14+
15+
constructor(private dialog: HTMLElement, private readonly maxRetries: number, private readonly document: Document) {
16+
this.document = document;
17+
18+
const maxRetriesElement = this.document.getElementById(UserSpecifiedDisplay.MaxRetriesId);
19+
20+
if (maxRetriesElement) {
21+
maxRetriesElement.innerText = this.maxRetries.toString();
22+
}
1223
}
1324

1425
show(): void {
1526
this.removeClasses();
1627
this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName);
1728
}
1829

30+
update(currentAttempt: number): void {
31+
const currentAttemptElement = this.document.getElementById(UserSpecifiedDisplay.CurrentAttemptId);
32+
33+
if (currentAttemptElement) {
34+
currentAttemptElement.innerText = currentAttempt.toString();
35+
}
36+
}
37+
1938
hide(): void {
2039
this.removeClasses();
2140
this.dialog.classList.add(UserSpecifiedDisplay.HideClassName);

src/Components/Web.JS/tests/DefaultReconnectDisplay.test.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ import { JSDOM } from 'jsdom';
33
import { NullLogger } from '../src/Platform/Logging/Loggers';
44

55
describe('DefaultReconnectDisplay', () => {
6+
let testDocument: Document;
7+
8+
beforeEach(() => {
9+
const window = new JSDOM().window;
10+
11+
//JSDOM does not support animate function so we need to mock it
12+
window.HTMLDivElement.prototype.animate = jest.fn();
13+
testDocument = window.document;
14+
})
615

716
it ('adds element to the body on show', () => {
8-
const testDocument = new JSDOM().window.document;
9-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
17+
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
1018

1119
display.show();
1220

@@ -16,6 +24,7 @@ describe('DefaultReconnectDisplay', () => {
1624
expect(element!.style.display).toBe('block');
1725
expect(element!.style.visibility).toBe('hidden');
1826

27+
expect(display.loader.style.display).toBe('inline-block');
1928
expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
2029
expect(display.button.style.display).toBe('none');
2130

@@ -27,8 +36,7 @@ describe('DefaultReconnectDisplay', () => {
2736
});
2837

2938
it ('does not add element to the body multiple times', () => {
30-
const testDocument = new JSDOM().window.document;
31-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
39+
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
3240

3341
display.show();
3442
display.show();
@@ -37,36 +45,46 @@ describe('DefaultReconnectDisplay', () => {
3745
});
3846

3947
it ('hides element', () => {
40-
const testDocument = new JSDOM().window.document;
41-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
48+
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
4249

4350
display.hide();
4451

4552
expect(display.modal.style.display).toBe('none');
4653
});
4754

4855
it ('updates message on fail', () => {
49-
const testDocument = new JSDOM().window.document;
50-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
56+
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
5157

5258
display.show();
5359
display.failed();
5460

5561
expect(display.modal.style.display).toBe('block');
5662
expect(display.message.innerHTML).toBe('Reconnection failed. Try <a href=\"\">reloading</a> the page if you\'re unable to reconnect.');
5763
expect(display.button.style.display).toBe('block');
64+
expect(display.loader.style.display).toBe('none');
5865
});
5966

6067
it ('updates message on refused', () => {
61-
const testDocument = new JSDOM().window.document;
62-
const display = new DefaultReconnectDisplay('test-dialog-id', testDocument, NullLogger.instance);
68+
const display = new DefaultReconnectDisplay('test-dialog-id', 6, testDocument, NullLogger.instance);
6369

6470
display.show();
6571
display.rejected();
6672

6773
expect(display.modal.style.display).toBe('block');
6874
expect(display.message.innerHTML).toBe('Could not reconnect to the server. <a href=\"\">Reload</a> the page to restore functionality.');
6975
expect(display.button.style.display).toBe('none');
76+
expect(display.loader.style.display).toBe('none');
7077
});
7178

79+
it('update message with current attempt', () => {
80+
const maxRetires = 6;
81+
const display = new DefaultReconnectDisplay('test-dialog-id', maxRetires, testDocument, NullLogger.instance);
82+
83+
display.show();
84+
85+
for (let index = 0; index < maxRetires; index++) {
86+
display.update(index);
87+
expect(display.message.innerHTML).toBe(`Attempting to reconnect to the server: ${index++} of ${maxRetires}`);
88+
}
89+
})
7290
});

src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ describe('DefaultReconnectionHandler', () => {
7575
expect(testDisplay.failed).toHaveBeenCalled();
7676
expect(reconnect).toHaveBeenCalledTimes(2);
7777
});
78+
79+
it('invokes update on each attempt', async () => {
80+
const testDisplay = createTestDisplay();
81+
const reconnect = jest.fn().mockRejectedValue(null);
82+
const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
83+
const maxRetries = 6;
84+
85+
handler.onConnectionDown({
86+
maxRetries: maxRetries,
87+
retryIntervalMilliseconds: 5,
88+
dialogId: 'ignored'
89+
});
90+
91+
await delay(500);
92+
expect(testDisplay.update).toHaveBeenCalledTimes(maxRetries);
93+
94+
})
7895
});
7996

8097
function attachUserSpecifiedUI(options: ReconnectionOptions): Element {
@@ -92,6 +109,7 @@ function delay(durationMilliseconds: number) {
92109
function createTestDisplay(): ReconnectDisplay {
93110
return {
94111
show: jest.fn(),
112+
update: jest.fn(),
95113
hide: jest.fn(),
96114
failed: jest.fn(),
97115
rejected: jest.fn()

src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
<Compile Include="..\src\BootJsonData.cs" LinkBase="Wasm" />
3737
<Compile Include="..\src\AssetsManifestFile.cs" LinkBase="Wasm" />
3838
<Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
39+
40+
<EmbeddedResource Include="..\src\targets\BlazorWasm.web.config" />
3941
</ItemGroup>
4042

4143
<Target Name="GenerateTestData" BeforeTargets="GetAssemblyAttributes">

src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public async Task Publish_MinimalApp_Works()
4141

4242
// Verify web.config
4343
Assert.FileExists(result, publishDirectory, "web.config");
44+
var webConfigContent = new StreamReader(GetType().Assembly.GetManifestResourceStream("Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.BlazorWasm.web.config")).ReadToEnd();
45+
Assert.FileContentEquals(result, Path.Combine(publishDirectory, "web.config"), webConfigContent);
4446
Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly);
4547

4648
VerifyBootManifestHashes(result, blazorPublishDirectory);

0 commit comments

Comments
 (0)