Implement ScopedHistory.block (#91099) (#92150)

* implements ScopedHistory.block

* add FTR tests

* fix test plugin id

* update generated doc

* deprecates AppMountParameters.onAppLeave

* typo fix

* add new FTR test

* fix added test
This commit is contained in:
Pierre Gayvallet 2021-02-22 14:59:14 +01:00 committed by GitHub
parent 579de122fa
commit 3c6bdca1c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 598 additions and 19 deletions

View file

@ -4,6 +4,11 @@
## AppLeaveHandler type
> Warning: This API is now obsolete.
>
> [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) has been deprecated in favor of [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md)
>
A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return `confirm` to to prompt a message to the user before leaving the page, or `default` to keep the default behavior (doing nothing).
See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples.

View file

@ -4,6 +4,11 @@
## AppMountParameters.onAppLeave property
> Warning: This API is now obsolete.
>
> [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) should be used instead.
>
A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.
This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url.

View file

@ -4,15 +4,10 @@
## ScopedHistory.block property
Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md)<!-- -->.
Add a block prompt requesting user confirmation when navigating away from the current page.
<b>Signature:</b>
```typescript
block: (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback;
```
## Remarks
We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers a modal when possible, falling back to a confirm dialog box in the beforeunload case.

View file

@ -27,7 +27,7 @@ export declare class ScopedHistory<HistoryLocationState = unknown> implements Hi
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [action](./kibana-plugin-core-public.scopedhistory.action.md) | | <code>Action</code> | The last action dispatched on the history stack. |
| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | <code>(prompt?: string &#124; boolean &#124; History.TransitionPromptHook&lt;HistoryLocationState&gt; &#124; undefined) =&gt; UnregisterCallback</code> | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md)<!-- -->. |
| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | <code>(prompt?: string &#124; boolean &#124; History.TransitionPromptHook&lt;HistoryLocationState&gt; &#124; undefined) =&gt; UnregisterCallback</code> | Add a block prompt requesting user confirmation when navigating away from the current page. |
| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | <code>(location: LocationDescriptorObject&lt;HistoryLocationState&gt;, { prependBasePath }?: {</code><br/><code> prependBasePath?: boolean &#124; undefined;</code><br/><code> }) =&gt; Href</code> | Creates an href (string) to the location. If <code>prependBasePath</code> is true (default), it will prepend the location's path with the scoped history basePath. |
| [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <code>&lt;SubHistoryLocationState = unknown&gt;(basePath: string) =&gt; ScopedHistory&lt;SubHistoryLocationState&gt;</code> | Creates a <code>ScopedHistory</code> for a subpath of this <code>ScopedHistory</code>. Useful for applications that may have sub-apps that do not need access to the containing application's history. |
| [go](./kibana-plugin-core-public.scopedhistory.go.md) | | <code>(n: number) =&gt; void</code> | Send the user forward or backwards in the history stack. |

View file

@ -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';
@ -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 {
@ -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 },
@ -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
@ -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$);

View file

@ -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();
});
});

View file

@ -0,0 +1,62 @@
/*
* 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: ' ', 'data-test-subj': 'navigationBlockConfirmModal' })
.then(
(confirmed) => {
callback(confirmed);
},
() => {
callback(false);
}
);
};
};

View file

@ -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', () => {
@ -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');
});
});
});

View file

@ -51,6 +51,10 @@ export class ScopedHistory<HistoryLocationState = unknown>
* The key of the current position of the window in the history stack.
*/
private currentLocationKeyIndex: number = 0;
/**
* Array of the current {@link block} unregister callbacks
*/
private blockUnregisterCallbacks: Set<UnregisterCallback> = new Set();
constructor(private readonly parentHistory: History, private readonly basePath: string) {
const parentPath = this.parentHistory.location.pathname;
@ -176,18 +180,20 @@ export class ScopedHistory<HistoryLocationState = unknown>
};
/**
* Not supported. Use {@link AppMountParameters.onAppLeave}.
*
* @remarks
* We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers
* a modal when possible, falling back to a confirm dialog box in the beforeunload case.
* Add a block prompt requesting user confirmation when navigating away from the current page.
*/
public block = (
prompt?: boolean | string | TransitionPromptHook<HistoryLocationState>
): UnregisterCallback => {
throw new Error(
`history.block is not supported. Please use the AppMountParameters.onAppLeave API.`
);
this.verifyActive();
const unregisterCallback = this.parentHistory.block(prompt);
this.blockUnregisterCallbacks.add(unregisterCallback);
return () => {
this.blockUnregisterCallbacks.delete(unregisterCallback);
unregisterCallback();
};
};
/**
@ -290,6 +296,12 @@ export class ScopedHistory<HistoryLocationState = unknown>
if (!location.pathname.startsWith(this.basePath)) {
unlisten();
this.isActive = false;
for (const unregisterBlock of this.blockUnregisterCallbacks) {
unregisterBlock();
}
this.blockUnregisterCallbacks.clear();
return;
}

View file

@ -478,6 +478,8 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
* return renderApp({ element, history });
* }
* ```
*
* @deprecated {@link ScopedHistory.block} should be used instead.
*/
onAppLeave: (handler: AppLeaveHandler) => void;
@ -523,6 +525,7 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
* See {@link AppMountParameters} for detailed usage examples.
*
* @public
* @deprecated {@link AppMountParameters.onAppLeave} has been deprecated in favor of {@link ScopedHistory.block}
*/
export type AppLeaveHandler = (
factory: AppLeaveActionFactory,
@ -590,6 +593,7 @@ export interface AppLeaveActionFactory {
* so we can show to the user the right UX for him to saved his/her/their changes
*/
confirm(text: string, title?: string, callback?: () => void): AppLeaveConfirmAction;
/**
* Returns a default action, resulting on executing the default behavior when
* the user tries to leave an application

View file

@ -116,7 +116,7 @@ export interface AppLeaveDefaultAction {
// Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts
//
// @public
// @public @deprecated
export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction;
// @public (undocumented)
@ -153,6 +153,7 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
appBasePath: string;
element: HTMLElement;
history: ScopedHistory<HistoryLocationState>;
// @deprecated
onAppLeave: (handler: AppLeaveHandler) => void;
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
}

View file

@ -0,0 +1,8 @@
{
"id": "coreHistoryBlock",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredBundles": ["kibanaReact"]
}

View file

@ -0,0 +1,14 @@
{
"name": "core_history_block",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/core_history_block",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "SSPL-1.0 OR Elastic License 2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
}
}

View file

@ -0,0 +1,83 @@
/*
* 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 React from 'react';
import ReactDOM from 'react-dom';
import { Router, Switch, Route, Prompt } from 'react-router-dom';
import type { AppMountParameters, IBasePath, ApplicationStart } from 'kibana/public';
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
const HomePage = ({
basePath,
application,
}: {
basePath: IBasePath;
application: ApplicationStart;
}) => (
<div data-test-subj="page-home">
<Prompt message="Unsaved changes, are you sure you wanna leave?" />
<RedirectAppLinks application={application}>
<h1>HOME PAGE</h1>
<br /> <br />
<a data-test-subj="applink-intra-test" href={basePath.prepend(`/app/core_history_block/foo`)}>
Link to foo on the same app
</a>
<br /> <br />
<a data-test-subj="applink-external-test" href={basePath.prepend(`/app/home`)}>
Link to the home app
</a>
</RedirectAppLinks>
</div>
);
const FooPage = ({
basePath,
application,
}: {
basePath: IBasePath;
application: ApplicationStart;
}) => (
<div data-test-subj="page-home">
<RedirectAppLinks application={application}>
<h1>FOO PAGE</h1>
<br /> <br />
<a data-test-subj="applink-intra-test" href={basePath.prepend(`/app/core_history_block`)}>
Link to home on the same app
</a>
<br /> <br />
<a data-test-subj="applink-nested-test" href={basePath.prepend(`/app/home`)}>
Link to the home app
</a>
</RedirectAppLinks>
</div>
);
interface AppOptions {
basePath: IBasePath;
application: ApplicationStart;
}
export const renderApp = (
{ basePath, application }: AppOptions,
{ element, history }: AppMountParameters
) => {
ReactDOM.render(
<Router history={history}>
<Switch>
<Route path="/" exact={true}>
<HomePage basePath={basePath} application={application} />
</Route>
<Route path="/foo" exact={true}>
<FooPage basePath={basePath} application={application} />
</Route>
</Switch>
</Router>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,13 @@
/*
* 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 { PluginInitializer } from 'kibana/public';
import { CoreAppLinkPlugin, CoreAppLinkPluginSetup, CoreAppLinkPluginStart } from './plugin';
export const plugin: PluginInitializer<CoreAppLinkPluginSetup, CoreAppLinkPluginStart> = () =>
new CoreAppLinkPlugin();

View file

@ -0,0 +1,40 @@
/*
* 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 { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
import { renderApp } from './app';
export class CoreAppLinkPlugin implements Plugin<CoreAppLinkPluginSetup, CoreAppLinkPluginStart> {
public setup(core: CoreSetup, deps: {}) {
core.application.register({
id: 'core_history_block',
title: 'History block test',
mount: async (params: AppMountParameters) => {
const [{ application }] = await core.getStartServices();
return renderApp(
{
basePath: core.http.basePath,
application,
},
params
);
},
});
return {};
}
public start() {
return {};
}
public stop() {}
}
export type CoreAppLinkPluginSetup = ReturnType<CoreAppLinkPlugin['setup']>;
export type CoreAppLinkPluginStart = ReturnType<CoreAppLinkPlugin['start']>;

View file

@ -0,0 +1,13 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"],
"exclude": [],
"references": [
{ "path": "../../../../src/core/tsconfig.json" },
{ "path": "../../../../src/plugins/kibana_react/tsconfig.json" }
]
}

View file

@ -0,0 +1,66 @@
/*
* 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 expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../services';
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
const testSubjects = getService('testSubjects');
describe('application using `ScopedHistory.block`', () => {
beforeEach(async () => {
await PageObjects.common.navigateToApp('core_history_block');
});
describe('when navigating to another app', () => {
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
await testSubjects.click('applink-external-test');
await testSubjects.existOrFail('navigationBlockConfirmModal');
await PageObjects.common.clickCancelOnModal(false);
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
});
it('allows navigation if user click confirm on the confirmation dialog', async () => {
await testSubjects.click('applink-external-test');
await testSubjects.existOrFail('navigationBlockConfirmModal');
await PageObjects.common.clickConfirmOnModal();
expect(await browser.getCurrentUrl()).to.contain('/app/home');
});
});
describe('when navigating to the same app', () => {
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
await testSubjects.click('applink-intra-test');
await testSubjects.existOrFail('navigationBlockConfirmModal');
await PageObjects.common.clickCancelOnModal(false);
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
expect(await browser.getCurrentUrl()).not.to.contain('/foo');
});
it('allows navigation if user click confirm on the confirmation dialog', async () => {
await testSubjects.click('applink-intra-test');
await testSubjects.existOrFail('navigationBlockConfirmModal');
await PageObjects.common.clickConfirmOnModal();
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo');
});
it('allows navigating back without prompt once the block handler has been disposed', async () => {
await testSubjects.click('applink-intra-test');
await PageObjects.common.clickConfirmOnModal();
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo');
await testSubjects.click('applink-intra-test');
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
expect(await browser.getCurrentUrl()).not.to.contain('/foo');
});
});
});
}

View file

@ -20,5 +20,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
loadTestFile(require.resolve('./application_status'));
loadTestFile(require.resolve('./rendering'));
loadTestFile(require.resolve('./chrome_help_menu_links'));
loadTestFile(require.resolve('./history_block'));
});
}