Просмотр исходного кода

API review: Web.JS. Fixes #12229 (#12361)

* Some initial tidying on Boot.Server.ts, though can't make much difference until stateful prerendering is removed

* In Web.JS, rename ILogger to Logger to match TypeScript conventions

* Move reconnection options into BlazorOptions

* In Web.JS, eliminate collection of CircuitHandlers and just have one ReconnectionHandler

* Expose Blazor.defaultReconnectionHandler

* Update binaries
Steve Sanderson 6 лет назад
Родитель
Сommit
e451597a0a

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webassembly.js


+ 18 - 37
src/Components/Web.JS/src/Boot.Server.ts

@@ -3,52 +3,33 @@ import './GlobalExports';
 import * as signalR from '@aspnet/signalr';
 import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
 import { shouldAutoStart } from './BootCommon';
-import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
-import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
-import RenderQueue from './Platform/Circuits/RenderQueue';
+import { RenderQueue } from './Platform/Circuits/RenderQueue';
 import { ConsoleLogger } from './Platform/Logging/Loggers';
-import { LogLevel, ILogger } from './Platform/Logging/ILogger';
+import { LogLevel, Logger } from './Platform/Logging/Logger';
 import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
 import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
-
-
-type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
-interface BlazorOptions {
-  configureSignalR: SignalRBuilder;
-  logLevel: LogLevel;
-}
+import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
+import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
 
 let renderingFailed = false;
 let started = false;
 
 async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
-
   if (started) {
     throw new Error('Blazor has already started.');
   }
   started = true;
 
-  const defaultOptions: BlazorOptions = {
-    configureSignalR: (_) => { },
-    logLevel: LogLevel.Warning,
-  };
-
-  const options: BlazorOptions = { ...defaultOptions, ...userOptions };
-
-  // For development.
-  // Simply put a break point here and modify the log level during
-  // development to get traces.
-  // In the future we will allow for users to configure this.
+  // Establish options to be used
+  const options = resolveOptions(userOptions);
   const logger = new ConsoleLogger(options.logLevel);
-
+  window['Blazor'].defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
+  options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
   logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
 
-  const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
-  window['Blazor'].circuitHandlers = circuitHandlers;
-
-  // pass options.configureSignalR to configure the signalR.HubConnectionBuilder
-  const initialConnection = await initializeConnection(options, circuitHandlers, logger);
-
+  // Initialize statefully prerendered circuits and their components
+  // Note: This will all be removed soon
+  const initialConnection = await initializeConnection(options, logger);
   const circuits = discoverPrerenderedCircuits(document);
   for (let i = 0; i < circuits.length; i++) {
     const circuit = circuits[i];
@@ -59,7 +40,6 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
   }
 
   const circuit = await startCircuit(initialConnection);
-
   if (!circuit) {
     logger.log(LogLevel.Information, 'No preregistered components to render.');
   }
@@ -69,14 +49,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
       // We can't reconnect after a failure, so exit early.
       return false;
     }
-    const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, logger);
+    const reconnection = existingConnection || await initializeConnection(options, logger);
     const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
 
     if (reconnectionFailed(results)) {
       return false;
     }
 
-    circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
+    options.reconnectionHandler!.onConnectionUp();
+
     return true;
   };
 
@@ -97,8 +78,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
   }
 }
 
-async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
-
+async function initializeConnection(options: BlazorOptions, logger: Logger): Promise<signalR.HubConnection> {
   const hubProtocol = new MessagePackHubProtocol();
   (hubProtocol as unknown as { name: string }).name = 'blazorpack';
 
@@ -124,7 +104,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
     queue.processBatch(batchId, batchData, connection);
   });
 
-  connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
+  connection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
   connection.on('JS.Error', error => unhandledError(connection, error, logger));
 
   window['Blazor']._internal.forceCloseConnection = () => connection.stop();
@@ -147,7 +127,7 @@ async function initializeConnection(options: Required<BlazorOptions>, circuitHan
   return connection;
 }
 
