Skip to content

Commit

Permalink
fix(stark-ui): remove all overlays by destroying the Angular CDK Over…
Browse files Browse the repository at this point in the history
…layContainer when navigating to an 'exit' state

ISSUES CLOSED: #1570
  • Loading branch information
christophercr committed Mar 27, 2020
1 parent 16d6a4f commit 8d3dee3
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 2 deletions.
27 changes: 25 additions & 2 deletions packages/stark-ui/src/modules/session-ui/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { HookResult, Transition } from "@uirouter/core";
import { Ng2StateDeclaration } from "@uirouter/angular";
import { OverlayContainer } from "@angular/cdk/overlay";
import {
starkLoginStateName,
starkLoginStateUrl,
Expand All @@ -16,6 +18,25 @@ import {
StarkSessionLogoutPageComponent
} from "./pages";

/**
* Hook to destroy the OverlayContainer inside which all overlays are rendered (i.e. stark-dropdown options)
* Fixes https://github.com/NationalBankBelgium/stark/issues/1570
*/
export function destroyOverlaysOnEnterFn(transition: Transition): HookResult {
try {
// inject the OverlayContainer
const overlayContainer = transition.injector().getNative<OverlayContainer>(OverlayContainer);
// destroy the container by calling its own "ngOnDestroy" method
// see https://github.com/angular/components/pull/5378/files
/* tslint:disable-next-line:no-lifecycle-call*/
overlayContainer.ngOnDestroy();
} catch (err) {
// the OverlayContainer could not be injected, do nothing
}

return true;
}

/**
* States defined by Session-UI Module
*/
Expand Down Expand Up @@ -46,7 +67,8 @@ export const SESSION_UI_STATES: Ng2StateDeclaration[] = [
"initOrExit@": {
component: StarkSessionExpiredPageComponent
}
}
},
onEnter: destroyOverlaysOnEnterFn
},
{
name: starkSessionLogoutStateName, // the parent is defined in the state's name (contains a dot)
Expand All @@ -55,6 +77,7 @@ export const SESSION_UI_STATES: Ng2StateDeclaration[] = [
"initOrExit@": {
component: StarkSessionLogoutPageComponent
}
}
},
onEnter: destroyOverlaysOnEnterFn
}
];
149 changes: 149 additions & 0 deletions packages/stark-ui/src/modules/session-ui/session-ui.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* tslint:disable:completed-docs */
import { async, fakeAsync, inject, TestBed, tick } from "@angular/core/testing";
import { Component, ModuleWithProviders, NgModuleFactoryLoader, SystemJsNgModuleLoader } from "@angular/core";
import { OverlayContainer } from "@angular/cdk/overlay";
import { UIRouterModule } from "@uirouter/angular";
import { Store } from "@ngrx/store";
import { EffectsModule } from "@ngrx/effects";
import { provideMockActions } from "@ngrx/effects/testing";
import { StateObject, StateService, UIRouter } from "@uirouter/core";
import { TranslateModule } from "@ngx-translate/core";
import { catchError, switchMap } from "rxjs/operators";
import { from, of, throwError } from "rxjs";
import {
SESSION_STATES,
STARK_SESSION_SERVICE,
starkSessionExpiredStateName,
starkSessionLogoutStateName
} from "@nationalbankbelgium/stark-core";
import { MockStarkSessionService } from "@nationalbankbelgium/stark-core/testing";
import { StarkSessionUiModule } from "./session-ui.module";
import createSpyObj = jasmine.createSpyObj;
import SpyObj = jasmine.SpyObj;

describe("SessionUiModule", () => {
let $state: StateService;
let router: UIRouter;
let overlayContainer: SpyObj<OverlayContainer>;
const homeStateNAme = "homepage";

@Component({ selector: "home-component", template: "HOME" })
class HomeComponent {}

const routerModule: ModuleWithProviders = UIRouterModule.forRoot({
useHash: true,
states: [
{
name: homeStateNAme,
url: `/${homeStateNAme}`,
parent: "",
component: HomeComponent
},
...SESSION_STATES // these are the parent states of the Session UI States
]
});

beforeEach(async(() => {
return TestBed.configureTestingModule({
declarations: [HomeComponent],
imports: [routerModule, EffectsModule.forRoot([]), TranslateModule.forRoot(), StarkSessionUiModule.forRoot()],
providers: [
provideMockActions(() => of("some action")),
{
provide: Store,
useValue: createSpyObj<Store<any>>("Store", ["dispatch"])
},
{
provide: OverlayContainer,
useValue: createSpyObj<OverlayContainer>("OverlayContainer", ["ngOnDestroy"])
},
{
provide: STARK_SESSION_SERVICE,
useValue: new MockStarkSessionService()
},
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader } // needed for ui-router
]
}).compileComponents();
}));

// Inject module dependencies
beforeEach(inject([UIRouter, OverlayContainer], (_router: UIRouter, _overlayContainer: SpyObj<OverlayContainer>) => {
router = _router;
overlayContainer = _overlayContainer;
$state = router.stateService;

overlayContainer.ngOnDestroy.calls.reset();
}));

afterEach(() => {
// IMPORTANT: reset the url after each test,
// otherwise UI-Router will try to find a match of the current url and navigate to it!!
router.urlService.url("");
});

describe("session UI states", () => {
describe("starkSessionExpiredState", () => {
it("when navigating to the state, it should destroy the Angular CDK OverlayContainer", fakeAsync(() => {
from($state.go(homeStateNAme))
.pipe(
switchMap((enteredState: StateObject) => {
expect(enteredState).toBeDefined();
expect(enteredState.name).toBe(homeStateNAme);

expect($state.$current.name).toBe(enteredState.name);
expect(overlayContainer.ngOnDestroy).not.toHaveBeenCalled();

return $state.go(starkSessionExpiredStateName);
}),
catchError((error: any) => {
return throwError(`currentState ${error}`);
})
)
.subscribe(
(enteredState: StateObject) => {
expect(enteredState).toBeDefined();
expect(enteredState.name).toBe(starkSessionExpiredStateName);

expect($state.$current.name).toBe(enteredState.name);
expect(overlayContainer.ngOnDestroy).toHaveBeenCalledTimes(1);
},
(error: any) => fail(error)
);

tick();
}));
});

describe("starkSessionLogoutState", () => {
it("when navigating to the state, it should destroy the Angular CDK OverlayContainer", fakeAsync(() => {
from($state.go(homeStateNAme))
.pipe(
switchMap((enteredState: StateObject) => {
expect(enteredState).toBeDefined();
expect(enteredState.name).toBe(homeStateNAme);

expect($state.$current.name).toBe(enteredState.name);
expect(overlayContainer.ngOnDestroy).not.toHaveBeenCalled();

return $state.go(starkSessionLogoutStateName);
}),
catchError((error: any) => {
return throwError(`currentState ${error}`);
})
)
.subscribe(
(enteredState: StateObject) => {
expect(enteredState).toBeDefined();
expect(enteredState.name).toBe(starkSessionLogoutStateName);

expect($state.$current.name).toBe(enteredState.name);
expect(overlayContainer.ngOnDestroy).toHaveBeenCalledTimes(1);
},
(error: any) => fail(error)
);

tick();
}));
});
});
});

0 comments on commit 8d3dee3

Please sign in to comment.