Skip to content

Commit

Permalink
implements ScopedHistory.block
Browse files Browse the repository at this point in the history
  • Loading branch information
pgayvallet committed Feb 11, 2021
1 parent d1653bc commit 5076baf
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 11 deletions.
15 changes: 13 additions & 2 deletions src/core/public/application/application_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import React from 'react';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators';
import { map, shareReplay, takeUntil, distinctUntilChanged, filter, take } from 'rxjs/operators';
import { createBrowserHistory, History } from 'history';

import { MountPoint } from '../types';
Expand All @@ -31,6 +31,7 @@ import {
NavigateToAppOptions,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
import { getUserConfirmationHandler } from './navigation_confirm';
import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';

interface SetupDeps {
Expand Down Expand Up @@ -92,6 +93,7 @@ export class ApplicationService {
private history?: History<any>;
private navigate?: (url: string, state: unknown, replace: boolean) => void;
private redirectTo?: (url: string) => void;
private overlayStart$ = new Subject<OverlayStart>();

public setup({
http: { basePath },
Expand All @@ -101,7 +103,14 @@ export class ApplicationService {
history,
}: SetupDeps): InternalApplicationSetup {
const basename = basePath.get();
this.history = history || createBrowserHistory({ basename });
this.history =
history ||
createBrowserHistory({
basename,
getUserConfirmation: getUserConfirmationHandler({
overlayPromise: this.overlayStart$.pipe(take(1)).toPromise(),
}),
});

this.navigate = (url, state, replace) => {
// basePath not needed here because `history` is configured with basename
Expand Down Expand Up @@ -173,6 +182,8 @@ export class ApplicationService {
throw new Error('ApplicationService#setup() must be invoked before start.');
}

this.overlayStart$.next(overlays);

const httpLoadingCount$ = new BehaviorSubject(0);
http.addLoadingCountSource(httpLoadingCount$);

Expand Down
96 changes: 96 additions & 0 deletions src/core/public/application/navigation_confirm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { OverlayStart } from '../overlays';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { getUserConfirmationHandler, ConfirmHandler } from './navigation_confirm';

const nextTick = () => new Promise((resolve) => setImmediate(resolve));

describe('getUserConfirmationHandler', () => {
let overlayStart: ReturnType<typeof overlayServiceMock.createStartContract>;
let overlayPromise: Promise<OverlayStart>;
let resolvePromise: Function;
let rejectPromise: Function;
let fallbackHandler: jest.MockedFunction<ConfirmHandler>;
let handler: ConfirmHandler;

beforeEach(() => {
overlayStart = overlayServiceMock.createStartContract();
overlayPromise = new Promise((resolve, reject) => {
resolvePromise = () => resolve(overlayStart);
rejectPromise = () => reject('some error');
});
fallbackHandler = jest.fn().mockImplementation((message, callback) => {
callback(true);
});

handler = getUserConfirmationHandler({
overlayPromise,
fallbackHandler,
});
});

it('uses the fallback handler if the promise is not resolved yet', () => {
const callback = jest.fn();
handler('foo', callback);

expect(fallbackHandler).toHaveBeenCalledTimes(1);
expect(fallbackHandler).toHaveBeenCalledWith('foo', callback);
});

it('calls the callback with the value returned by the fallback handler', async () => {
const callback = jest.fn();
handler('foo', callback);

expect(fallbackHandler).toHaveBeenCalledTimes(1);
expect(fallbackHandler).toHaveBeenCalledWith('foo', callback);

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(true);
});

it('uses the overlay handler once the promise is resolved', async () => {
resolvePromise();
await nextTick();

const callback = jest.fn();
handler('foo', callback);

expect(fallbackHandler).not.toHaveBeenCalled();

expect(overlayStart.openConfirm).toHaveBeenCalledTimes(1);
expect(overlayStart.openConfirm).toHaveBeenCalledWith('foo', expect.any(Object));
});

it('calls the callback with the value returned by `openConfirm`', async () => {
overlayStart.openConfirm.mockResolvedValue(true);

resolvePromise();
await nextTick();

const callback = jest.fn();
handler('foo', callback);

await nextTick();

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(true);
});

it('uses the fallback handler if the promise rejects', async () => {
rejectPromise();
await nextTick();

const callback = jest.fn();
handler('foo', callback);

expect(fallbackHandler).toHaveBeenCalledTimes(1);
expect(overlayStart.openConfirm).not.toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions src/core/public/application/navigation_confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { OverlayStart } from 'kibana/public';

export type ConfirmHandlerCallback = (result: boolean) => void;
export type ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => void;

interface GetUserConfirmationHandlerParams {
overlayPromise: Promise<OverlayStart>;
fallbackHandler?: ConfirmHandler;
}

export const getUserConfirmationHandler = ({
overlayPromise,
fallbackHandler = windowConfirm,
}: GetUserConfirmationHandlerParams): ConfirmHandler => {
let overlayConfirm: ConfirmHandler;

overlayPromise.then(
(overlay) => {
overlayConfirm = getOverlayConfirmHandler(overlay);
},
() => {
// should never append, but even if it does, we don't need to do anything,
// and will just use the default window confirm instead
}
);

return (message: string, callback: ConfirmHandlerCallback) => {
if (overlayConfirm) {
overlayConfirm(message, callback);
} else {
fallbackHandler(message, callback);
}
};
};

const windowConfirm: ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => {
const confirmed = window.confirm(message);
callback(confirmed);
};

const getOverlayConfirmHandler = (overlay: OverlayStart): ConfirmHandler => {
return (message: string, callback: ConfirmHandlerCallback) => {
overlay.openConfirm(message, { title: ' ' }).then(
(confirmed) => {
callback(confirmed);
},
() => {
callback(false);
}
);
};
};
152 changes: 151 additions & 1 deletion src/core/public/application/scoped_history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import { ScopedHistory } from './scoped_history';
import { createMemoryHistory } from 'history';
import { createMemoryHistory, History } from 'history';
import type { ConfirmHandler } from './navigation_confirm';

describe('ScopedHistory', () => {
describe('construction', () => {
Expand Down Expand Up @@ -336,4 +337,153 @@ describe('ScopedHistory', () => {
expect(gh.length).toBe(4);
});
});

describe('block', () => {
let gh: History;
let h: ScopedHistory;

const initHistory = ({
initialPath = '/app/wow',
scopedHistoryPath = '/app/wow',
confirmHandler,
}: {
initialPath?: string;
scopedHistoryPath?: string;
confirmHandler?: ConfirmHandler;
} = {}) => {
gh = createMemoryHistory({
getUserConfirmation: confirmHandler,
});
gh.push(initialPath);
h = new ScopedHistory(gh, scopedHistoryPath);
};

it('calls block on the global history', () => {
initHistory();

const blockSpy = jest.spyOn(gh, 'block');
h.block('confirm');

expect(blockSpy).toHaveBeenCalledTimes(1);
expect(blockSpy).toHaveBeenCalledWith('confirm');
});

it('returns a wrapped unregister function', () => {
initHistory();

const blockSpy = jest.spyOn(gh, 'block');
const unregister = jest.fn();
blockSpy.mockReturnValue(unregister);

const wrapperUnregister = h.block('confirm');

expect(unregister).not.toHaveBeenCalled();

wrapperUnregister();

expect(unregister).toHaveBeenCalledTimes(1);
});

it('calls the block handler when navigating to another app', () => {
initHistory();

const blockHandler = jest.fn().mockReturnValue(true);

h.block(blockHandler);

gh.push('/app/other');

expect(blockHandler).toHaveBeenCalledTimes(1);
expect(gh.location.pathname).toEqual('/app/other');
});

it('calls the block handler when navigating inside the current app', () => {
initHistory();

const blockHandler = jest.fn().mockReturnValue(true);

h.block(blockHandler);

gh.push('/app/wow/another-page');

expect(blockHandler).toHaveBeenCalledTimes(1);
expect(gh.location.pathname).toEqual('/app/wow/another-page');
});

it('can block the navigation', () => {
initHistory();

const blockHandler = jest.fn().mockReturnValue(false);

h.block(blockHandler);

gh.push('/app/other');

expect(blockHandler).toHaveBeenCalledTimes(1);
expect(gh.location.pathname).toEqual('/app/wow');
});

it('no longer blocks the navigation when unregistered', () => {
initHistory();

const blockHandler = jest.fn().mockReturnValue(false);

const unregister = h.block(blockHandler);

gh.push('/app/other');

expect(gh.location.pathname).toEqual('/app/wow');

unregister();

gh.push('/app/other');

expect(gh.location.pathname).toEqual('/app/other');
});

it('throws if the history is no longer active', () => {
initHistory();

gh.push('/app/other');

expect(() => h.block()).toThrowErrorMatchingInlineSnapshot(
`"ScopedHistory instance has fell out of navigation scope for basePath: /app/wow"`
);
});

it('unregisters the block handler when the history is no longer active', () => {
initHistory();

const blockSpy = jest.spyOn(gh, 'block');
const unregister = jest.fn();
blockSpy.mockReturnValue(unregister);

h.block('confirm');

expect(unregister).not.toHaveBeenCalled();

gh.push('/app/other');

expect(unregister).toHaveBeenCalledTimes(1);
});

it('calls the defined global history confirm handler', () => {
const confirmHandler: jest.MockedFunction<ConfirmHandler> = jest
.fn()
.mockImplementation((message, callback) => {
callback(true);
});

initHistory({
confirmHandler,
});

h.block('are you sure');

gh.push('/app/other');

expect(confirmHandler).toHaveBeenCalledTimes(1);
expect(gh.location.pathname).toEqual('/app/other');
});
});
});
Loading

0 comments on commit 5076baf

Please sign in to comment.