-function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
+function unhandledError(connection: signalR.HubConnection, err: Error, logger: Logger): void {
   logger.log(LogLevel.Error, err);
 
   // Disconnect on errors.
@@ -160,6 +140,7 @@ function unhandledError(connection: signalR.HubConnection, err: Error, logger: I
 }
 
 window['Blazor'].start = boot;
+
 if (shouldAutoStart()) {
   boot();
 }

+ 3 - 3
src/Components/Web.JS/src/BootCommon.ts

@@ -40,7 +40,7 @@ interface BootJsonData {
 
 // Tells you if the script was added without <script src="..." autostart="false"></script>
 export function shouldAutoStart() {
-  return document &&
+  return !!(document &&
     document.currentScript &&
-    document.currentScript.getAttribute('autostart') !== 'false';
-}
+    document.currentScript.getAttribute('autostart') !== 'false');
+}

+ 0 - 55
src/Components/Web.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts

@@ -1,55 +0,0 @@
-import { CircuitHandler } from './CircuitHandler';
-import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
-import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
-import { ReconnectDisplay } from './ReconnectDisplay';
-import { ILogger, LogLevel } from '../Logging/ILogger';
-export class AutoReconnectCircuitHandler implements CircuitHandler {
-  public static readonly MaxRetries = 5;
-
-  public static readonly RetryInterval = 3000;
-
-  public static readonly DialogId = 'components-reconnect-modal';
-
-  public reconnectDisplay: ReconnectDisplay;
-
-  public logger: ILogger;
-
-  public constructor(logger: ILogger) {
-    this.logger = logger;
-    this.reconnectDisplay = new DefaultReconnectDisplay(document);
-    document.addEventListener('DOMContentLoaded', () => {
-      const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
-      if (modal) {
-        this.reconnectDisplay = new UserSpecifiedDisplay(modal);
-      }
-    });
-  }
-
-  public onConnectionUp(): void {
-    this.reconnectDisplay.hide();
-  }
-
-  public delay(): Promise<void> {
-    return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
-  }
-
-  public async onConnectionDown(): Promise<void> {
-    this.reconnectDisplay.show();
-
-    for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
-      await this.delay();
-      try {
-        const result = await window['Blazor'].reconnect();
-        if (!result) {
-          // If the server responded and refused to reconnect, stop auto-retrying.
-          break;
-        }
-        return;
-      } catch (err) {
-        this.logger.log(LogLevel.Error, err);
-      }
-    }
-
-    this.reconnectDisplay.failed();
-  }
-}

+ 40 - 0
src/Components/Web.JS/src/Platform/Circuits/BlazorOptions.ts

@@ -0,0 +1,40 @@
+import { LogLevel } from '../Logging/Logger';
+
+export interface BlazorOptions {
+  configureSignalR: (builder: signalR.HubConnectionBuilder) => void;
+  logLevel: LogLevel;
+  reconnectionOptions: ReconnectionOptions;
+  reconnectionHandler?: ReconnectionHandler;
+}
+
+export function resolveOptions(userOptions?: Partial<BlazorOptions>): BlazorOptions {
+    const result = { ...defaultOptions, ...userOptions };
+
+    // The spread operator can't be used for a deep merge, so do the same for subproperties
+    if (userOptions && userOptions.reconnectionOptions) {
+      result.reconnectionOptions = { ...defaultOptions.reconnectionOptions, ...userOptions.reconnectionOptions };
+    }
+
+    return result;
+}
+
+export interface ReconnectionOptions {
+  maxRetries: number;
+  retryIntervalMilliseconds: number;
+  dialogId: string;
+}
+
+export interface ReconnectionHandler {
+  onConnectionDown(options: ReconnectionOptions, error?: Error): void;
+  onConnectionUp(): void;
+}
+
+const defaultOptions: BlazorOptions = {
+    configureSignalR: (_) => { },
+    logLevel: LogLevel.Warning,
+    reconnectionOptions: {
+      maxRetries: 5,
+      retryIntervalMilliseconds: 3000,
+      dialogId: 'components-reconnect-modal',
+    },
+};

