mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Explicitly test custom appRoutes (#55405)
* Explicitly test custom appRoutes * Extract common navigation function
This commit is contained in:
parent
a895977aca
commit
fe5e470aae
7 changed files with 220 additions and 63 deletions
|
@ -23,7 +23,7 @@ import { createMemoryHistory, History, createHashHistory } from 'history';
|
|||
|
||||
import { AppRouter, AppNotFound } from '../ui';
|
||||
import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types';
|
||||
import { createRenderer, createAppMounter, createLegacyAppMounter } from './utils';
|
||||
import { createRenderer, createAppMounter, createLegacyAppMounter, getUnmounter } from './utils';
|
||||
import { AppStatus } from '../types';
|
||||
|
||||
describe('AppContainer', () => {
|
||||
|
@ -36,7 +36,6 @@ describe('AppContainer', () => {
|
|||
history.push(path);
|
||||
return update();
|
||||
};
|
||||
|
||||
const mockMountersToMounters = () =>
|
||||
new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter]));
|
||||
const setAppLeaveHandlerMock = () => undefined;
|
||||
|
@ -58,7 +57,8 @@ describe('AppContainer', () => {
|
|||
createLegacyAppMounter('legacyApp1', jest.fn()),
|
||||
createAppMounter('app2', '<div>App 2</div>'),
|
||||
createLegacyAppMounter('baseApp:legacyApp2', jest.fn()),
|
||||
createAppMounter('app3', '<div>App 3</div>', '/custom/path'),
|
||||
createAppMounter('app3', '<div>Chromeless A</div>', '/chromeless-a/path'),
|
||||
createAppMounter('app4', '<div>Chromeless B</div>', '/chromeless-b/path'),
|
||||
createAppMounter('disabledApp', '<div>Disabled app</div>'),
|
||||
createLegacyAppMounter('disabledLegacyApp', jest.fn()),
|
||||
] as Array<MockedMounterTuple<EitherApp>>);
|
||||
|
@ -75,23 +75,24 @@ describe('AppContainer', () => {
|
|||
});
|
||||
|
||||
it('calls mount handler and returned unmount function when navigating between apps', async () => {
|
||||
const dom1 = await navigate('/app/app1');
|
||||
const app1 = mounters.get('app1')!;
|
||||
const app2 = mounters.get('app2')!;
|
||||
let dom = await navigate('/app/app1');
|
||||
|
||||
expect(app1.mounter.mount).toHaveBeenCalled();
|
||||
expect(dom1?.html()).toMatchInlineSnapshot(`
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /app/app1
|
||||
html: <span>App 1</span>
|
||||
</div></div>"
|
||||
`);
|
||||
|
||||
const app1Unmount = await app1.mounter.mount.mock.results[0].value;
|
||||
const dom2 = await navigate('/app/app2');
|
||||
const app1Unmount = await getUnmounter(app1);
|
||||
dom = await navigate('/app/app2');
|
||||
|
||||
expect(app1Unmount).toHaveBeenCalled();
|
||||
expect(mounters.get('app2')!.mounter.mount).toHaveBeenCalled();
|
||||
expect(dom2?.html()).toMatchInlineSnapshot(`
|
||||
expect(app2.mounter.mount).toHaveBeenCalled();
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /app/app2
|
||||
html: <div>App 2</div>
|
||||
|
@ -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(`
|
||||
"<div><div>
|
||||
basename: /app/app1
|
||||
html: <span>App 1</span>
|
||||
</div></div>"
|
||||
`);
|
||||
|
||||
const standardAppUnmount = await getUnmounter(standardApp);
|
||||
dom = await navigate('/chromeless-a/path');
|
||||
|
||||
expect(standardAppUnmount).toHaveBeenCalled();
|
||||
expect(chromelessApp.mounter.mount).toHaveBeenCalled();
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /chromeless-a/path
|
||||
html: <div>Chromeless A</div>
|
||||
</div></div>"
|
||||
`);
|
||||
|
||||
const chromelessAppUnmount = await getUnmounter(standardApp);
|
||||
dom = await navigate('/app/app1');
|
||||
|
||||
expect(chromelessAppUnmount).toHaveBeenCalled();
|
||||
expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2);
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /app/app1
|
||||
html: <span>App 1</span>
|
||||
</div></div>"
|
||||
`);
|
||||
});
|
||||
|
||||
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(`
|
||||
"<div><div>
|
||||
basename: /chromeless-a/path
|
||||
html: <div>Chromeless A</div>
|
||||
</div></div>"
|
||||
`);
|
||||
|
||||
const chromelessAppAUnmount = await getUnmounter(chromelessAppA);
|
||||
dom = await navigate('/chromeless-b/path');
|
||||
|
||||
expect(chromelessAppAUnmount).toHaveBeenCalled();
|
||||
expect(chromelessAppB.mounter.mount).toHaveBeenCalled();
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /chromeless-b/path
|
||||
html: <div>Chromeless B</div>
|
||||
</div></div>"
|
||||
`);
|
||||
|
||||
const chromelessAppBUnmount = await getUnmounter(chromelessAppB);
|
||||
dom = await navigate('/chromeless-a/path');
|
||||
|
||||
expect(chromelessAppBUnmount).toHaveBeenCalled();
|
||||
expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2);
|
||||
expect(dom?.html()).toMatchInlineSnapshot(`
|
||||
"<div><div>
|
||||
basename: /chromeless-a/path
|
||||
html: <div>Chromeless A</div>
|
||||
</div></div>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not mount when partial route path matches', async () => {
|
||||
mounters.set(...createAppMounter('spaces', '<div>Custom Space</div>', '/spaces/fake-login'));
|
||||
mounters.set(...createAppMounter('login', '<div>Login Page</div>', '/fake-login'));
|
||||
|
|
|
@ -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<typeof mount> | null;
|
||||
type Renderer = () => Dom | Promise<Dom>;
|
||||
|
@ -80,3 +80,7 @@ export const createLegacyAppMounter = (
|
|||
unmount: jest.fn(),
|
||||
},
|
||||
];
|
||||
|
||||
export function getUnmounter(app: Mountable<EitherApp>) {
|
||||
return app.mounter.mount.mock.results[0].value;
|
||||
}
|
||||
|
|
|
@ -26,18 +26,19 @@ export type ApplicationServiceContract = PublicMethodsOf<ApplicationService>;
|
|||
export type EitherApp = App | LegacyApp;
|
||||
/** @internal */
|
||||
export type MockedUnmount = jest.Mocked<AppUnmount>;
|
||||
|
||||
/** @internal */
|
||||
export interface Mountable<T extends EitherApp> {
|
||||
mounter: MockedMounter<T>;
|
||||
unmount: MockedUnmount;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type MockedMounter<T extends EitherApp> = jest.Mocked<Mounter<jest.Mocked<T>>>;
|
||||
/** @internal */
|
||||
export type MockedMounterTuple<T extends EitherApp> = [
|
||||
string,
|
||||
{ mounter: MockedMounter<T>; unmount: MockedUnmount }
|
||||
];
|
||||
export type MockedMounterTuple<T extends EitherApp> = [string, Mountable<T>];
|
||||
/** @internal */
|
||||
export type MockedMounterMap<T extends EitherApp> = Map<
|
||||
string,
|
||||
{ mounter: MockedMounter<T>; unmount: MockedUnmount }
|
||||
>;
|
||||
export type MockedMounterMap<T extends EitherApp> = Map<string, Mountable<T>>;
|
||||
/** @internal */
|
||||
export type MockLifecycle<
|
||||
T extends keyof ApplicationService,
|
||||
|
|
|
@ -313,11 +313,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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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
|
||||
|
|
|
@ -31,12 +31,6 @@ export class CorePluginChromelessPlugin
|
|||
return renderApp(context, params);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
getGreeting() {
|
||||
return 'Hello from Plugin Chromeless!';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -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(<h1 data-test-subj="renderingHeader">rendering service</h1>, element);
|
||||
|
||||
return () => unmountComponentAtNode(element);
|
||||
},
|
||||
});
|
||||
|
||||
core.application.register({
|
||||
id: 'custom-app-route',
|
||||
title: 'Custom App Route',
|
||||
appRoute: '/custom/appRoute',
|
||||
async mount(context, { element }) {
|
||||
render(<h1 data-test-subj="customAppRouteHeader">Custom App Route</h1>, element);
|
||||
|
||||
return () => unmountComponentAtNode(element);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -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 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('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 browser.execute(() => {
|
||||
return window.__RENDERING_SESSION__;
|
||||
})
|
||||
).to.eql(['/render/core', '/custom/appRoute', '/render/core']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue