Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[toasts] migrate toastNotifications to the new platform #21772

Merged
merged 3 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions src/core/public/core_system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformService } from './legacy_platform';
import { NotificationsService } from './notifications';

const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
function _MockLegacyPlatformService(this: any) {
Expand Down Expand Up @@ -52,6 +53,18 @@ jest.mock('./fatal_errors', () => ({
FatalErrorsService: MockFatalErrorsService,
}));

const mockNotificationStartContract = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I'm experimenting with mocking right now, trying to find the ideal way to mock modules :) There is also another alternative I use, it's not better, just wanted to share:

jest.mock('./notifications');
......
import { NotificationsService } from './notifications';
const MockNotificationsService = NotificationsService as jest.Mock<NotificationsService>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've played with options similar to that, and functions that use generics and require() to do the same thing, but ultimately found this to be relatively elegant. Definitely think we could improve it still.

const MockNotificationsService = jest.fn<NotificationsService>(function _MockNotificationsService(
this: any
) {
this.start = jest.fn().mockReturnValue(mockNotificationStartContract);
this.add = jest.fn();
this.stop = jest.fn();
});
jest.mock('./notifications', () => ({
NotificationsService: MockNotificationsService,
}));

import { CoreSystem } from './core_system';
jest.spyOn(CoreSystem.prototype, 'stop');

Expand All @@ -74,6 +87,8 @@ describe('constructor', () => {

expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
});

it('passes injectedMetadata param to InjectedMetadataService', () => {
Expand All @@ -92,14 +107,12 @@ describe('constructor', () => {
});

it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => {
const rootDomElement = document.createElement('div');
const requireLegacyFiles = { requireLegacyFiles: true } as any;
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;

// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
rootDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
Expand All @@ -112,6 +125,18 @@ describe('constructor', () => {
});
});

it('passes a dom element to NotificationsService', () => {
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
});

expect(MockNotificationsService).toHaveBeenCalledTimes(1);
expect(MockNotificationsService).toHaveBeenCalledWith({
targetDomElement: expect.any(HTMLElement),
});
});

it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
const rootDomElement = document.createElement('div');
const injectedMetadata = { injectedMetadata: true } as any;
Expand Down Expand Up @@ -161,11 +186,11 @@ describe('#start()', () => {
core.start();
}

it('clears the children of the rootDomElement and appends container for legacyPlatform', () => {
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
startCore(root);
expect(root.innerHTML).toBe('<div></div>');
expect(root.innerHTML).toBe('<div></div><div></div>');
});

it('calls injectedMetadata#start()', () => {
Expand All @@ -181,6 +206,13 @@ describe('#start()', () => {
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
});

it('calls notifications#start()', () => {
startCore();
const [mockInstance] = MockNotificationsService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
});
});

describe('LegacyPlatform targetDomElement', () => {
Expand All @@ -207,3 +239,28 @@ describe('LegacyPlatform targetDomElement', () => {
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});

describe('Notifications targetDomElement', () => {
it('only mounts the element when started, before starting the notificationsService', () => {
const rootDomElement = document.createElement('div');
const core = new CoreSystem({
...defaultCoreSystemParams,
rootDomElement,
});

const [notifications] = MockNotificationsService.mock.instances;

let targetDomElementParentInStart: HTMLElement;
(notifications as any).start.mockImplementation(() => {
targetDomElementParentInStart = targetDomElement.parentElement;
});

// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
const [[{ targetDomElement }]] = MockNotificationsService.mock.calls;
expect(targetDomElement).toHaveProperty('parentElement', null);

// starting the core system should mount the targetDomElement as a child of the rootDomElement
core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});
13 changes: 12 additions & 1 deletion src/core/public/core_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import './core.css';
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
import { NotificationsService } from './notifications';

interface Params {
rootDomElement: HTMLElement;
Expand All @@ -39,8 +40,10 @@ export class CoreSystem {
private readonly fatalErrors: FatalErrorsService;
private readonly injectedMetadata: InjectedMetadataService;
private readonly legacyPlatform: LegacyPlatformService;
private readonly notifications: NotificationsService;

private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement: HTMLDivElement;
private readonly legacyPlatformTargetDomElement: HTMLDivElement;

constructor(params: Params) {
Expand All @@ -60,6 +63,11 @@ export class CoreSystem {
},
});

this.notificationsTargetDomElement = document.createElement('div');
this.notifications = new NotificationsService({
targetDomElement: this.notificationsTargetDomElement,
});

this.legacyPlatformTargetDomElement = document.createElement('div');
this.legacyPlatform = new LegacyPlatformService({
targetDomElement: this.legacyPlatformTargetDomElement,
Expand All @@ -73,18 +81,21 @@ export class CoreSystem {
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
this.rootDomElement.appendChild(this.notificationsTargetDomElement);
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);

const notifications = this.notifications.start();
const injectedMetadata = this.injectedMetadata.start();
const fatalErrors = this.fatalErrors.start();
this.legacyPlatform.start({ injectedMetadata, fatalErrors });
this.legacyPlatform.start({ injectedMetadata, fatalErrors, notifications });
} catch (error) {
this.fatalErrors.add(error);
}
}

public stop() {
this.legacyPlatform.stop();
this.notifications.stop();
this.rootDomElement.textContent = '';
}
}
1 change: 0 additions & 1 deletion src/core/public/fatal_errors/fatal_errors_screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* under the License.
*/

// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
import { EuiCallOut } from '@elastic/eui';
import testSubjSelector from '@kbn/test-subj-selector';
import { mount, shallow } from 'enzyme';
Expand Down
3 changes: 0 additions & 3 deletions src/core/public/fatal_errors/fatal_errors_screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
import {
EuiButton,
EuiButtonEmpty,
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
EuiCallOut,
// @ts-ignore EuiCodeBlock not available until we upgrade to EUI 3.1.0
EuiCodeBlock,
// @ts-ignore EuiEmptyPrompt not available until we upgrade to EUI 3.1.0
EuiEmptyPrompt,
EuiPage,
EuiPageBody,
Expand Down
34 changes: 34 additions & 0 deletions src/core/public/legacy_platform/legacy_platform_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,20 @@ jest.mock('ui/notify/fatal_error', () => {
};
});

const mockNotifyToastsInit = jest.fn();
jest.mock('ui/notify/toasts', () => {
mockLoadOrder.push('ui/notify/toasts');
return {
__newPlatformInit__: mockNotifyToastsInit,
};
});

import { LegacyPlatformService } from './legacy_platform_service';

const fatalErrorsStartContract = {} as any;
const notificationsStartContract = {
toasts: {},
} as any;

const injectedMetadataStartContract = {
getLegacyMetadata: jest.fn(),
Expand Down Expand Up @@ -88,6 +99,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
Expand All @@ -102,12 +114,28 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockFatalErrorInit).toHaveBeenCalledTimes(1);
expect(mockFatalErrorInit).toHaveBeenCalledWith(fatalErrorsStartContract);
});

it('passes toasts service to ui/notify/toasts', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});

legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockNotifyToastsInit).toHaveBeenCalledTimes(1);
expect(mockNotifyToastsInit).toHaveBeenCalledWith(notificationsStartContract.toasts);
});

describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
Expand All @@ -117,6 +145,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
Expand All @@ -134,6 +163,7 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
Expand All @@ -155,11 +185,13 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockLoadOrder).toEqual([
'ui/metadata',
'ui/notify/fatal_error',
'ui/notify/toasts',
'ui/chrome',
'legacy files',
]);
Expand All @@ -178,11 +210,13 @@ describe('#start()', () => {
legacyPlatform.start({
fatalErrors: fatalErrorsStartContract,
injectedMetadata: injectedMetadataStartContract,
notifications: notificationsStartContract,
});

expect(mockLoadOrder).toEqual([
'ui/metadata',
'ui/notify/fatal_error',
'ui/notify/toasts',
'ui/test_harness',
'legacy files',
]);
Expand Down
5 changes: 4 additions & 1 deletion src/core/public/legacy_platform/legacy_platform_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import angular from 'angular';
import { FatalErrorsStartContract } from '../fatal_errors';
import { InjectedMetadataStartContract } from '../injected_metadata';
import { NotificationsStartContract } from '../notifications';

interface Deps {
injectedMetadata: InjectedMetadataStartContract;
fatalErrors: FatalErrorsStartContract;
notifications: NotificationsStartContract;
}

export interface LegacyPlatformParams {
Expand All @@ -42,11 +44,12 @@ export interface LegacyPlatformParams {
export class LegacyPlatformService {
constructor(private readonly params: LegacyPlatformParams) {}

public start({ injectedMetadata, fatalErrors }: Deps) {
public start({ injectedMetadata, fatalErrors, notifications }: Deps) {
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
require('ui/notify/toasts').__newPlatformInit__(notifications.toasts);

// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
* under the License.
*/

export { GlobalToastList } from './global_toast_list';
export { toastNotifications } from './toast_notifications';
export { Toast, ToastInput, ToastsStartContract } from './toasts';
export { NotificationsService, NotificationsStartContract } from './notifications_service';
53 changes: 53 additions & 0 deletions src/core/public/notifications/notifications_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ToastsService } from './toasts';

interface Params {
targetDomElement: HTMLElement;
}

export class NotificationsService {
private readonly toasts: ToastsService;

private readonly toastsContainer: HTMLElement;

constructor(private readonly params: Params) {
this.toastsContainer = document.createElement('div');
this.toasts = new ToastsService({
targetDomElement: this.toastsContainer,
});
}

public start() {
this.params.targetDomElement.appendChild(this.toastsContainer);

return {
toasts: this.toasts.start(),
};
}

public stop() {
this.toasts.stop();

this.params.targetDomElement.textContent = '';
}
}

export type NotificationsStartContract = ReturnType<NotificationsService['start']>;
Loading