+ 0 - 10
src/Components/Web.JS/src/Platform/Circuits/CircuitHandler.ts

@@ -1,10 +0,0 @@
-export interface CircuitHandler {
-  /** Invoked when a server connection is established or re-established after a connection failure.
-     */
-  onConnectionUp?(): void;
-
-  /** Invoked when a server connection is dropped.
-     * @param {Error} error Optionally argument containing the error that caused the connection to close (if any).
-     */
-  onConnectionDown?(error?: Error): void;
-}

+ 3 - 3
src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts

@@ -1,5 +1,5 @@
 import { ReconnectDisplay } from './ReconnectDisplay';
-import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
+
 export class DefaultReconnectDisplay implements ReconnectDisplay {
   modal: HTMLDivElement;
 
@@ -9,9 +9,9 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
 
   addedToDom: boolean = false;
 
-  constructor(private document: Document) {
+  constructor(dialogId: string, private document: Document) {
     this.modal = this.document.createElement('div');
-    this.modal.id = AutoReconnectCircuitHandler.DialogId;
+    this.modal.id = dialogId;
 
     const modalStyles = [
       'position: fixed',

+ 82 - 0
src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectionHandler.ts

@@ -0,0 +1,82 @@
+import { ReconnectionHandler, ReconnectionOptions } from './BlazorOptions';
+import { ReconnectDisplay } from './ReconnectDisplay';
+import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
+import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
+import { Logger, LogLevel } from '../Logging/Logger';
+
+export class DefaultReconnectionHandler implements ReconnectionHandler {
+  private readonly _logger: Logger;
+  private readonly _overrideDisplay?: ReconnectDisplay;
+  private readonly _reconnectCallback: () => Promise<boolean>;
+  private _currentReconnectionProcess: ReconnectionProcess | null = null;
+
+  constructor(logger: Logger, overrideDisplay?: ReconnectDisplay, reconnectCallback?: () => Promise<boolean>) {
+    this._logger = logger;
+    this._overrideDisplay = overrideDisplay;
+    this._reconnectCallback = reconnectCallback || (() => window['Blazor'].reconnect());
+  }
+
+  onConnectionDown (options: ReconnectionOptions, error?: Error) {
+    if (!this._currentReconnectionProcess) {
+      this._currentReconnectionProcess = new ReconnectionProcess(options, this._logger, this._reconnectCallback, this._overrideDisplay);
+    }
+  }
+
+  onConnectionUp() {
+    if (this._currentReconnectionProcess) {
+      this._currentReconnectionProcess.dispose();
+      this._currentReconnectionProcess = null;
+    }
+  }
+};
+
+class ReconnectionProcess {
+  readonly reconnectDisplay: ReconnectDisplay;
+  isDisposed = false;
+
+  constructor(options: ReconnectionOptions, private logger: Logger, private reconnectCallback: () => Promise<boolean>, display?: ReconnectDisplay) {
+    const modal = document.getElementById(options.dialogId);
+    this.reconnectDisplay = display || (modal
+        ? new UserSpecifiedDisplay(modal)
+        : new DefaultReconnectDisplay(options.dialogId, document));
+
+    this.reconnectDisplay.show();
+    this.attemptPeriodicReconnection(options);
+  }
+
+  public dispose() {
+    this.isDisposed = true;
+    this.reconnectDisplay.hide();
+  }
+
+  async attemptPeriodicReconnection(options: ReconnectionOptions) {
+    for (let i = 0; i < options.maxRetries; i++) {
+      await this.delay(options.retryIntervalMilliseconds);
+      if (this.isDisposed) {
+        break;
+      }
+
+      try {
+        // reconnectCallback 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 result = await this.reconnectCallback();
+        if (!result) {
+          // If the server responded and refused to reconnect, stop auto-retrying.
+          break;
+        }
+        return;
+      } catch (err) {
+        // We got an exception so will try again momentarily
+        this.logger.log(LogLevel.Error, err);
+      }
+    }
+
+    this.reconnectDisplay.failed();
+  }
+
+  delay(durationMilliseconds: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
+  }
+}

+ 5 - 5
src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts

@@ -1,23 +1,23 @@
 import { renderBatch } from '../../Rendering/Renderer';
 import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
-import { ILogger, LogLevel } from '../Logging/ILogger';
+import { Logger, LogLevel } from '../Logging/Logger';
 import { HubConnection } from '@aspnet/signalr';
 
-export default class RenderQueue {
+export class RenderQueue {
   private static renderQueues = new Map<number, RenderQueue>();
 
   private nextBatchId = 2;
 
   public browserRendererId: number;
 
-  public logger: ILogger;
+  public logger: Logger;
 
-  public constructor(browserRendererId: number, logger: ILogger) {
+  public constructor(browserRendererId: number, logger: Logger) {
     this.browserRendererId = browserRendererId;
     this.logger = logger;
   }
 
-  public static getOrCreateQueue(browserRendererId: number, logger: ILogger): RenderQueue {
+  public static getOrCreateQueue(browserRendererId: number, logger: Logger): RenderQueue {
     const queue = this.renderQueues.get(browserRendererId);
     if (queue) {
       return queue;

+ 1 - 1
src/Components/Web.JS/src/Platform/Logging/ILogger.ts → src/Components/Web.JS/src/Platform/Logging/Logger.ts

@@ -24,7 +24,7 @@ export enum LogLevel {
 }
 
 /** An abstraction that provides a sink for diagnostic messages. */
-export interface ILogger { // eslint-disable-line @typescript-eslint/interface-name-prefix
+export interface Logger { // eslint-disable-line @typescript-eslint/interface-name-prefix
   /** Called by the framework to emit a diagnostic message.
    *
    * @param {LogLevel} logLevel The severity level of the message.

+ 4 - 4
src/Components/Web.JS/src/Platform/Logging/Loggers.ts

@@ -1,9 +1,9 @@
 /* eslint-disable no-console */
 
-import { ILogger, LogLevel } from './ILogger';
+import { Logger, LogLevel } from './Logger';
 
-export class NullLogger implements ILogger {
-  public static instance: ILogger = new NullLogger();
+export class NullLogger implements Logger {
+  public static instance: Logger = new NullLogger();
 
   private constructor() { }
 
@@ -11,7 +11,7 @@ export class NullLogger implements ILogger {
   }
 }
 
-export class ConsoleLogger implements ILogger {
+export class ConsoleLogger implements Logger {
   private readonly minimumLogLevel: LogLevel;
 
   public constructor(minimumLogLevel: LogLevel) {

+ 0 - 78
src/Components/Web.JS/tests/AutoReconnectCircuitHandler.test.ts

@@ -1,78 +0,0 @@
-
-import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconnectCircuitHandler";
-import { UserSpecifiedDisplay } from "../src/Platform/Circuits/UserSpecifiedDisplay";
-import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
-import { ReconnectDisplay } from "../src/Platform/Circuits/ReconnectDisplay";
-import { NullLogger} from '../src/Platform/Logging/Loggers';
-import '../src/GlobalExports';
-
-describe('AutoReconnectCircuitHandler', () => {
-    it('creates default element', () => {
-        const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
-
-        document.dispatchEvent(new Event('DOMContentLoaded'));
-        expect(handler.reconnectDisplay).toBeInstanceOf(DefaultReconnectDisplay);
-    });
-
-    it('locates user-specified handler', () => {
-        const element = document.createElement('div');
-        element.id = 'components-reconnect-modal';
-        document.body.appendChild(element);
-        const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
-
-        document.dispatchEvent(new Event('DOMContentLoaded'));
-        expect(handler.reconnectDisplay).toBeInstanceOf(UserSpecifiedDisplay);
-
-        document.body.removeChild(element);
-    });
-
-    const TestDisplay = jest.fn<ReconnectDisplay, any[]>(() => ({
-        show: jest.fn(),
-        hide: jest.fn(),
-        failed: jest.fn()
-    }));
-
-    it('hides display on connection up', () => {
-        const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
-        const testDisplay = new TestDisplay();
-        handler.reconnectDisplay = testDisplay;
-
-        handler.onConnectionUp();
-
-        expect(testDisplay.hide).toHaveBeenCalled();
-
-    });
-
-    it('shows display on connection down', async () => {
-        const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
-        handler.delay = () => Promise.resolve();
-        const reconnect = jest.fn().mockResolvedValue(true);
-        window['Blazor'].reconnect = reconnect;
-
-        const testDisplay = new TestDisplay();
-        handler.reconnectDisplay = testDisplay;
-
-        await handler.onConnectionDown();
-
-        expect(testDisplay.show).toHaveBeenCalled();
-        expect(testDisplay.failed).not.toHaveBeenCalled();
-        expect(reconnect).toHaveBeenCalledTimes(1);
-    });
-
-    it('invokes failed if reconnect fails', async () => {
-        const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
-        handler.delay = () => Promise.resolve();
-        const reconnect = jest.fn().mockRejectedValue(new Error('some error'));
-        window.console.error = jest.fn();
-        window['Blazor'].reconnect = reconnect;
-
-        const testDisplay = new TestDisplay();
-        handler.reconnectDisplay = testDisplay;
-
-        await handler.onConnectionDown();
-
-        expect(testDisplay.show).toHaveBeenCalled();
-        expect(testDisplay.failed).toHaveBeenCalled();
-        expect(reconnect).toHaveBeenCalledTimes(AutoReconnectCircuitHandler.MaxRetries);
-    });
-});

+ 5 - 5
src/Components/Web.JS/tests/DefaultReconnectDisplay.test.ts

@@ -6,13 +6,13 @@ describe('DefaultReconnectDisplay', () => {
 
     it ('adds element to the body on show', () => {
         const testDocument = new JSDOM().window.document;
-        const display = new DefaultReconnectDisplay(testDocument);
+        const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
 
         display.show();
 
         const element = testDocument.body.querySelector('div');
         expect(element).toBeDefined();
-        expect(element!.id).toBe(AutoReconnectCircuitHandler.DialogId);
+        expect(element!.id).toBe('test-dialog-id');
         expect(element!.style.display).toBe('block');
 
         expect(display.message.textContent).toBe('Attempting to reconnect to the server...');
@@ -21,7 +21,7 @@ describe('DefaultReconnectDisplay', () => {
 
     it ('does not add element to the body multiple times', () => {
         const testDocument = new JSDOM().window.document;
-        const display = new DefaultReconnectDisplay(testDocument);
+        const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
 
         display.show();
         display.show();
@@ -31,7 +31,7 @@ describe('DefaultReconnectDisplay', () => {
 
     it ('hides element', () => {
         const testDocument = new JSDOM().window.document;
-        const display = new DefaultReconnectDisplay(testDocument);
+        const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
 
         display.hide();
 
@@ -40,7 +40,7 @@ describe('DefaultReconnectDisplay', () => {
 
     it ('updates message on fail', () => {
         const testDocument = new JSDOM().window.document;
-        const display = new DefaultReconnectDisplay(testDocument);
+        const display = new DefaultReconnectDisplay('test-dialog-id', testDocument);
 
         display.show();
         display.failed();

+ 98 - 0
src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts

@@ -0,0 +1,98 @@
+import '../src/GlobalExports';
+import { UserSpecifiedDisplay } from '../src/Platform/Circuits/UserSpecifiedDisplay';
+import { DefaultReconnectionHandler } from '../src/Platform/Circuits/DefaultReconnectionHandler';
+import { NullLogger} from '../src/Platform/Logging/Loggers';
+import { resolveOptions, ReconnectionOptions } from "../src/Platform/Circuits/BlazorOptions";
+import { ReconnectDisplay } from '../src/Platform/Circuits/ReconnectDisplay';
+
+const defaultReconnectionOptions = resolveOptions().reconnectionOptions;
+
+describe('DefaultReconnectionHandler', () => {
+  it('toggles user-specified UI on disconnection/connection', () => {
+    const element = attachUserSpecifiedUI(defaultReconnectionOptions);
+    const handler = new DefaultReconnectionHandler(NullLogger.instance);
+
+    // Shows on disconnection
+    handler.onConnectionDown(defaultReconnectionOptions);
+    expect(element.className).toBe(UserSpecifiedDisplay.ShowClassName);
+
+    // Hides on reconnection
+    handler.onConnectionUp();
+    expect(element.className).toBe(UserSpecifiedDisplay.HideClassName);
+
+    document.body.removeChild(element);
+  });
+
+  it('hides display on connection up, and stops retrying', async () => {
+    const testDisplay = createTestDisplay();
+    const reconnect = jest.fn().mockResolvedValue(true);
+    const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
+
+    handler.onConnectionDown({
+      maxRetries: 1000,
+      retryIntervalMilliseconds: 100,
+      dialogId: 'ignored'
+    });
+    handler.onConnectionUp();
+
+    expect(testDisplay.hide).toHaveBeenCalled();
+    await delay(200);
+    expect(reconnect).not.toHaveBeenCalled();
+  });
+
+  it('shows display on connection down', async () => {
+    const testDisplay = createTestDisplay();
+    const reconnect = jest.fn().mockResolvedValue(true);
+    const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
+
+    handler.onConnectionDown({
+      maxRetries: 1000,
+      retryIntervalMilliseconds: 100,
+      dialogId: 'ignored'
+    });
+    expect(testDisplay.show).toHaveBeenCalled();
+    expect(testDisplay.failed).not.toHaveBeenCalled();
+    expect(reconnect).not.toHaveBeenCalled();
+
+    await delay(150);
+    expect(reconnect).toHaveBeenCalledTimes(1);
+  });
+
+  it('invokes failed if reconnect fails', async () => {
+    const testDisplay = createTestDisplay();
+    const reconnect = jest.fn().mockRejectedValue(null);
+    const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
+    window.console.error = jest.fn();
+
+    handler.onConnectionDown({
+      maxRetries: 3,
+      retryIntervalMilliseconds: 20,
+      dialogId: 'ignored'
+    });
+
+    await delay(100);
+    expect(testDisplay.show).toHaveBeenCalled();
+    expect(testDisplay.failed).toHaveBeenCalled();
+    expect(reconnect).toHaveBeenCalledTimes(3);
+  });
+});
+
+function attachUserSpecifiedUI(options: ReconnectionOptions): Element {
+  const element = document.createElement('div');
+  element.id = options.dialogId;
+  element.className = UserSpecifiedDisplay.HideClassName;
+  document.body.appendChild(element);
+  return element;
+}
+
+function delay(durationMilliseconds: number) {
+  return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
+}
+
+function createTestDisplay(): ReconnectDisplay {
+  return {
+    show: jest.fn(),
+    hide: jest.fn(),
+    failed: jest.fn()
+  };
+}

+ 1 - 1
src/Components/Web.JS/tests/RenderQueue.test.ts

@@ -1,6 +1,6 @@
 (global as any).DotNet = { attachReviver: jest.fn() };
 
-import RenderQueue from '../src/Platform/Circuits/RenderQueue';
+import { RenderQueue } from '../src/Platform/Circuits/RenderQueue';
 import { NullLogger } from '../src/Platform/Logging/Loggers';
 import * as signalR from '@aspnet/signalr';
 

Некоторые файлы не были показаны из-за большого количества измененных файлов