Explicitly test custom appRoutes (#55405)

* Explicitly test custom appRoutes

* Extract common navigation function
This commit is contained in:
Eli Perelman 2020-01-23 09:08:13 -06:00 committed by GitHub
parent a895977aca
commit fe5e470aae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 220 additions and 63 deletions

View file

@ -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'));

View file

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

View file

@ -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,

View file

@ -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

View file

@ -31,12 +31,6 @@ export class CorePluginChromelessPlugin
return renderApp(context, params);
},
});
return {
getGreeting() {
return 'Hello from Plugin Chromeless!';
},
};
}
public start() {}

View file

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

View file

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