Skip to content

Commit 292e093

Browse files
committed
Changes per PR
1 parent f08a064 commit 292e093

31 files changed

+5313
-221
lines changed

src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
</Target>
3939

4040
<ItemGroup>
41-
<ProjectReference Condition="'$(BuildNodeJS)' != 'false'" Include="$(RepositoryRoot)src\Components\Browser.JS\src\Microsoft.AspNetCore.Components.Browser.JS.npmproj" ReferenceOutputAssembly="false" />
41+
<ProjectReference Condition="'$(BuildNodeJS)' != 'false'" Include="$(RepositoryRoot)src\Components\Browser.JS\Microsoft.AspNetCore.Components.Browser.JS.npmproj" ReferenceOutputAssembly="false" />
4242
<Reference Include="Microsoft.AspNetCore.Components" />
4343
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
4444
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />

src/Components/Browser.JS/src/Microsoft.AspNetCore.Components.Browser.JS.npmproj renamed to src/Components/Browser.JS/Microsoft.AspNetCore.Components.Browser.JS.npmproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.props))\Directory.Build.props" />
33

44
<PropertyGroup>
5-
<IsTestProject>false</IsTestProject>
5+
<IsTestProject>true</IsTestProject>
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
module.exports = {
5+
globals: {
6+
"ts-jest": {
7+
"tsConfig": "./tests/tsconfig.json",
8+
"babeConfig": true,
9+
"diagnostics": true
10+
}
11+
},
12+
preset: 'ts-jest',
13+
testEnvironment: 'jsdom'
14+
};

