basename: /app/app2
html:
App 2
@@ -99,6 +100,82 @@ describe('AppContainer', () => {
`);
});
+ it('can navigate between standard application and one with custom appRoute', async () => {
+ const standardApp = mounters.get('app1')!;
+ const chromelessApp = mounters.get('app3')!;
+ let dom = await navigate('/app/app1');
+
+ expect(standardApp.mounter.mount).toHaveBeenCalled();
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /app/app1
+ html: App 1
+
"
+ `);
+
+ const standardAppUnmount = await getUnmounter(standardApp);
+ dom = await navigate('/chromeless-a/path');
+
+ expect(standardAppUnmount).toHaveBeenCalled();
+ expect(chromelessApp.mounter.mount).toHaveBeenCalled();
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /chromeless-a/path
+ html:
Chromeless A
+
"
+ `);
+
+ const chromelessAppUnmount = await getUnmounter(standardApp);
+ dom = await navigate('/app/app1');
+
+ expect(chromelessAppUnmount).toHaveBeenCalled();
+ expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2);
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /app/app1
+ html: App 1
+
"
+ `);
+ });
+
+ it('can navigate between two applications with custom appRoutes', async () => {
+ const chromelessAppA = mounters.get('app3')!;
+ const chromelessAppB = mounters.get('app4')!;
+ let dom = await navigate('/chromeless-a/path');
+
+ expect(chromelessAppA.mounter.mount).toHaveBeenCalled();
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /chromeless-a/path
+ html:
Chromeless A
+
"
+ `);
+
+ const chromelessAppAUnmount = await getUnmounter(chromelessAppA);
+ dom = await navigate('/chromeless-b/path');
+
+ expect(chromelessAppAUnmount).toHaveBeenCalled();
+ expect(chromelessAppB.mounter.mount).toHaveBeenCalled();
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /chromeless-b/path
+ html:
Chromeless B
+
"
+ `);
+
+ const chromelessAppBUnmount = await getUnmounter(chromelessAppB);
+ dom = await navigate('/chromeless-a/path');
+
+ expect(chromelessAppBUnmount).toHaveBeenCalled();
+ expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2);
+ expect(dom?.html()).toMatchInlineSnapshot(`
+ "
+ basename: /chromeless-a/path
+ html:
Chromeless A
+
"
+ `);
+ });
+
it('should not mount when partial route path matches', async () => {
mounters.set(...createAppMounter('spaces', '
Custom Space
', '/spaces/fake-login'));
mounters.set(...createAppMounter('login', '
Login Page
', '/fake-login'));
diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx
index 6367d1fa12697..4f34438fc822a 100644
--- a/src/core/public/application/integration_tests/utils.tsx
+++ b/src/core/public/application/integration_tests/utils.tsx
@@ -23,7 +23,7 @@ import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { App, LegacyApp, AppMountParameters } from '../types';
-import { MockedMounter, MockedMounterTuple } from '../test_types';
+import { EitherApp, MockedMounter, MockedMounterTuple, Mountable } from '../test_types';
type Dom = ReturnType
| null;
type Renderer = () => Dom | Promise;
@@ -80,3 +80,7 @@ export const createLegacyAppMounter = (
unmount: jest.fn(),
},
];
+
+export function getUnmounter(app: Mountable) {
+ return app.mounter.mount.mock.results[0].value;
+}
diff --git a/src/core/public/application/test_types.ts b/src/core/public/application/test_types.ts
index 3d992cb950eb4..b822597e510cb 100644
--- a/src/core/public/application/test_types.ts
+++ b/src/core/public/application/test_types.ts
@@ -26,18 +26,19 @@ export type ApplicationServiceContract = PublicMethodsOf;
export type EitherApp = App | LegacyApp;
/** @internal */
export type MockedUnmount = jest.Mocked;
+
+/** @internal */
+export interface Mountable {
+ mounter: MockedMounter;
+ unmount: MockedUnmount;
+}
+
/** @internal */
export type MockedMounter = jest.Mocked>>;
/** @internal */
-export type MockedMounterTuple = [
- string,
- { mounter: MockedMounter; unmount: MockedUnmount }
-];
+export type MockedMounterTuple = [string, Mountable];
/** @internal */
-export type MockedMounterMap = Map<
- string,
- { mounter: MockedMounter; unmount: MockedUnmount }
->;
+export type MockedMounterMap = Map>;
/** @internal */
export type MockLifecycle<
T extends keyof ApplicationService,
diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts
index 59498bd8413a7..ae68be3ed7987 100644
--- a/test/functional/services/browser.ts
+++ b/test/functional/services/browser.ts
@@ -311,11 +311,23 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
/**
* Moves forwards in the browser history.
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#forward
+ *
+ * @return {Promise}
*/
public async goForward() {
await driver.navigate().forward();
}
+ /**
+ * Navigates to a URL via the browser history.
+ * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#to
+ *
+ * @return {Promise}
+ */
+ public async navigateTo(url: string) {
+ await driver.navigate().to(url);
+ }
+
/**
* Sends a sequance of keyboard keys. For each key, this will record a pair of keyDown and keyUp actions
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#sendKeys
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx
index 03870410fb334..7ce348fa2111e 100644
--- a/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx
@@ -31,12 +31,6 @@ export class CorePluginChromelessPlugin
return renderApp(context, params);
},
});
-
- return {
- getGreeting() {
- return 'Hello from Plugin Chromeless!';
- },
- };
}
public start() {}
diff --git a/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx b/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx
index 6e80b56953ca0..a4925cdb8f8df 100644
--- a/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx
+++ b/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx
@@ -26,13 +26,24 @@ export class RenderingPlugin implements Plugin {
core.application.register({
id: 'rendering',
title: 'Rendering',
- appRoute: '/render',
+ appRoute: '/render/core',
async mount(context, { element }) {
render(rendering service
, element);
return () => unmountComponentAtNode(element);
},
});
+
+ core.application.register({
+ id: 'custom-app-route',
+ title: 'Custom App Route',
+ appRoute: '/custom/appRoute',
+ async mount(context, { element }) {
+ render(Custom App Route
, element);
+
+ return () => unmountComponentAtNode(element);
+ },
+ });
}
public start() {}
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index d0025c82a7ba5..e30d81eedd985 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -22,43 +22,55 @@ import expect from '@kbn/expect';
import '../../plugins/core_provider_plugin/types';
import { PluginFunctionalProviderContext } from '../../services';
+declare global {
+ interface Window {
+ /**
+ * We use this global variable to track page history changes to ensure that
+ * navigation is done without causing a full page reload.
+ */
+ __RENDERING_SESSION__: string[];
+ }
+}
+
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
+ const appsMenu = getService('appsMenu');
const browser = getService('browser');
const find = getService('find');
const testSubjects = getService('testSubjects');
- function navigate(path: string) {
- return browser.get(`${PageObjects.common.getHostPort()}${path}`);
- }
-
- function getLegacyMode() {
+ const navigateTo = (path: string) =>
+ browser.navigateTo(`${PageObjects.common.getHostPort()}${path}`);
+ const navigateToApp = async (title: string) => {
+ await appsMenu.clickLink(title);
return browser.execute(() => {
+ if (!('__RENDERING_SESSION__' in window)) {
+ window.__RENDERING_SESSION__ = [];
+ }
+
+ window.__RENDERING_SESSION__.push(window.location.pathname);
+ });
+ };
+ const getLegacyMode = () =>
+ browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!)
.legacyMode;
});
- }
-
- function getUserSettings() {
- return browser.execute(() => {
+ const getUserSettings = () =>
+ browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!)
.legacyMetadata.uiSettings.user;
});
- }
-
- async function init() {
- const loading = await testSubjects.find('kbnLoadingMessage', 5000);
-
- return () => find.waitForElementStale(loading);
- }
+ const exists = (selector: string) => testSubjects.exists(selector, { timeout: 2000 });
+ const findLoadingMessage = () => testSubjects.find('kbnLoadingMessage', 5000);
describe('rendering service', () => {
it('renders "core" application', async () => {
- await navigate('/render/core');
+ await navigateTo('/render/core');
- const [loaded, legacyMode, userSettings] = await Promise.all([
- init(),
+ const [loadingMessage, legacyMode, userSettings] = await Promise.all([
+ findLoadingMessage(),
getLegacyMode(),
getUserSettings(),
]);
@@ -66,16 +78,16 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(legacyMode).to.be(false);
expect(userSettings).to.not.be.empty();
- await loaded();
+ await find.waitForElementStale(loadingMessage);
- expect(await testSubjects.exists('renderingHeader')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(true);
});
it('renders "core" application without user settings', async () => {
- await navigate('/render/core?includeUserSettings=false');
+ await navigateTo('/render/core?includeUserSettings=false');
- const [loaded, legacyMode, userSettings] = await Promise.all([
- init(),
+ const [loadingMessage, legacyMode, userSettings] = await Promise.all([
+ findLoadingMessage(),
getLegacyMode(),
getUserSettings(),
]);
@@ -83,16 +95,16 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(legacyMode).to.be(false);
expect(userSettings).to.be.empty();
- await loaded();
+ await find.waitForElementStale(loadingMessage);
- expect(await testSubjects.exists('renderingHeader')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(true);
});
it('renders "legacy" application', async () => {
- await navigate('/render/core_plugin_legacy');
+ await navigateTo('/render/core_plugin_legacy');
- const [loaded, legacyMode, userSettings] = await Promise.all([
- init(),
+ const [loadingMessage, legacyMode, userSettings] = await Promise.all([
+ findLoadingMessage(),
getLegacyMode(),
getUserSettings(),
]);
@@ -100,17 +112,17 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(legacyMode).to.be(true);
expect(userSettings).to.not.be.empty();
- await loaded();
+ await find.waitForElementStale(loadingMessage);
- expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true);
- expect(await testSubjects.exists('renderingHeader')).to.be(false);
+ expect(await exists('coreLegacyCompatH1')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(false);
});
it('renders "legacy" application without user settings', async () => {
- await navigate('/render/core_plugin_legacy?includeUserSettings=false');
+ await navigateTo('/render/core_plugin_legacy?includeUserSettings=false');
- const [loaded, legacyMode, userSettings] = await Promise.all([
- init(),
+ const [loadingMessage, legacyMode, userSettings] = await Promise.all([
+ findLoadingMessage(),
getLegacyMode(),
getUserSettings(),
]);
@@ -118,10 +130,56 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(legacyMode).to.be(true);
expect(userSettings).to.be.empty();
- await loaded();
+ await find.waitForElementStale(loadingMessage);
+
+ expect(await exists('coreLegacyCompatH1')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(false);
+ });
+
+ it('navigates between standard application and one with custom appRoute', async () => {
+ await navigateTo('/');
+ await find.waitForElementStale(await findLoadingMessage());
+
+ await navigateToApp('App Status');
+ expect(await exists('appStatusApp')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(false);
+
+ await navigateToApp('Rendering');
+ expect(await exists('appStatusApp')).to.be(false);
+ expect(await exists('renderingHeader')).to.be(true);
+
+ await navigateToApp('App Status');
+ expect(await exists('appStatusApp')).to.be(true);
+ expect(await exists('renderingHeader')).to.be(false);
+
+ expect(
+ await browser.execute(() => {
+ return window.__RENDERING_SESSION__;
+ })
+ ).to.eql(['/app/app_status', '/render/core', '/app/app_status']);
+ });
+
+ it('navigates between applications with custom appRoutes', async () => {
+ await navigateTo('/');
+ await find.waitForElementStale(await findLoadingMessage());
+
+ await navigateToApp('Rendering');
+ expect(await exists('renderingHeader')).to.be(true);
+ expect(await exists('customAppRouteHeader')).to.be(false);
+
+ await navigateToApp('Custom App Route');
+ expect(await exists('renderingHeader')).to.be(false);
+ expect(await exists('customAppRouteHeader')).to.be(true);
+
+ await navigateToApp('Rendering');
+ expect(await exists('renderingHeader')).to.be(true);
+ expect(await exists('customAppRouteHeader')).to.be(false);
- expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true);
- expect(await testSubjects.exists('renderingHeader')).to.be(false);
+ expect(
+ await browser.execute(() => {
+ return window.__RENDERING_SESSION__;
+ })
+ ).to.eql(['/render/core', '/custom/appRoute', '/render/core']);
});
});
}