diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
index ae19f7df5e..f1ae22f3ae 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
@@ -3,23 +3,22 @@ import { HttpErrorResponse } from '@angular/common/http';
import {
ApplicationRef,
ComponentFactoryResolver,
+ ComponentRef,
EmbeddedViewRef,
Inject,
Injectable,
Injector,
RendererFactory2,
- Type,
- ComponentRef,
} from '@angular/core';
-import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/router-plugin';
+import { Navigate, RouterDataResolved, RouterError, RouterState } from '@ngxs/router-plugin';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
import snq from 'snq';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
-import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
+import { ErrorScreenErrorCodes, HttpErrorConfig } from '../models/common';
import { Confirmation } from '../models/confirmation';
import { ConfirmationService } from '../services/confirmation.service';
-import { filter, tap } from 'rxjs/operators';
export const DEFAULT_ERROR_MESSAGES = {
defaultError: {
@@ -58,7 +57,6 @@ export class ErrorHandler {
private injector: Injector,
@Inject('HTTP_ERROR_CONFIG') private httpErrorConfig: HttpErrorConfig,
) {
- this.httpErrorConfig.skipHandledErrorCodes = this.httpErrorConfig.skipHandledErrorCodes || [];
this.listenToRestError();
this.listenToRouterError();
this.listenToRouterDataResolved();
@@ -66,7 +64,7 @@ export class ErrorHandler {
private listenToRouterError() {
this.actions
- .pipe(ofActionSuccessful(RouterError), filter(this.filterRouteErrors), tap(console.warn))
+ .pipe(ofActionSuccessful(RouterError), filter(this.filterRouteErrors))
.subscribe(() => this.show404Page());
}
@@ -84,14 +82,15 @@ export class ErrorHandler {
private listenToRestError() {
this.actions
- .pipe(ofActionSuccessful(RestOccurError), filter(this.filterRestErrors))
- .subscribe(({ payload: { err = {} as HttpErrorResponse } }) => {
- const body = snq(
- () => (err as HttpErrorResponse).error.error,
- DEFAULT_ERROR_MESSAGES.defaultError.title,
- );
+ .pipe(
+ ofActionSuccessful(RestOccurError),
+ map(action => action.payload),
+ filter(this.filterRestErrors),
+ )
+ .subscribe(err => {
+ const body = snq(() => err.error.error, DEFAULT_ERROR_MESSAGES.defaultError.title);
- if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) {
+ if (err.headers.get('_AbpErrorFormat')) {
const confirmation$ = this.showError(null, null, body);
if (err.status === 401) {
@@ -100,7 +99,7 @@ export class ErrorHandler {
});
}
} else {
- switch ((err as HttpErrorResponse).status) {
+ switch (err.status) {
case 401:
this.canCreateCustomError(401)
? this.show401Page()
@@ -156,7 +155,7 @@ export class ErrorHandler {
});
break;
case 0:
- if ((err as HttpErrorResponse).statusText === 'Unknown Error') {
+ if (err.statusText === 'Unknown Error') {
this.createErrorComponent({
title: {
key: 'AbpAccount::DefaultErrorMessage',
@@ -238,6 +237,7 @@ export class ErrorHandler {
.create(this.injector);
for (const key in instance) {
+ /* istanbul ignore else */
if (this.componentRef.instance.hasOwnProperty(key)) {
this.componentRef.instance[key] = instance[key];
}
@@ -270,11 +270,8 @@ export class ErrorHandler {
);
}
- private filterRestErrors = (instance: RestOccurError): boolean => {
- const {
- payload: { err: { status } = {} as HttpErrorResponse },
- } = instance;
- if (!status) return false;
+ private filterRestErrors = ({ status }: HttpErrorResponse): boolean => {
+ if (typeof status !== 'number') return false;
return this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0;
};
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
index c33a6372cb..bacbb93ed0 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
@@ -1,121 +1,219 @@
-import { CoreModule, RestOccurError, RouterOutletComponent } from '@abp/ng.core';
-import { Location } from '@angular/common';
+import { CoreModule, RestOccurError } from '@abp/ng.core';
+import { APP_BASE_HREF } from '@angular/common';
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Component, NgModule } from '@angular/core';
-import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
-import { NgxsModule, Store } from '@ngxs/store';
+import { NavigationError, ResolveEnd, RouterModule } from '@angular/router';
+import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
+import { Navigate, RouterDataResolved, RouterError } from '@ngxs/router-plugin';
+import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
+import { OAuthService } from 'angular-oauth2-oidc';
+import { of } from 'rxjs';
+import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers';
-import { ThemeSharedModule } from '../theme-shared.module';
-import { RouterError, RouterDataResolved } from '@ngxs/router-plugin';
-import { NavigationError, ResolveEnd } from '@angular/router';
-import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
+import { ConfirmationService } from '../services';
+import { httpErrorConfigFactory } from '../tokens/http-error.token';
-@Component({
- selector: 'abp-dummy',
- template: 'dummy works! ',
+@NgModule({
+ exports: [HttpErrorWrapperComponent],
+ declarations: [HttpErrorWrapperComponent],
+ entryComponents: [HttpErrorWrapperComponent],
+ imports: [CoreModule],
})
-class DummyComponent {
- constructor(public errorHandler: ErrorHandler) {}
-}
+class MockModule {}
-let spectator: SpectatorRouting;
+let spectator: SpectatorService;
+let service: ErrorHandler;
let store: Store;
+const errorConfirmation: jest.Mock = jest.fn(() => of(null));
+const CONFIRMATION_BUTTONS = {
+ hideCancelBtn: true,
+ yesText: 'AbpAccount::Close',
+};
describe('ErrorHandler', () => {
- const createComponent = createRoutingFactory({
- component: DummyComponent,
- imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
+ const createService = createServiceFactory({
+ service: ErrorHandler,
+ imports: [RouterModule.forRoot([]), NgxsModule.forRoot([]), CoreModule, MockModule],
mocks: [OAuthService],
- stubsEnabled: false,
- routes: [
- { path: '', component: DummyComponent },
- { path: 'account/login', component: RouterOutletComponent },
+ providers: [
+ { provide: APP_BASE_HREF, useValue: '/' },
+ {
+ provide: 'HTTP_ERROR_CONFIG',
+ useFactory: httpErrorConfigFactory,
+ },
+ {
+ provide: ConfirmationService,
+ useValue: {
+ error: errorConfirmation,
+ },
+ },
],
});
beforeEach(() => {
- spectator = createComponent();
+ spectator = createService();
+ service = spectator.service;
store = spectator.get(Store);
+ store.selectSnapshot = jest.fn(() => '/x');
+ });
- const abpError = document.querySelector('abp-http-error-wrapper');
- if (abpError) document.body.removeChild(abpError);
+ afterEach(() => {
+ errorConfirmation.mockClear();
+ removeIfExistsInDom(selectHtmlErrorWrapper);
});
- test.skip('should display the error component when server error occurs', () => {
- store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
- expect(document.querySelector('.error-template')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError500.title,
- );
- expect(document.querySelector('.error-details')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError500.details,
- );
+ test('should display HttpErrorWrapperComponent when server error occurs', () => {
+ const createComponent = jest.spyOn(service, 'createErrorComponent');
+ const error = new HttpErrorResponse({ status: 500 });
+ const params = {
+ title: {
+ key: 'AbpAccount::500Message',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title,
+ },
+ details: {
+ key: 'AbpAccount::InternalServerErrorMessage',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details,
+ },
+ status: 500,
+ };
+
+ expect(selectHtmlErrorWrapper()).toBeNull();
+
+ store.dispatch(new RestOccurError(error));
+
+ expect(createComponent).toHaveBeenCalledWith(params);
+
+ const wrapper = service.componentRef.instance;
+ expect(wrapper.title).toEqual(params.title);
+ expect(wrapper.details).toEqual(params.details);
+ expect(wrapper.status).toBe(params.status);
+
+ expect(selectHtmlErrorWrapper()).not.toBeNull();
});
- test.skip('should display the error component when authorize error occurs', () => {
- store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
- expect(document.querySelector('.error-template')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError403.title,
- );
- expect(document.querySelector('.error-details')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError403.details,
- );
+ test('should display HttpErrorWrapperComponent when authorize error occurs', () => {
+ const createComponent = jest.spyOn(service, 'createErrorComponent');
+ const error = new HttpErrorResponse({ status: 403 });
+ const params = {
+ title: {
+ key: 'AbpAccount::DefaultErrorMessage403',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title,
+ },
+ details: {
+ key: 'AbpAccount::DefaultErrorMessage403Detail',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details,
+ },
+ status: 403,
+ };
+
+ expect(selectHtmlErrorWrapper()).toBeNull();
+
+ store.dispatch(new RestOccurError(error));
+
+ expect(createComponent).toHaveBeenCalledWith(params);
+
+ const wrapper = service.componentRef.instance;
+ expect(wrapper.title).toEqual(params.title);
+ expect(wrapper.details).toEqual(params.details);
+ expect(wrapper.status).toBe(params.status);
+
+ expect(selectHtmlErrorWrapper()).not.toBeNull();
});
- test.skip('should display the error component when unknown error occurs', () => {
- store.dispatch(
- new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })),
- );
- expect(document.querySelector('.error-template')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError.title,
- );
+ test('should display HttpErrorWrapperComponent when unknown error occurs', () => {
+ const createComponent = jest.spyOn(service, 'createErrorComponent');
+ const error = new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' });
+ const params = {
+ title: {
+ key: 'AbpAccount::DefaultErrorMessage',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
+ },
+ details: error.message,
+ isHomeShow: false,
+ };
+
+ expect(selectHtmlErrorWrapper()).toBeNull();
+
+ store.dispatch(new RestOccurError(error));
+
+ expect(createComponent).toHaveBeenCalledWith(params);
+
+ const wrapper = service.componentRef.instance;
+ expect(wrapper.title).toEqual(params.title);
+ expect(wrapper.details).toEqual(params.details);
+ expect(wrapper.isHomeShow).toBe(params.isHomeShow);
+
+ expect(selectHtmlErrorWrapper()).not.toBeNull();
});
- test.skip('should display the confirmation when not found error occurs', () => {
+ test('should call error method of ConfirmationService when not found error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
- spectator.detectChanges();
- expect(spectator.query('.confirmation .title')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError404.title,
- );
- expect(spectator.query('.confirmation .message')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError404.details,
+
+ expect(errorConfirmation).toHaveBeenCalledWith(
+ {
+ key: 'AbpAccount::DefaultErrorMessage404',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
+ },
+ {
+ key: 'AbpAccount::DefaultErrorMessage404Detail',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
+ },
+ CONFIRMATION_BUTTONS,
);
});
- test.skip('should display the confirmation when default error occurs', () => {
+ test('should call error method of ConfirmationService when default error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
- spectator.detectChanges();
- expect(spectator.query('.confirmation .title')).toHaveText(
- DEFAULT_ERROR_MESSAGES.defaultError.title,
- );
- expect(spectator.query('.confirmation .message')).toHaveText(
+
+ expect(errorConfirmation).toHaveBeenCalledWith(
DEFAULT_ERROR_MESSAGES.defaultError.details,
+ DEFAULT_ERROR_MESSAGES.defaultError.title,
+ CONFIRMATION_BUTTONS,
);
});
- test.skip('should display the confirmation when authenticated error occurs', async () => {
+ test('should call error method of ConfirmationService when authenticated error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
- spectator.detectChanges();
- spectator.click('#confirm');
- await spectator.fixture.whenStable();
- expect(spectator.get(Location).path()).toBe('/account/login');
+ expect(errorConfirmation).toHaveBeenCalledWith(
+ {
+ key: 'AbpAccount::DefaultErrorMessage401',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
+ },
+ {
+ key: 'AbpAccount::DefaultErrorMessage401Detail',
+ defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details,
+ },
+ CONFIRMATION_BUTTONS,
+ );
});
- test.skip('should display the confirmation when authenticated error occurs with _AbpErrorFormat header', async () => {
- let headers: HttpHeaders = new HttpHeaders();
- headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat');
+ test('should call error method of ConfirmationService when authenticated error occurs with _AbpErrorFormat header', done => {
+ spectator
+ .get(Actions)
+ .pipe(ofActionDispatched(Navigate))
+ .subscribe(({ path, queryParams, extras }) => {
+ expect(path).toEqual(['/account/login']);
+ expect(queryParams).toBeNull();
+ expect(extras).toEqual({ state: { redirectUrl: '/x' } });
+
+ done();
+ });
+ const headers: HttpHeaders = new HttpHeaders({
+ _AbpErrorFormat: '_AbpErrorFormat',
+ });
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401, headers })));
- spectator.detectChanges();
- spectator.click('#confirm');
- await spectator.fixture.whenStable();
- expect(spectator.get(Location).path()).toBe('/account/login');
+ expect(errorConfirmation).toHaveBeenCalledWith(
+ DEFAULT_ERROR_MESSAGES.defaultError.title,
+ null,
+ CONFIRMATION_BUTTONS,
+ );
});
- test.skip('should display the confirmation when error occurs with _AbpErrorFormat header', () => {
+ test('should call error method of ConfirmationService when error occurs with _AbpErrorFormat header', () => {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat');
-
store.dispatch(
new RestOccurError(
new HttpErrorResponse({
@@ -125,17 +223,28 @@ describe('ErrorHandler', () => {
}),
),
);
- spectator.detectChanges();
- expect(spectator.query('.title')).toHaveText('test message');
- expect(spectator.query('.confirmation .message')).toHaveText('test detail');
+ expect(errorConfirmation).toHaveBeenCalledWith(
+ 'test detail',
+ 'test message',
+ CONFIRMATION_BUTTONS,
+ );
+ });
+
+ test('should call destroy method of componentRef when ResolveEnd is dispatched', () => {
+ store.dispatch(new RouterError(null, null, new NavigationError(1, 'test', 'Cannot match')));
+
+ const destroyComponent = jest.spyOn(service.componentRef, 'destroy');
+
+ store.dispatch(new RouterDataResolved(null, new ResolveEnd(1, 'test', 'test', null)));
+
+ expect(destroyComponent).toHaveBeenCalledTimes(1);
});
});
@Component({
selector: 'abp-dummy-error',
- template:
- '{{errorStatus}}
',
+ template: '{{errorStatus}}
',
})
class DummyErrorComponent {
errorStatus;
@@ -150,68 +259,103 @@ class DummyErrorComponent {
class ErrorModule {}
describe('ErrorHandler with custom error component', () => {
- const createComponent = createRoutingFactory({
- component: DummyComponent,
+ const createService = createServiceFactory({
+ service: ErrorHandler,
imports: [
- CoreModule,
- ThemeSharedModule.forRoot({
- httpErrorConfig: {
- errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] },
- },
- }),
+ RouterModule.forRoot([]),
NgxsModule.forRoot([]),
+ CoreModule,
+ MockModule,
ErrorModule,
],
- mocks: [OAuthService],
- stubsEnabled: false,
-
- routes: [
- { path: '', component: DummyComponent },
- { path: 'account/login', component: RouterOutletComponent },
+ mocks: [OAuthService, ConfirmationService],
+ providers: [
+ { provide: APP_BASE_HREF, useValue: '/' },
+ {
+ provide: 'HTTP_ERROR_CONFIG',
+ useFactory: customHttpErrorConfigFactory,
+ },
],
});
beforeEach(() => {
- spectator = createComponent();
+ spectator = createService();
+ service = spectator.service;
store = spectator.get(Store);
+ store.selectSnapshot = jest.fn(() => '/x');
+ });
- const abpError = document.querySelector('abp-http-error-wrapper');
- if (abpError) document.body.removeChild(abpError);
+ afterEach(() => {
+ removeIfExistsInDom(selectCustomError);
});
describe('Custom error component', () => {
- test.skip('should create when occur 401', () => {
+ test('should be created when 401 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
- expect(document.querySelector('abp-dummy-error')).toBeTruthy();
- expect(document.querySelector('p')).toHaveExactText('401');
+
+ expect(selectCustomErrorText()).toBe('401');
});
- test.skip('should create when occur 403', () => {
+ test('should be created when 403 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
- expect(document.querySelector('p')).toHaveExactText('403');
+
+ expect(selectCustomErrorText()).toBe('403');
});
- test.skip('should create when occur 404', () => {
+ test('should be created when 404 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
- expect(document.querySelector('p')).toHaveExactText('404');
+
+ expect(selectCustomErrorText()).toBe('404');
});
- test.skip('should create when dispatched the RouterError', () => {
+ test('should be created when RouterError is dispatched', () => {
store.dispatch(new RouterError(null, null, new NavigationError(1, 'test', 'Cannot match')));
- expect(document.querySelector('p')).toHaveExactText('404');
- store.dispatch(new RouterDataResolved(null, new ResolveEnd(1, 'test', 'test', null)));
+
+ expect(selectCustomErrorText()).toBe('404');
});
- test.skip('should create when occur 500', () => {
+ test('should be created when 500 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
- expect(document.querySelector('p')).toHaveExactText('500');
+
+ expect(selectCustomErrorText()).toBe('500');
});
- test.skip('should be destroyed when click the close button', () => {
- store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
- document.querySelector('#close-dummy').click();
- spectator.detectChanges();
- expect(document.querySelector('abp-dummy-error')).toBeFalsy();
+ test('should call destroy method of componentRef when destroy$ emits', () => {
+ store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
+
+ expect(selectCustomErrorText()).toBe('401');
+
+ const destroyComponent = jest.spyOn(service.componentRef, 'destroy');
+
+ service.componentRef.instance.destroy$.next();
+
+ expect(destroyComponent).toHaveBeenCalledTimes(1);
});
});
});
+
+export function customHttpErrorConfigFactory() {
+ return httpErrorConfigFactory({
+ errorScreen: {
+ component: DummyErrorComponent,
+ forWhichErrors: [401, 403, 404, 500],
+ },
+ });
+}
+
+function removeIfExistsInDom(errorSelector: () => HTMLDivElement | null) {
+ const abpError = errorSelector();
+ if (abpError) abpError.parentNode.removeChild(abpError);
+}
+
+function selectHtmlErrorWrapper(): HTMLDivElement | null {
+ return document.querySelector('abp-http-error-wrapper');
+}
+
+function selectCustomError(): HTMLDivElement | null {
+ return document.querySelector('abp-dummy-error');
+}
+
+function selectCustomErrorText(): string {
+ return selectCustomError().querySelector('p').textContent;
+}