Skip to content

fix(console): reload app when chunks hash modified #12335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion gravitee-apim-console-webui/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as angular from 'angular';

import { CommonModule } from '@angular/common';
import { provideHttpClient, withInterceptorsFromDi, withXsrfConfiguration } from '@angular/common/http';
import { ApplicationRef, DoBootstrap, importProvidersFrom, NgModule } from '@angular/core';
import { ApplicationRef, DoBootstrap, ErrorHandler, importProvidersFrom, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { setAngularJSGlobal, UpgradeModule } from '@angular/upgrade/static';
Expand All @@ -35,6 +35,7 @@ import { AppRoutingModule } from './app-routing.module';
import { UserComponent } from './user/my-accout/user.component';
import { AuthModule } from './auth/auth.module';
import { GioFormJsonSchemaExtendedModule } from './shared/components/form-json-schema-extended/form-json-schema-extended.module';
import { GlobalErrorHandler } from './error-handling/global-error-handler';

@NgModule({
declarations: [AppComponent, UserComponent],
Expand Down Expand Up @@ -80,6 +81,7 @@ import { GioFormJsonSchemaExtendedModule } from './shared/components/form-json-s
headerName: 'none',
}),
),
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
})
export class AppModule implements DoBootstrap {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GlobalErrorHandler } from './global-error-handler';

describe('GlobalErrorHandler', () => {
let handler: GlobalErrorHandler;
let consoleErrorSpy: jest.SpyInstance;
let locationReloadSpy: jest.SpyInstance;
let sessionStorageGetItemSpy: jest.SpyInstance;
let sessionStorageSetItemSpy: jest.SpyInstance;
const originalLocation = window.location;

beforeEach(() => {
handler = new GlobalErrorHandler();

consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

// To mock window.location.reload
delete (window as any).location;
window.location = { ...originalLocation, reload: jest.fn() };
locationReloadSpy = jest.spyOn(window.location, 'reload');

sessionStorageGetItemSpy = jest.spyOn(Storage.prototype, 'getItem');
sessionStorageSetItemSpy = jest.spyOn(Storage.prototype, 'setItem');
});

afterEach(() => {
jest.restoreAllMocks();
sessionStorage.clear();
window.location = originalLocation;
});

it('should forward non-chunk-load errors to console.error', () => {
const error = new Error('A regular error');
handler.handleError(error);
expect(consoleErrorSpy).toHaveBeenCalledWith(error);
expect(locationReloadSpy).not.toHaveBeenCalled();
expect(sessionStorageSetItemSpy).not.toHaveBeenCalled();
});

describe('ChunkLoadError handling', () => {
const chunkLoadErrorString = 'Loading chunk 123 failed';
const chunkLoadError = new Error(chunkLoadErrorString);

it('should reload the page on the first chunk load error (string)', () => {
sessionStorageGetItemSpy.mockReturnValue(null);
handler.handleError(chunkLoadErrorString);

expect(sessionStorageSetItemSpy).toHaveBeenCalledWith('chunkLoadRetries', '1');
expect(locationReloadSpy).toHaveBeenCalled();
expect(consoleErrorSpy).not.toHaveBeenCalled();
});

it('should reload the page on the first chunk load error (Error object)', () => {
sessionStorageGetItemSpy.mockReturnValue(null);
handler.handleError(chunkLoadError);

expect(sessionStorageSetItemSpy).toHaveBeenCalledWith('chunkLoadRetries', '1');
expect(locationReloadSpy).toHaveBeenCalled();
expect(consoleErrorSpy).not.toHaveBeenCalled();
});

it('should not reload the page if max retries have been reached', () => {
sessionStorageGetItemSpy.mockReturnValue('1');
handler.handleError(chunkLoadError);

expect(sessionStorageSetItemSpy).not.toHaveBeenCalled();
expect(locationReloadSpy).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith('Chunk loading failed multiple times. Please refresh manually.');
});

it('should handle different chunk numbers', () => {
const differentChunkError = 'Loading chunk 456 failed';
sessionStorageGetItemSpy.mockReturnValue(null);
handler.handleError(differentChunkError);

expect(sessionStorageSetItemSpy).toHaveBeenCalledWith('chunkLoadRetries', '1');
expect(locationReloadSpy).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorHandler, Injectable } from '@angular/core';

const CHUNK_LOAD_ERROR_REGEX = /Loading chunk [\d]+ failed/;
const CHUNK_LOAD_RETRIES_SESSION_KEY = 'chunkLoadRetries';
const MAX_RETRIES = 1;

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
if (this.isChunkLoadError(error)) {
this.handleChunkLoadError();
} else {
// Forward all other errors
// eslint-disable-next-line angular/log
console.error(error);
}
}

private isChunkLoadError(error: unknown): boolean {
let message = '';
if (typeof error === 'string') {
message = error;
} else if (error instanceof Error) {
message = error.message;
}
return CHUNK_LOAD_ERROR_REGEX.test(message);
}

private handleChunkLoadError(): void {
const retries = sessionStorage.getItem(CHUNK_LOAD_RETRIES_SESSION_KEY) || '0';
const retryCount = parseInt(retries, 10);

if (retryCount < MAX_RETRIES) {
sessionStorage.setItem(CHUNK_LOAD_RETRIES_SESSION_KEY, (retryCount + 1).toString());
window.location.reload();
} else {
// eslint-disable-next-line angular/log
console.error('Chunk loading failed multiple times. Please refresh manually.');
}
}
}