src/Components/Browser.JS/src/package.json renamed to src/Components/Browser.JS/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
"description": "",
66
"main": "index.js",
77
"scripts": {
8-
"build:debug": "webpack --mode development",
9-
"build:production": "webpack --mode production",
10-
"test": "echo \"Error: no test specified\" && exit 1"
8+
"build:debug": "cd src && webpack --mode development --config ./webpack.config.js",
9+
"build:production": "cd src && webpack --mode production --config ./webpack.config.js",
10+
"test": "jest"
1111
},
1212
"devDependencies": {
1313
"@aspnet/signalr": "^1.0.0",
1414
"@aspnet/signalr-protocol-msgpack": "^1.0.0",
1515
"@dotnet/jsinterop": "^0.1.1",
16+
"@types/jsdom": "11.0.6",
17+
"@types/jest": "^24.0.6",
1618
"@types/emscripten": "0.0.31",
19+
"jest": "^24.1.0",
20+
"ts-jest": "^24.0.0",
1721
"ts-loader": "^4.4.1",
1822
"typescript": "^2.9.2",
1923
"webpack": "^4.12.0",

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ async function boot() {
3030

3131
window['Blazor'].reconnect = async () => {
3232
const reconnection = await initializeConnection(circuitHandlers);
33-
if (!await reconnection.invoke<Boolean>('ConnectCircuit', circuitId)) {
34-
throw new Error('Failed to reconnect to the server. The supplied circuitId is invalid.');
33+
if (!(await reconnection.invoke<Boolean>('ConnectCircuit', circuitId))) {
34+
return false;
3535
}
3636

3737
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
38+
return true;
3839
};
3940

4041
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
import { CircuitHandler } from './CircuitHandler';
2+
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
3+
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
4+
import { ReconnectDisplay } from './ReconnectDisplay';
25
export class AutoReconnectCircuitHandler implements CircuitHandler {
3-
modal: HTMLDivElement;
4-
message: Text;
6+
static readonly MaxRetries = 5;
7+
static readonly RetryInterval = 3000;
8+
static readonly DialogId = 'components-reconnect-modal';
9+
reconnectDisplay: ReconnectDisplay;
510

6-
constructor(private maxRetries: number = 5, private retryInterval: number = 3000) {
7-
this.modal = document.createElement('div');
8-
this.modal.className = 'modal';
9-
this.message = document.createTextNode('');
10-
this.modal.appendChild(this.message);
11-
document.addEventListener('DOMContentLoaded', () => document.body.appendChild(this.modal));
11+
constructor() {
12+
this.reconnectDisplay = new DefaultReconnectDisplay(document);
13+
document.addEventListener('DOMContentLoaded', () => {
14+
const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
15+
if (modal) {
16+
this.reconnectDisplay = new UserSpecifiedDisplay(modal);
17+
}
18+
});
19+
}
20+
onConnectionUp() : void{
21+
this.reconnectDisplay.hide();
1222
}
13-
onConnectionUp() {
14-
this.modal.style.display = 'none';
23+
24+
delay() : Promise<void>{
25+
return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
1526
}
16-
async onConnectionDown() {
17-
this.message.textContent = 'Attempting to reconnect to the server...';
1827

19-
this.modal.style.display = 'block';
20-
const delay = () => new Promise((resolve) => setTimeout(resolve, this.retryInterval));
21-
for (let i = 0; i < this.maxRetries; i++) {
22-
await delay();
28+
async onConnectionDown() : Promise<void> {
29+
this.reconnectDisplay.show();
30+
31+
for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
32+
await this.delay();
2333
try {
24-
await window['Blazor'].reconnect();
25-
break;
34+
const result = await window['Blazor'].reconnect();
35+
if (!result) {
36+
// If the server responded and refused to reconnect, stop auto-retrying.
37+
break;
38+
}
39+
return;
2640
} catch (err) {
2741
console.error(err);
2842
}
2943
}
3044

31-
this.message.textContent = 'Failed to connect to server.';
45+
this.reconnectDisplay.failed();
3246
}
3347
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ReconnectDisplay } from "./ReconnectDisplay";
2+
import { AutoReconnectCircuitHandler } from "./AutoReconnectCircuitHandler";
3+
export class DefaultReconnectDisplay implements ReconnectDisplay {
4+
modal: HTMLDivElement;
5+
message: HTMLHeadingElement;
6+
button: HTMLButtonElement;
7+
addedToDom: boolean = false;
8+
constructor(private document: Document) {
9+
this.modal = this.document.createElement('div');
10+
this.modal.id = AutoReconnectCircuitHandler.DialogId;
11+
this.modal.style.cssText = 'position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:none;overflow:hidden;background-color:#fff;opacity:0.8;text-align:center;font-weight:bold';
12+
this.modal.innerHTML = '<h5 style="margin-top: 20px"></h5><button style="margin:5px auto 5px">Retry?</button>';
13+
this.message = this.modal.querySelector('h5')!;
14+
this.button = this.modal.querySelector('button')!;
15+
16+
this.button.addEventListener('click', () => window['Blazor'].reconnect());
17+
}
18+
show(): void {
19+
if (!this.addedToDom) {
20+
this.addedToDom = true;
21+
this.document.body.appendChild(this.modal);
22+
}
23+
this.modal.style.display = 'block';
24+
this.button.style.display = 'none';
25+
this.message.textContent = 'Attempting to reconnect to the server...';
26+
}
27+
hide(): void {
28+
this.modal.style.display = 'none';
29+
}
30+
failed(): void {
31+
this.button.style.display = 'block';
32+
this.message.textContent = 'Failed to reconnect to the server.';
33+
}
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface ReconnectDisplay {
2+
show(): void;
3+
hide(): void;
4+
failed(): void;
5+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ReconnectDisplay } from "./ReconnectDisplay";
2+
export class UserSpecifiedDisplay implements ReconnectDisplay {
3+
static readonly ShowClassName = 'components-reconnect-show';
4+
static readonly HideClassName = 'components-reconnect-hide';
5+
static readonly FailedClassName = 'components-reconnect-failed';
6+
constructor(private dialog: HTMLElement) {
7+
}
8+
show(): void {
9+
this.removeClasses();
10+
this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName);
11+
}
12+
hide(): void {
13+
this.removeClasses();
14+
this.dialog.classList.add(UserSpecifiedDisplay.HideClassName);
15+
}
16+
failed(): void {
17+
this.removeClasses();
18+
this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName);
19+
}
20+
private removeClasses() {
21+
this.dialog.classList.remove(UserSpecifiedDisplay.ShowClassName, UserSpecifiedDisplay.HideClassName, UserSpecifiedDisplay.FailedClassName);
22+
}
23+
}

src/Components/Browser.JS/src/Services/UriHelper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import '@dotnet/jsinterop';
2+
13
let hasRegisteredEventListeners = false;
24

35
// Will be initialized once someone registers
Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
{
2-
"compilerOptions": {
3-
"noImplicitAny": false,
4-
"noEmitOnError": true,
5-
"removeComments": false,
6-
"sourceMap": true,
7-
"target": "es5",
8-
"lib": ["es2015", "dom"],
9-
"strict": true
10-
},
11-
"exclude": [
12-
"node_modules",
13-
"wwwroot"
14-
]
2+
"extends": "../tsconfig.base.json",
3+
"include": [
4+
"./**/*"
5+
]
156
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
2+
import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconnectCircuitHandler";
3+
import { UserSpecifiedDisplay } from "../src/Platform/Circuits/UserSpecifiedDisplay";
4+
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
5+
import { ReconnectDisplay } from "../src/Platform/Circuits/ReconnectDisplay";
6+
import '../src/GlobalExports';
7+
8+
describe('AutoReconnectCircuitHandler', () => {
9+
it('creates default element', () => {
10+
const handler = new AutoReconnectCircuitHandler();
11+
12+
document.dispatchEvent(new Event('DOMContentLoaded'));
13+
expect(handler.reconnectDisplay).toBeInstanceOf(DefaultReconnectDisplay);
14+
});
15+
16+
it('locates user-specified handler', () => {
17+
const element = document.createElement('div');
18+
element.id = 'components-reconnect-modal';
19+
document.body.appendChild(element);
20+
const handler = new AutoReconnectCircuitHandler();
21+
22+
document.dispatchEvent(new Event('DOMContentLoaded'));
23+
expect(handler.reconnectDisplay).toBeInstanceOf(UserSpecifiedDisplay);
24+
25+
document.body.removeChild(element);
26+
});
27+
28+
const TestDisplay = jest.fn<ReconnectDisplay, any[]>(() => ({
29+
show: jest.fn(),
30+
hide: jest.fn(),
31+
failed: jest.fn()
32+
}));
33+
34+
it('hides display on connection up', () => {
35+
const handler = new AutoReconnectCircuitHandler();
36+
const testDisplay = new TestDisplay();
37+
handler.reconnectDisplay = testDisplay;
38+
39+
handler.onConnectionUp();
40+
41+
expect(testDisplay.hide).toHaveBeenCalled();
42+
43+
});
44+
45+
it('shows display on connection down', async () => {
46+
const handler = new AutoReconnectCircuitHandler();
47+
handler.delay = () => Promise.resolve();
48+
const reconnect = jest.fn().mockResolvedValue(true);
49+
window['Blazor'].reconnect = reconnect;
50+
51+
const testDisplay = new TestDisplay();
52+
handler.reconnectDisplay = testDisplay;
53+
54+
await handler.onConnectionDown();
55+
56+
expect(testDisplay.show).toHaveBeenCalled();
57+
expect(testDisplay.failed).not.toHaveBeenCalled();
58+
expect(reconnect).toHaveBeenCalledTimes(1);
59+
});
60+
61+
it('invokes failed if reconnect fails', async () => {
62+
const handler = new AutoReconnectCircuitHandler();
63+
handler.delay = () => Promise.resolve();
64+
const reconnect = jest.fn().mockRejectedValue(new Error('some error'));
65+
window.console.error = jest.fn();
66+
window['Blazor'].reconnect = reconnect;
67+
68+
const testDisplay = new TestDisplay();
69+
handler.reconnectDisplay = testDisplay;
70+
71+
await handler.onConnectionDown();
72+
73+
expect(testDisplay.show).toHaveBeenCalled();
74+
expect(testDisplay.failed).toHaveBeenCalled();
75+
expect(reconnect).toHaveBeenCalledTimes(AutoReconnectCircuitHandler.MaxRetries);
76+
});
77+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
2+
import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconnectCircuitHandler";
3+
import {JSDOM} from 'jsdom';
4+
5+
describe('DefaultReconnectDisplay', () => {
6+
7+
it ('adds element to the body on show', () => {
8+
const testDocument = new JSDOM().window.document;
9+
const display = new DefaultReconnectDisplay(testDocument);
10+
11+
display.show();
12+
13+
const element = testDocument.body.querySelector('div');
14+
expect(element).toBeDefined();
15+
expect(element!.id).toBe(AutoReconnectCircuitHandler.DialogId);
16+
expect(element!.style.display).toBe('block');
17+
18+
expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
19+
expect(display.button.style.display).toBe('none');
20+
});
21+
22+
it ('does not add element to the body multiple times', () => {
23+
const testDocument = new JSDOM().window.document;
24+
const display = new DefaultReconnectDisplay(testDocument);
25+
26+
display.show();
27+
display.show();
28+
29+
expect(testDocument.body.childElementCount).toBe(1);
30+
});
31+
32+
it ('hides element', () => {
33+
const testDocument = new JSDOM().window.document;
34+
const display = new DefaultReconnectDisplay(testDocument);
35+
36+
display.hide();
37+
38+
expect(display.modal.style.display).toBe('none');
39+
});
40+
41+
it ('updates message on fail', () => {
42+
const testDocument = new JSDOM().window.document;
43+
const display = new DefaultReconnectDisplay(testDocument);
44+
45+
display.show();
46+
display.failed();
47+
48+
expect(display.modal.style.display).toBe('block');
49+
expect(display.message.textContent).toBe('Failed to reconnect to the server.');
50+
expect(display.button.style.display).toBe('block');
51+
});
52+
53+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../tsconfig.base.json",
3+
"include": ["./**/*"],
4+
"compilerOptions": {
5+
"noUnusedLocals": false,
6+
"noUnusedParameters": false
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"noImplicitAny": false,
4+
"noEmitOnError": true,
5+
"removeComments": false,
6+
"sourceMap": true,
7+
"target": "es5",
8+
"lib": ["es2015", "dom"],
9+
"strict": true
10+
}
11+
}

0 commit comments

Comments
 (0)