Skip to content

Commit

Permalink
Merge pull request #2142 from abpframework/feat/theme-shared/custom-e…
Browse files Browse the repository at this point in the history
…rror

feature: custom http error component
  • Loading branch information
thediaval authored Nov 11, 2019
2 parents 2f01666 + 8985187 commit 1505632
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class ErrorComponent implements AfterViewInit, OnDestroy {
if (this.customComponent) {
const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(null);
customComponentRef.instance.errorStatus = this.status;
customComponentRef.instance.destroy$ = this.destroy$;
this.containerRef.nativeElement.appendChild((customComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
customComponentRef.changeDetectorRef.detectChanges();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const DEFAULT_ERROR_MESSAGES = {
details: 'The resource requested could not found on the server.',
},
defaultError500: {
title: '500',
title: 'Internal server error',
details: 'Error detail not sent by server.',
},
};
Expand Down Expand Up @@ -106,11 +106,11 @@ export class ErrorHandler {
: this.showError(
{
key: 'AbpAccount::DefaultErrorMessage404',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
},
{
key: 'AbpAccount::DefaultErrorMessage404Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export class ConfirmationService extends AbstractToaster<Confirmation.Options> {
options?: Confirmation.Options,
): Observable<Toaster.Status> {
this.listenToEscape();

return super.show(message, title, severity, options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ErrorComponent } from '../components/error/error.component';
import { LocalizationPipe } from '@abp/ng.core';
import { Store } from '@ngxs/store';
import { Renderer2, ElementRef } from '@angular/core';
import { Subject } from 'rxjs';

describe('ErrorComponent', () => {
let spectator: SpectatorHost<ErrorComponent>;
Expand All @@ -16,21 +17,26 @@ describe('ErrorComponent', () => {
],
});

beforeEach(() => (spectator = createHost('<abp-error></abp-error>')));
beforeEach(() => {
spectator = createHost('<abp-error></abp-error>');
spectator.component.destroy$ = new Subject();
});

describe('#destroy', () => {
it('should remove the dom', () => {
const renderer = spectator.get(Renderer2);
const rendererSpy = jest.spyOn(renderer, 'removeChild');
spectator.component.renderer = renderer;
it('should be call when pressed the esc key', done => {
spectator.component.destroy$.subscribe(res => {
done();
});

spectator.keyboard.pressEscape();
});

const elementRef = spectator.get(ElementRef);
spectator.component.elementRef = elementRef;
spectator.component.host = spectator.hostComponent;
it('should be call when clicked the close button', done => {
spectator.component.destroy$.subscribe(res => {
done();
});

spectator.click('button#abp-close-button');
spectator.detectChanges();
expect(rendererSpy).toHaveBeenCalledWith(spectator.hostComponent, elementRef.nativeElement);
spectator.click('#abp-close-button');
});
});
});
102 changes: 86 additions & 16 deletions npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { CoreModule, RestOccurError, RouterOutletComponent } from '@abp/ng.core';
import { Location } from '@angular/common';
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Component } from '@angular/core';
import { Component, NgModule } from '@angular/core';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store';
import { DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers';
import { ThemeSharedModule } from '../theme-shared.module';
import { MessageService } from 'primeng/components/common/messageservice';
import { RouterError, RouterDataResolved } from '@ngxs/router-plugin';
import { NavigationError, ResolveEnd } from '@angular/router';

@Component({ selector: 'abp-dummy', template: 'dummy works! <abp-confirmation></abp-confirmation>' })
class DummyComponent {
constructor(public errorHandler: ErrorHandler, public store: Store) {}
constructor(public errorHandler: ErrorHandler) {}
}

let spectator: SpectatorRouting<DummyComponent>;
let store: Store;
describe('ErrorHandler', () => {
let spectator: SpectatorRouting<DummyComponent>;
let store: Store;

const createComponent = createRoutingFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
Expand All @@ -25,35 +27,27 @@ describe('ErrorHandler', () => {

beforeEach(() => {
spectator = createComponent();
store = spectator.component.store;
store = spectator.get(Store);

const abpError = document.querySelector('abp-error');
if (abpError) document.body.removeChild(abpError);
});

it('should display the error component when server error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
spectator.detectChanges();
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError500.title);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError500.details.defaultValue,
);
expect(document.querySelector('.error-details')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError500.details);
});

it('should display the error component when authorize error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
spectator.detectChanges();
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError403.title);
expect(document.querySelector('.error-details')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError403.details);
});

it('should display the error component when unknown error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })));
spectator.detectChanges();
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.title);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.details.defaultValue,
);
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError.title);
});

it('should display the confirmation when not found error occurs', () => {
Expand Down Expand Up @@ -110,3 +104,79 @@ describe('ErrorHandler', () => {
expect(spectator.query('.abp-confirm-body')).toHaveText('test detail');
});
});

@Component({
selector: 'abp-dummy-error',
template: '<p>{{errorStatus}}</p><button id="close-dummy" (click)="destroy$.next()">Close</button>',
})
class DummyErrorComponent {
errorStatus;
destroy$;
}

@NgModule({
declarations: [DummyErrorComponent],
exports: [DummyErrorComponent],
entryComponents: [DummyErrorComponent],
})
class ErrorModule {}

describe('ErrorHandler with custom error component', () => {
const createComponent = createRoutingFactory({
component: DummyComponent,
imports: [
CoreModule,
ThemeSharedModule.forRoot({
httpErrorConfig: { errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] } },
}),
NgxsModule.forRoot([]),
ErrorModule,
],
stubsEnabled: false,
routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }],
});

beforeEach(() => {
spectator = createComponent();
store = spectator.get(Store);

const abpError = document.querySelector('abp-error');
if (abpError) document.body.removeChild(abpError);
});

describe('Custom error component', () => {
it('should create when occur 401', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
expect(document.querySelector('abp-dummy-error')).toBeTruthy();
expect(document.querySelector('p')).toHaveExactText('401');
});

it('should create when occur 403', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
expect(document.querySelector('p')).toHaveExactText('403');
});

it('should create when occur 404', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
expect(document.querySelector('p')).toHaveExactText('404');
});

it('should create when dispatched the RouterError', () => {
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)));
});

it('should create when occur 500', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
expect(document.querySelector('p')).toHaveExactText('500');
});

it('should be destroyed when click the close button', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
document.querySelector<HTMLButtonElement>('#close-dummy').click();
spectator.detectChanges();
expect(document.querySelector('abp-dummy-error')).toBeFalsy();
});
});
});

0 comments on commit 1505632

Please sign in to comment.