Release 1.3.0

This commit is contained in:
2021-08-26 12:33:15 +02:00
commit 9bb72f0207
1148 changed files with 92133 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
require('regenerator-runtime/runtime');
require('whatwg-fetch');
window.crypto = {
getRandomValues: require('polyfill-crypto.getrandomvalues'),
};
require('tests/__setup__/enzyme.setup.js');
require('tests/__setup__/jasmine.setup.js');
let testsContext = require.context('.', true, /\.spec\.js$/);
testsContext.keys().forEach(testsContext);

View File

@@ -0,0 +1,51 @@
import {FakeWidget, FakeService} from 'tests/__fixtures__/services';
export const DashboardsFactory = () => {
return [
{
id: 'testing',
name: 'Testing',
services: [
new FakeService({
instance: 'fake_instance',
widgetComponent: FakeWidget,
characteristics: {
spam: true,
},
layout: {
x: 0,
y: 0,
w: 1,
h: 1,
},
}),
new FakeService({
instance: 'other_fake_instance',
widgetComponent: FakeWidget,
characteristics: {
spam: true,
},
layout: {
x: 0,
y: 1,
w: 1,
h: 1,
},
}),
],
},
];
};
export const DashboardsJSONFactory = (dashboards) => {
dashboards = dashboards || DashboardsFactory();
return dashboards.map((dashboard) => {
return {
...dashboard,
services: dashboard.services.map((service) => {
return service.toJSON();
}),
};
});
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import {BaseService} from 'src/lib/services';
export const FakeWidget = (props) => { // eslint-disable-line no-unused-vars
return <span>FakeWidget</span>;
};
export const FakeWidgetSettingsView = (props) => { // eslint-disable-line no-unused-vars
return <span>FakeWidgetSettingsView</span>;
};
FakeWidget.defaultLayout = {w: 1, h: 1};
FakeWidget.settingsView = FakeWidgetSettingsView;
export class FakeService extends BaseService {
static kind = 'FakeService';
static widget = 'FakeWidget';
}

View File

@@ -0,0 +1,7 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({
adapter: new Adapter(),
disableLifecycleMethods: true,
});

View File

@@ -0,0 +1,22 @@
/* eslint-disable no-unused-vars */
jasmine.getEnv().beforeAll(() => {
jasmine.addMatchers({
toBeUUIDv4: (util, customEqualityTesters) => {
return {
compare: (actual, expected) => {
const reUUIDv4 = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/;
const result = {};
result.pass = reUUIDv4.test(actual);
if (!result.pass) {
result.message = `Expected ${actual} to be a UUIDv4`;
}
return result;
},
};
},
});
});

View File

@@ -0,0 +1,51 @@
import * as Services from 'src/api/services';
import * as RPC from 'src/lib/rpc';
describe('src/api/services', () => {
beforeEach(() => {
spyOn(RPC, 'callMethod').and.resolveTo('ok');
});
describe('start', () => {
it('calls and RPC method to start a service', async () => {
// When
const result = await Services.start('FakeService', 'fake_instance', {
'spam': true,
});
// Then
expect(result).toEqual('ok');
expect(RPC.callMethod).toHaveBeenCalledWith('services.start', [
'FakeService', 'fake_instance', {'spam': true},
]);
});
});
describe('stop', () => {
it('calls and RPC method to stop a service', async () => {
// When
const result = await Services.stop('FakeService', 'fake_instance');
// Then
expect(result).toEqual('ok');
expect(RPC.callMethod).toHaveBeenCalledWith('services.stop', [
'FakeService', 'fake_instance',
]);
});
});
describe('use', () => {
it('calls and RPC method to use a service capability', async () => {
// When
const result = await Services.use(
'FakeService', 'fake_instance', 'testing', ['spam'],
);
// Then
expect(result).toEqual('ok');
expect(RPC.callMethod).toHaveBeenCalledWith('services.use', [
'FakeService', 'fake_instance', 'testing', ['spam'],
]);
});
});
});

View File

@@ -0,0 +1,32 @@
import * as State from 'src/api/state';
import * as RPC from 'src/lib/rpc';
describe('src/api/state', () => {
beforeEach(() => {
spyOn(RPC, 'callMethod').and.resolveTo('ok');
});
describe('get', () => {
it('calls and RPC method to get frontend state', async () => {
// When
const result = await State.get();
// Then
expect(result).toEqual('ok');
expect(RPC.callMethod).toHaveBeenCalledWith('state.get_frontend');
});
});
describe('save', () => {
it('calls and RPC method to save frontend state', async () => {
// When
const result = await State.save('spam');
// Then
expect(result).toEqual('ok');
expect(RPC.callMethod).toHaveBeenCalledWith(
'state.save_frontend', ['spam']
);
});
});
});

View File

@@ -0,0 +1,750 @@
/* eslint-disable no-unused-vars */
import {shallow} from 'enzyme';
import React from 'react';
import {withContext} from 'shallow-with-context';
import {ServiceContainer} from 'src/containers/ServiceContainer';
import {DEFAULT_DASHBOARDS_CONTEXT} from 'src/context/DashboardsContext';
import {DummyService, ServiceState} from 'src/lib/services';
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
describe('src/containers/ServiceContainer', () => {
const ServiceContainerWithContext = withContext(
ServiceContainer, DEFAULT_DASHBOARDS_CONTEXT
);
let context = null;
beforeEach(() => {
context = {
...DEFAULT_DASHBOARDS_CONTEXT,
nukeService: jasmine.createSpy(),
saveServiceCharacteristics: jasmine.createSpy(),
saveServiceLayout: jasmine.createSpy(),
addService: jasmine.createSpy(),
setCurrentDashboardId: jasmine.createSpy(),
addDashboard: jasmine.createSpy(),
dashboards: DashboardsFactory(),
};
});
describe('constructor', () => {
it('initializes the state', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// Then
expect(component.state('serviceState')).toBe(null);
expect(component.state('showSettingsModal')).toBe(false);
expect(component.state('nextCharacteristics')).toBe(null);
});
});
describe('service', () => {
it('looks up the bound service instance', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// When
const service = component.instance().service();
// Then
expect(service).toEqual(context.dashboards[0].services[0]);
});
});
describe('setServiceState', () => {
it('initializes new state object if the current state is `null`', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// When
component.instance().setServiceState({data: {spam: true}}, {});
// Then
expect(component.state('serviceState')).toBeInstanceOf(ServiceState);
expect(component.state('serviceState').data()).toEqual({spam: true});
});
it('initializes new state object if reset is requested', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
const oldState = new ServiceState({data: {spam: false}});
component.setState({serviceState: oldState});
// When
component.instance().setServiceState(
{data: {spam: true}}, {reset: true}
);
// Then
expect(component.state('serviceState')).not.toBe(oldState);
});
it('updates the existing state object with new payload', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
const oldState = new ServiceState({data: {spam: false}});
component.setState({serviceState: oldState});
// When
component.instance().setServiceState({data: {spam: true}}, {});
// Then
expect(component.state('serviceState')).not.toBe(oldState);
expect(component.state('serviceState').data()).toEqual({spam: true});
});
});
it('allows setting next characteristics', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// When
component.instance().setNextCharacteristics({spam: true});
// Then
expect(component.state('nextCharacteristics')).toEqual({spam: true});
});
describe('setNextCharacteristicsFromService', () => {
it('resets the next characteristics if the service is null', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
component.setState({nextCharacteristics: {spam: false}});
// When
component.instance().setNextCharacteristicsFromService(null);
// Then
expect(component.state('nextCharacteristics')).toBe(null);
});
it('resets the next characteristics if the service has no widget', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
component.setState({nextCharacteristics: {spam: false}});
const service = context.dashboards[0].services[0];
service.widgetComponent = null;
// When
component.instance().setNextCharacteristicsFromService(service);
// Then
expect(component.state('nextCharacteristics')).toBe(null);
});
it('resets the next characteristics if the service is dummy', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
component.setState({nextCharacteristics: {spam: false}});
const service = new DummyService();
// When
component.instance().setNextCharacteristicsFromService(service);
// Then
expect(component.state('nextCharacteristics')).toBe(null);
});
it('sets the next characteristics from the service characteristics', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
const service = context.dashboards[0].services[0];
// When
component.instance().setNextCharacteristicsFromService(service);
// Then
expect(component.state('nextCharacteristics')).toEqual(
service.characteristics
);
});
});
describe('showHideSettingsModal', () => {
it('sets next characteristics from the service when modal is to be shown', () => {
// Givem
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setNextCharacteristicsFromService');
// When
component.instance().showHideSettingsModal(true);
// Then
expect(component.instance().setNextCharacteristicsFromService).toHaveBeenCalledWith(
context.dashboards[0].services[0]
);
});
it('resets next characteristics when modal is to be hidden', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setNextCharacteristicsFromService');
// When
component.instance().showHideSettingsModal(false);
// Then
expect(component.instance().setNextCharacteristicsFromService).toHaveBeenCalledWith(
null
);
});
it('updates the state with new visibility flag', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setNextCharacteristicsFromService');
// When
component.instance().showHideSettingsModal(true);
// Then
expect(component.state('showSettingsModal')).toBe(true);
});
});
describe('onSettingsButtonClick', () => {
it('requests the settings modal to be shown', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'showHideSettingsModal');
// When
component.instance().onSettingsButtonClick();
// Then
expect(component.instance().showHideSettingsModal).toHaveBeenCalledWith(
true
);
});
});
describe('onSettingsModalClose', () => {
it('requests the settings modal to be hidden', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'showHideSettingsModal');
// When
component.instance().onSettingsModalClose();
// Then
expect(component.instance().showHideSettingsModal).toHaveBeenCalledWith(
false
);
});
});
describe('onSettingsModalSaveButtonClick', () => {
it('requests the next characteristics to be saved', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
component.setState({nextCharacteristics: {spam: true}});
// When
component.instance().onSettingsModalSaveButtonClick();
// Then
expect(context.saveServiceCharacteristics).toHaveBeenCalledWith(
'FakeService', 'fake_instance', {spam: true}
);
});
it('closes the settings modal', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'onSettingsModalClose');
// When
component.instance().onSettingsModalSaveButtonClick();
// Then
expect(component.instance().onSettingsModalClose).toHaveBeenCalled();
});
});
describe('onSettingsModalNukeButtonClick', () => {
it('requests the service to be nuked', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// When
component.instance().onSettingsModalNukeButtonClick();
// Then
expect(context.nukeService).toHaveBeenCalledWith(
'FakeService', 'fake_instance'
);
});
it('closes the settings modal', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'onSettingsModalClose');
// When
component.instance().onSettingsModalNukeButtonClick();
// Then
expect(component.instance().onSettingsModalClose).toHaveBeenCalled();
});
});
describe('onAppearancePopupColorChange', () => {
it('requests the updated characteristics to be saved', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
// When
component.instance().onAppearancePopupColorChange('red');
// Then
expect(context.saveServiceCharacteristics).toHaveBeenCalledWith(
'FakeService', 'fake_instance', {
spam: true,
appearance: {
color: 'red',
},
}
);
});
});
describe('onServiceRestartButtonClick', () => {
it('requests the updated characteristics to be saved', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
const service = context.dashboards[0].services[0];
spyOn(service, 'restart');
// When
component.instance().onServiceRestartButtonClick();
// Then
expect(service.restart).toHaveBeenCalled();
});
});
describe('unsubscribeIfNeeded', () => {
it('calls the unsubscribe callback', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
component.instance().serviceUnsubscribe = jasmine.createSpy();
// When
component.instance().unsubscribeIfNeeded();
// Then
expect(component.instance().serviceUnsubscribe).toHaveBeenCalled();
});
});
describe('componentDidMount', () => {
it('is a noop if the service is null', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setServiceState');
spyOn(component.instance(), 'unsubscribeIfNeeded');
spyOn(component.instance(), 'service').and.returnValue(null);
// When
component.instance().componentDidMount();
// Then
expect(component.instance().setServiceState).not.toHaveBeenCalled();
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
expect(component.instance().serviceUnsubscribe).toBe(null);
});
it('is a noop if the service has no widget', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setServiceState');
spyOn(component.instance(), 'unsubscribeIfNeeded');
context.dashboards[0].services[0].widgetComponent = null;
// When
component.instance().componentDidMount();
// Then
expect(component.instance().setServiceState).not.toHaveBeenCalled();
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
expect(component.instance().serviceUnsubscribe).toBe(null);
});
it('is a noop if the service is dummy', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setServiceState');
spyOn(component.instance(), 'unsubscribeIfNeeded');
spyOn(component.instance(), 'service').and.returnValue(
new DummyService()
);
// When
component.instance().componentDidMount();
// Then
expect(component.instance().setServiceState).not.toHaveBeenCalled();
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
expect(component.instance().serviceUnsubscribe).toBe(null);
});
it('sets itself up for the service', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
spyOn(component.instance(), 'setServiceState');
spyOn(component.instance(), 'unsubscribeIfNeeded');
const service = context.dashboards[0].services[0];
spyOn(service, 'start');
const initialState = new ServiceState({data: {initial: true}});
spyOn(service, 'initialState').and.returnValue(initialState);
const fakeUnsubscribe = jasmine.createSpy();
spyOn(service, 'subscribe').and.returnValue(fakeUnsubscribe);
// When
component.instance().componentDidMount();
// Then
expect(component.instance().setServiceState).toHaveBeenCalledWith(
initialState, true
);
expect(service.start).toHaveBeenCalled();
expect(component.instance().unsubscribeIfNeeded).toHaveBeenCalled();
expect(service.subscribe).toHaveBeenCalledWith(
component.instance().setServiceState
);
expect(component.instance().serviceUnsubscribe).toEqual(fakeUnsubscribe);
});
});
describe('componentWillUnmount', () => {
it('unsubscribes from the service', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{(props) => <span>It works!</span>}
</ServiceContainerWithContext>,
{
context: context,
}
);
const mockUnsubscribeIfNeeded = jasmine.createSpy();
component.instance().unsubscribeIfNeeded = mockUnsubscribeIfNeeded;
// When
component.unmount();
// Then
expect(mockUnsubscribeIfNeeded).toHaveBeenCalled();
});
});
describe('render', () => {
let childrenFunc = null;
beforeEach(() => {
childrenFunc = jasmine.createSpy();
childrenFunc.returnValue = <span>It works!</span>;
});
it('calls the children func with props', () => {
// Given
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{childrenFunc}
</ServiceContainerWithContext>,
{
context: context,
}
);
const serviceState = new ServiceState({data: {spam: true}});
component.setState({
serviceState: serviceState,
showSettingsModal: true,
nextCharacteristics: {eggs: true},
});
// Then
expect(childrenFunc).toHaveBeenCalledWith({
hasSettingsView: true,
service: context.dashboards[0].services[0],
serviceState: serviceState,
setServiceState: component.instance().setServiceState,
settingsViewProps: {
kind: 'FakeService',
instance: 'fake_instance',
nextCharacteristics: {eggs: true},
setNextCharacteristics: component.instance().setNextCharacteristics,
},
showSettingsModal: true,
onAppearancePopupColorChange: component.instance().onAppearancePopupColorChange,
onServiceRestartButtonClick: component.instance().onServiceRestartButtonClick,
onSettingsButtonClick: component.instance().onSettingsButtonClick,
onSettingsModalClose: component.instance().onSettingsModalClose,
onSettingsModalNukeButtonClick: component.instance().onSettingsModalNukeButtonClick,
onSettingsModalSaveButtonClick: component.instance().onSettingsModalSaveButtonClick,
});
});
it('disables settings view if the service is dummy', () => {
// Given
const service = context.dashboards[0].services[0];
spyOn(service, 'isDummy').and.returnValue(true);
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{childrenFunc}
</ServiceContainerWithContext>,
{
context: context,
}
);
// Then
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
hasSettingsView: false,
}));
});
it('disables settings view if the service has no widget', () => {
// Given
const service = context.dashboards[0].services[0];
service.widgetComponent = null;
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{childrenFunc}
</ServiceContainerWithContext>,
{
context: context,
}
);
// Then
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
hasSettingsView: false,
}));
});
it('disables settings view if the service widget has no settings view', () => {
// Given
const service = context.dashboards[0].services[0];
const oldSettingsView = service.widgetComponent.settingsView;
service.widgetComponent.settingsView = undefined;
const component = shallow(
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
{childrenFunc}
</ServiceContainerWithContext>,
{
context: context,
}
);
// Then
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
hasSettingsView: false,
}));
// After
service.widgetComponent.settingsView = oldSettingsView;
});
});
});

View File

@@ -0,0 +1,304 @@
/* eslint-disable no-unused-vars */
import {mount} from 'enzyme';
import React from 'react';
import {
DEFAULT_DASHBOARDS_CONTEXT, DashboardsContext,
} from 'src/context/DashboardsContext';
import * as Hooks from 'src/hooks';
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
describe('src/hooks', () => {
const HookWrapper = ({context, hook, hookArgs, output}) => {
const Children = (props) => {
output.result = hook(...hookArgs);
return <span>It works!</span>;
};
return (
<DashboardsContext.Provider value={context}>
<Children />
</DashboardsContext.Provider>
);
};
let context = null;
beforeEach(() => {
context = {
...DEFAULT_DASHBOARDS_CONTEXT,
dashboards: DashboardsFactory(),
};
});
describe('useServices', () => {
it('returns the the current dashboard services', () => {
// Given
const output = {};
context = {
...context,
currentDashboardId: context.dashboards[0].id,
};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useServices}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual(context.dashboards[0].services);
});
});
describe('useService', () => {
it('returns the the specified service instance', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useService}
hookArgs={['FakeService', 'fake_instance']}
output={output}
/>
);
// Then
expect(output.result).toEqual(context.dashboards[0].services[0]);
});
});
describe('useSaveServiceCharacteristics', () => {
it('returns the saveServiceCharacteristics callback', () => {
// Given
const output = {};
// WHen
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useSaveServiceCharacteristics}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.saveServiceCharacteristics);
});
});
describe('useSaveServiceLayout', () => {
it('returns the saveServiceLayout callback', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useSaveServiceLayout}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.saveServiceLayout);
});
});
describe('useNukeService', () => {
it('returns the nukeService callback', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useNukeService}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.nukeService);
});
});
describe('useAddService', () => {
it('returns the addService callback', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useAddService}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.addService);
});
});
describe('useServicesSavingState', () => {
it('returns the service saving state', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useServicesSavingState}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual({
lastSaveTimestamp: context.lastSaveTimestamp,
lastSaveError: context.lastSaveError,
isSaving: context.isSaving,
});
});
});
describe('useWebSocketState', () => {
it('returns the websocket state', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useWebSocketState}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual({
isWebSocketConnected: context.isWebSocketConnected,
});
});
});
describe('useDashboards', () => {
it('returns the dashboards', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useDashboards}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual(context.dashboards);
});
});
describe('useCurrentDashboardId', () => {
it('returns the dashboards', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useCurrentDashboardId}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual(context.currentDashboardId);
});
});
describe('useSetCurrentDashboardId', () => {
it('returns the setCurrentDashboardId callback', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useSetCurrentDashboardId}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.setCurrentDashboardId);
});
});
describe('useDashboardsHash', () => {
it('returns the dashboards hash', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useDashboardsHash}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toEqual(context.dashboardsHash);
});
});
describe('useAddDashboard', () => {
it('returns the addDashboard callback', () => {
// Given
const output = {};
// When
const component = mount(
<HookWrapper
context={context}
hook={Hooks.useAddDashboard}
hookArgs={[]}
output={output}
/>
);
// Then
expect(output.result).toBe(context.addDashboard);
});
});
});

View File

@@ -0,0 +1,242 @@
import * as BaseLib from 'src/lib/base';
describe('src/lib/base', () => {
describe('SubscribableMixin', () => {
const TestClass = class extends BaseLib.SubscribableMixin(BaseLib.HomeHubBaseClass) {
};
describe('constructor', () => {
it('initializes the instance', () => {
// Given
const mixin = new TestClass();
// Then
expect(mixin.subscribers).toEqual([]);
});
});
describe('notify', () => {
it('notifies the subscribers', () => {
// Given
const mixin = new TestClass();
const mockSubscriber = jasmine.createSpy();
mixin.subscribers.push(mockSubscriber);
mixin.subscribers.push(null);
// When
mixin.notify('test');
// Then
expect(mockSubscriber).toHaveBeenCalledWith('test', {});
});
it('allows sending a notification with user info', () => {
// Given
const mixin = new TestClass();
const mockSubscriber = jasmine.createSpy();
mixin.subscribers.push(mockSubscriber);
// When
mixin.notify('test', {reset: true});
// Then
expect(mockSubscriber).toHaveBeenCalledWith('test', {reset: true});
});
});
describe('unsubscriberFactory', () => {
it('returns an unsubscriber function', () => {
// Given
const mixin = new TestClass();
// When
const result = mixin.unsubscriberFactory(0);
// Then
expect(result).toBeInstanceOf(Function);
});
it('configures the unsubscriber function to remove the bound subscriber', () => {
// Given
const mixin = new TestClass();
const mockSubscriber1 = jasmine.createSpy();
mixin.subscribers.push(mockSubscriber1);
const mockSubscriber2 = jasmine.createSpy();
mixin.subscribers.push(mockSubscriber2);
const mockSubscriber3 = jasmine.createSpy();
mixin.subscribers.push(mockSubscriber3);
const unsubscriber = mixin.unsubscriberFactory(1);
// When
unsubscriber();
// Then
expect(mixin.subscribers.length).toEqual(3);
expect(mixin.subscribers[0]).toBe(mockSubscriber1);
expect(mixin.subscribers[1]).toBe(null);
expect(mixin.subscribers[2]).toBe(mockSubscriber3);
});
});
describe('subscribe', () => {
it('adds a subscriber and returns its unsubscriber', () => {
// Given
const mixin = new TestClass();
const mockUnsubscriber = jasmine.createSpy();
spyOn(mixin, 'unsubscriberFactory').and.returnValue = mockUnsubscriber;
const mockSubscriber = jasmine.createSpy();
// When
mixin.subscribe(mockSubscriber);
// Then
expect(mixin.subscribers.length).toEqual(1);
expect(mixin.subscribers[0]).toBe(mockSubscriber);
expect(mixin.unsubscriberFactory).toHaveBeenCalledWith(0);
});
});
});
describe('EventSouceMixin', () => {
const Base = BaseLib.EventSourceMixin(BaseLib.HomeHubBaseClass, ['start']);
const TestClass = class extends Base {
};
describe('constructor', () => {
it('initializes the instance', () => {
// Given
const mixin = new TestClass();
// Then
expect(mixin.eventListeners).toEqual({start: []});
});
});
describe('addEventListener', () => {
let mockListener = null;
beforeEach(() => {
mockListener = jasmine.createSpy();
});
it('ignores an uknown event', () => {
// Given
const mixin = new TestClass();
// When
mixin.addEventListener('testing', mockListener);
// Then
expect(mixin.eventListeners.testing).not.toBeDefined();
});
it('adds a listener for an event', () => {
// Given
const mixin = new TestClass();
// When
mixin.addEventListener('start', mockListener);
// Then
expect(mixin.eventListeners.start.length).toEqual(1);
expect(mixin.eventListeners.start[0]).toBe(mockListener);
});
});
describe('addEventListener', () => {
let mockListener = null;
beforeEach(() => {
mockListener = jasmine.createSpy();
});
it('ignores an uknown event', () => {
// Given
const mixin = new TestClass();
mixin.addEventListener('start', mockListener);
// When
mixin.removeEventListener('testing', mockListener);
// Then
expect(mixin.eventListeners.testing).not.toBeDefined();
});
it('ignores an unknown listener', () => {
// Given
const mixin = new TestClass();
mixin.addEventListener('start', mockListener);
const mockListener2 = jasmine.createSpy();
// When
mixin.removeEventListener('start', mockListener2);
// Then
expect(mixin.eventListeners.start.length).toEqual(1);
expect(mixin.eventListeners.start[0]).toBe(mockListener);
});
it('removes a listener for an event', () => {
// Given
const mixin = new TestClass();
mixin.addEventListener('start', mockListener);
const mockListener2 = jasmine.createSpy();
mixin.addEventListener('start', mockListener2);
const mockListener3 = jasmine.createSpy();
mixin.addEventListener('start', mockListener3);
// When
mixin.removeEventListener('start', mockListener2);
// Then
expect(mixin.eventListeners.start.length).toEqual(3);
expect(mixin.eventListeners.start[0]).toBe(mockListener);
expect(mixin.eventListeners.start[1]).toBe(null);
expect(mixin.eventListeners.start[2]).toBe(mockListener3);
});
});
describe('fireEvent', () => {
let mockListener = null;
beforeEach(() => {
mockListener = jasmine.createSpy();
});
it('ignores an uknown event', () => {
// Given
const mixin = new TestClass();
mixin.addEventListener('start', mockListener);
// When
mixin.fireEvent('testing', mockListener);
// Then
expect(mockListener).not.toHaveBeenCalled();
});
it('calls event listeners for an event', () => {
// Given
const mixin = new TestClass();
mixin.addEventListener('start', mockListener);
// When
mixin.fireEvent('start', mockListener);
// Then
expect(mockListener).toHaveBeenCalledWith(mixin);
});
});
});
});

View File

@@ -0,0 +1,23 @@
import * as DashboardsLib from 'src/lib/dashboards';
describe('src/lib/dashboards', () => {
describe('DashboardFactory', () => {
it('initializes the dashboard with defaults', () => {
// Given
const result = DashboardsLib.DashboardFactory();
// Then
expect(result.name).toEqual(jasmine.any(String));
expect(result.id).toBeUUIDv4();
expect(result.services).toEqual([]);
});
it('allows specifying an arbitrary name', () => {
// Given
const result = DashboardsLib.DashboardFactory('Testing');
// Then
expect(result.name).toEqual('Testing');
});
});
});

View File

@@ -0,0 +1,15 @@
import * as HashLib from 'src/lib/hashlib';
describe('src/lib/hashlib', () => {
describe('sha256', () => {
it('generates a hex digest of a message', async () => {
// Given
const result = await HashLib.sha256('spam');
// Then
expect(result).toEqual(
'f10c5a90947d4313c6af600facfb0c259444e4932a217fc341be0b3776f40933'
);
});
});
});

View File

@@ -0,0 +1,48 @@
import * as LocalStorageLib from 'src/lib/localStorage';
describe('src/lib/localStorage', () => {
beforeEach(() => {
window.localStorage.setItem('test', '{"spam":true}');
});
afterEach(() => {
window.localStorage.removeItem('test');
});
describe('getItem', () => {
it('returns the parsed item', () => {
// Given
const result = LocalStorageLib.getItem('test');
// Then
expect(result).toEqual({'spam': true});
});
it('returns default if the item is not present', () => {
// Given
const result = LocalStorageLib.getItem('test2', 'default');
// Then
expect(result).toEqual('default');
});
it('defaults the default to null', () => {
// Given
const result = LocalStorageLib.getItem('test3');
// Then
expect(result).toBe(null);
});
});
describe('setItem', () => {
it('stores the serialized item', () => {
// Given
LocalStorageLib.setItem('test', {spam: false});
// Then
const storedItem = window.localStorage.getItem('test');
expect(storedItem).toEqual('{"spam":false}');
});
});
});

View File

@@ -0,0 +1,117 @@
import * as RPCLib from 'src/lib/rpc';
describe('src/lib/rpc', () => {
describe('callMethod', () => {
beforeEach(() => {
spyOn(window, 'fetch');
});
it('formats and sends a POST request to the backend RPC URL', async () => {
// Given
window.fetch.and.resolveTo({
ok: true,
status: 200,
statusText: 'OK',
json: jasmine.createSpy().and.resolveTo({
result: 'ok',
}),
});
// When
await RPCLib.callMethod('testing', ['spam']);
// Then
expect(window.fetch).toHaveBeenCalledWith('/backend/rpc', {
method: 'POST',
body: jasmine.any(String),
headers: {
'Content-Type': 'application/json',
},
});
const callArgs = window.fetch.calls.argsFor(0);
const body = JSON.parse(callArgs[1].body);
expect(body.jsonrpc).toEqual('2.0');
expect(body.method).toEqual('testing');
expect(body.params).toEqual(['spam']);
expect(body.id).toBeUUIDv4();
});
it('formats a notification call', async () => {
// Given
window.fetch.and.resolveTo({
ok: true,
status: 200,
statusText: 'OK',
json: jasmine.createSpy().and.resolveTo({
result: 'ok',
}),
});
// When
await RPCLib.callMethod('testing', ['spam'], true);
// Then
const callArgs = window.fetch.calls.argsFor(0);
const body = JSON.parse(callArgs[1].body);
expect(body.id).not.toBeDefined();
});
it('returns an error is the response was not OK', async () => {
// Given
window.fetch.and.resolveTo({
ok: false,
status: 400,
statusText: 'Bad Request',
});
// When
const result = await RPCLib.callMethod('testing');
// Then
expect(result).not.toContain('data');
expect(result.error).toMatch('HTTP 400 Bad Request');
});
it('returns an error is the response was a JSONRPC error', async () => {
// Given
window.fetch.and.resolveTo({
ok: true,
status: 200,
statusText: 'OK',
json: jasmine.createSpy().and.resolveTo({
error: {
message: 'Internal error',
data: 'Test',
},
}),
});
// When
const result = await RPCLib.callMethod('testing');
// Then
expect(result).not.toContain('data');
expect(result.error).toMatch('RPC Error: Internal error Test');
});
it('returns an result is the response was successful', async () => {
// Given
window.fetch.and.resolveTo({
ok: true,
status: 200,
statusText: 'OK',
json: jasmine.createSpy().and.resolveTo({
result: 'ok',
}),
});
// When
const result = await RPCLib.callMethod('testing');
// Then
expect(result.data).toEqual('ok');
expect(result).not.toContain('error');
});
});
});

View File

@@ -0,0 +1,495 @@
import * as ServicesLib from 'src/lib/services';
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
import {FakeService, FakeWidget} from 'tests/__fixtures__/services';
describe('src/lib/services', () => {
describe('ServiceState', () => {
describe('constructor', () => {
it('initializes the instance with a payload', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
error: {
message: 'FIAL',
},
});
// Then
expect(serviceState.payload.data).toEqual({spam: true});
expect(serviceState.payload.error).toEqual({message: 'FIAL'});
});
it('initializes the instance with the default payload', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
// Then
expect(serviceState.payload.data).toBe(null);
expect(serviceState.payload.error).toBe(null);
});
});
describe('isLoading', () => {
it('returns true if both data and error fields are null', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
// Then
expect(serviceState.isLoading()).toBe(true);
});
it('returns false if data is null and error is not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
error: {
message: 'FIAL',
},
});
// Then
expect(serviceState.isLoading()).toBe(false);
});
it('returns false if data is not null and error is null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
});
// Then
expect(serviceState.isLoading()).toBe(false);
});
it('returns false if both data and error fields are not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
error: {
message: 'FIAL',
},
});
// Then
expect(serviceState.isLoading()).toBe(false);
});
});
describe('hasData', () => {
it('returns false if isLoading is true and data is not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
});
spyOn(serviceState, 'isLoading').and.returnValue(true);
// Then
expect(serviceState.hasData()).toBe(false);
});
it('returns false if isLoading is false and data is null', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
spyOn(serviceState, 'isLoading').and.returnValue(true);
// Then
expect(serviceState.hasData()).toBe(false);
});
it('returns true if isLoading is false and data is not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
});
spyOn(serviceState, 'isLoading').and.returnValue(false);
// Then
expect(serviceState.hasData()).toBe(true);
});
});
describe('hasError', () => {
it('returns false if isLoading is true and error is not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
error: {
message: 'FIAL',
},
});
spyOn(serviceState, 'isLoading').and.returnValue(true);
// Then
expect(serviceState.hasError()).toBe(false);
});
it('returns false if isLoading is false and error is null', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
spyOn(serviceState, 'isLoading').and.returnValue(true);
// Then
expect(serviceState.hasError()).toBe(false);
});
it('returns true if isLoading is false and error is not null', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
error: {
message: 'FIAL',
},
});
spyOn(serviceState, 'isLoading').and.returnValue(false);
// Then
expect(serviceState.hasError()).toBe(true);
});
});
describe('hasFatalError', () => {
it('returns true if hasData is false and hasError is true', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
spyOn(serviceState, 'hasData').and.returnValue(false);
spyOn(serviceState, 'hasError').and.returnValue(true);
// Then
expect(serviceState.hasFatalError()).toBe(true);
});
it('returns false if hasData is true and hasError is true', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
spyOn(serviceState, 'hasData').and.returnValue(true);
spyOn(serviceState, 'hasError').and.returnValue(true);
// Then
expect(serviceState.hasFatalError()).toBe(false);
});
it('returns false if hasData is true and hasError is false', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
spyOn(serviceState, 'hasData').and.returnValue(true);
spyOn(serviceState, 'hasError').and.returnValue(false);
// Then
expect(serviceState.hasFatalError()).toBe(false);
});
});
describe('update', () => {
it('returns a new ServiceState with updated payload', () => {
// Given
const serviceState = new ServicesLib.ServiceState();
// When
const result = serviceState.update({
data: {
spam: true,
},
error: {
message: 'FIAL',
},
});
// Then
expect(result).not.toBe(serviceState);
expect(result.payload.data).toEqual({spam: true});
expect(result.payload.error).toEqual({message: 'FIAL'});
expect(serviceState.payload).toEqual({
data: null,
error: null,
});
});
});
describe('data', () => {
it('returns the data payload field', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
error: {
message: 'FIAL',
},
});
// Then
expect(serviceState.data()).toEqual(serviceState.payload.data);
});
});
describe('error', () => {
it('returns the error payload field', () => {
// Given
const serviceState = new ServicesLib.ServiceState({
data: {
spam: true,
},
error: {
message: 'FIAL',
},
});
// Then
expect(serviceState.error()).toEqual(serviceState.payload.error);
});
});
});
describe('BaseService', () => {
let spec = null;
beforeEach(() => {
spec = {
instance: 'fake_instance',
characteristics: {
spam: true,
},
widgetComponent: FakeWidget,
layout: {
x: 0,
y: 0,
w: 1,
h: 1,
},
};
});
it('includes the subscribable mixin', () => {
// Given
const service = new FakeService(spec);
// Then
expect(service.__mixins__).toContain('SubscribableMixin');
});
describe('emptyCharacteristics', () => {
it('returns the empty characteristics', () => {
// Given
const result = FakeService.emptyCharacteristics();
// Then
expect(result).toEqual({});
});
});
describe('constructor', () => {
it('initializes the instance', () => {
// Given
const service = new FakeService(spec);
// Then
expect(service.kind).toEqual(FakeService.kind);
expect(service.instance).toEqual(spec.instance);
expect(service.characteristics).toEqual(spec.characteristics);
expect(service.widget).toEqual(FakeService.widget);
expect(service.widgetComponent).toEqual(spec.widgetComponent);
expect(service.layout).toEqual(spec.layout);
});
});
describe('restart', () => {
it('restarts the service', async () => {
// Given
const service = new FakeService(spec);
spyOn(service, 'notify');
spyOn(service, 'start').and.resolveTo(null);
spyOn(service, 'stop').and.resolveTo(null);
// When
await service.restart();
// Then
expect(service.notify).toHaveBeenCalledWith(null, {reset: true});
expect(service.stop).toHaveBeenCalledBefore(service.start);
expect(service.start).toHaveBeenCalled();
});
});
describe('isDummy', () => {
it('returns false', () => {
// Given
const service = new FakeService(spec);
// Then
expect(service.isDummy()).toBe(false);
});
});
describe('initialState', () => {
it('returns null', () => {
// Given
const service = new FakeService(spec);
// Then
expect(service.initialState()).toBe(null);
});
});
describe('setCharacteristics', () => {
it('sets the new characteristics', () => {
// Given
const service = new FakeService(spec);
const newCharacteristics = {spam: false};
// When
service.setCharacteristics(newCharacteristics);
// Then
expect(service.characteristics).not.toBe(newCharacteristics);
expect(service.characteristics).toEqual(newCharacteristics);
});
});
describe('setLayout', () => {
it('sets the new layout', () => {
// Given
const service = new FakeService(spec);
const newLayout = {x: 1, y: 1, w: 2, h: 2};
// When
service.setLayout(newLayout);
// Then
expect(service.layout).not.toBe(newLayout);
expect(service.layout).toEqual(newLayout);
});
});
describe('toJSON', () => {
it('returns a JSON-serializable representation of the service', () => {
// Given
const service = new FakeService(spec);
// When
const result = service.toJSON();
// Then
expect(result).toEqual({
kind: service.kind,
instance: service.instance,
characteristics: service.characteristics,
layout: service.layout,
});
});
});
});
describe('DummyService', () => {
let spec = null;
beforeEach(() => {
spec = {
instance: 'fake_instance',
characteristics: {
spam: true,
},
widgetComponent: FakeWidget,
layout: {
x: 0,
y: 0,
w: 1,
h: 1,
},
};
});
describe('constructor', () => {
it('initializes the instance', () => {
// Given
const service = new ServicesLib.DummyService(spec);
// Then
expect(service.instance).toBe(null);
expect(service.widgetComponent).toBe(null);
});
});
describe('isDummy', () => {
it('returns true', () => {
// Given
const service = new ServicesLib.DummyService(spec);
// Then
expect(service.isDummy()).toBe(true);
});
});
});
describe('lookupService', () => {
let dashboards = null;
beforeEach(() => {
dashboards = DashboardsFactory();
});
it('returns DummyService instance if the specified kind does not exist', () => {
// Given
const result = ServicesLib.lookupService(
dashboards, 'Testing', 'fake_instance'
);
// Then
expect(result).toBeInstanceOf(ServicesLib.DummyService);
expect(result.isDummy()).toBe(true);
});
it('returns DummyService instance if the specified instance does not exist', () => {
// Given
const result = ServicesLib.lookupService(
dashboards, 'FakeService', 'testing'
);
// Then
expect(result).toBeInstanceOf(ServicesLib.DummyService);
expect(result.isDummy()).toBe(true);
});
it('returns the service that matches the specified kind and instace', () => {
// Given
const result = ServicesLib.lookupService(
dashboards, 'FakeService', 'fake_instance'
);
// Then
expect(result).toBe(dashboards[0].services[0]);
});
});
describe('lookupServices', () => {
let dashboards = null;
beforeEach(() => {
dashboards = DashboardsFactory();
dashboards[0].services.push(new ServicesLib.DummyService());
dashboards[0].services.push(new FakeService({
instance: 'fake_instance2',
}));
});
it('returns all instances of services specified by kind', () => {
// Givem
const result = ServicesLib.lookupServices(dashboards, 'FakeService');
// Then
expect(result.length).toEqual(3);
expect(result[0]).toBe(dashboards[0].services[0]);
expect(result[1]).toBe(dashboards[0].services[1]);
expect(result[2]).toBe(dashboards[0].services[3]);
});
});
});

View File

@@ -0,0 +1,268 @@
import * as WebSocketLib from 'src/lib/websocket';
describe('src/lib/websocket', () => {
describe('HomeHubWebSocket', () => {
const settings = {
url: '/websocket',
};
it('includes the subscribable mixin', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
// Then
expect(webSocket.__mixins__).toContain('SubscribableMixin');
});
it('includes the event source mixin', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
// Then
expect(webSocket.__mixins__).toContain('EventSourceMixin');
});
describe('constructor', () => {
it('initializes the instance', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
// Then
expect(webSocket.debug).toBe(false);
expect(webSocket.settings).toEqual(settings);
expect(webSocket.socket).toBe(null);
expect(webSocket.reconnectTimeout).toBe(null);
expect(webSocket.reconnectCounter).toEqual(0);
});
});
describe('logDebug', () => {
beforeEach(() => {
spyOn(console, 'log');
});
it('logs a message if debug is true', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(true, settings);
// When
webSocket.logDebug('Testing');
// Then
expect(console.log).toHaveBeenCalledWith('Testing'); // eslint-disable-line no-console
});
it('does not log a message if debug is false', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
// When
webSocket.logDebug('Testing');
// Then
expect(console.log).not.toHaveBeenCalled(); // eslint-disable-line no-console
});
});
describe('stopReconnect', () => {
beforeEach(() => {
spyOn(window, 'clearTimeout');
});
it('stops the reconnect process', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
webSocket.reconnectTimeout = 123;
webSocket.reconnectCounter = 5;
// When
webSocket.stopReconnect();
// Then
expect(webSocket.reconnectTimeout).toBe(null);
expect(webSocket.reconnectCounter).toEqual(0);
expect(window.clearTimeout).toHaveBeenCalledWith(123);
});
});
describe('startReconnect', () => {
beforeEach(() => {
spyOn(window, 'setTimeout').and.returnValue(123);
});
it('starts the reconnect process', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
webSocket.reconnectCounter = 5;
// When
webSocket.startReconnect();
// Then
expect(webSocket.reconnectCounter).toEqual(6);
expect(webSocket.reconnectTimeout).toEqual(123);
expect(window.setTimeout).toHaveBeenCalledWith(webSocket.start, 1000);
});
it('breaks the reconnect process when retry count reaches limit', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
webSocket.reconnectCounter = 30;
spyOn(webSocket, 'stopReconnect');
// When
let error = null;
try {
webSocket.startReconnect();
} catch (exc) {
error = exc;
}
// Then
expect(error).toBeInstanceOf(Error);
expect(webSocket.stopReconnect).toHaveBeenCalled();
});
});
describe('start', () => {
let fakeWebSocket = null;
beforeEach(() => {
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
spyOn(window, 'WebSocket').and.returnValue(fakeWebSocket);
});
it('configures and opens the websocket connection', () => {
// Given
let fullSettings = {
...settings,
protocols: ['spam', 'eggs'],
};
const webSocket = new WebSocketLib.HomeHubWebSocket(
false, fullSettings
);
// When
webSocket.start();
// Then
expect(window.WebSocket).toHaveBeenCalledWith(
fullSettings.url, fullSettings.protocols
);
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
'open', webSocket.onSocketOpen
);
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
'close', webSocket.onSocketClose
);
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
'message', webSocket.onSocketMessage
);
});
});
describe('stop', () => {
let fakeWebSocket = null;
beforeEach(() => {
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
});
it('closes the websocket connection', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
webSocket.socket = fakeWebSocket;
// When
webSocket.stop();
// Then
expect(fakeWebSocket.close).toHaveBeenCalled();
});
});
describe('onSocketOpen', () => {
let fakeWebSocket = null;
beforeEach(() => {
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
fakeWebSocket.readyState = 1;
});
it('handles the open websocket event', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
webSocket.socket = fakeWebSocket;
spyOn(webSocket, 'logDebug');
spyOn(webSocket, 'stopReconnect');
spyOn(webSocket, 'fireEvent');
// When
webSocket.onSocketOpen();
// Then
expect(webSocket.logDebug).toHaveBeenCalled();
expect(webSocket.stopReconnect).toHaveBeenCalled();
expect(webSocket.fireEvent).toHaveBeenCalledWith('start');
});
});
describe('onSocketClose', () => {
it('handles the open websocket event', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
spyOn(webSocket, 'logDebug');
spyOn(webSocket, 'startReconnect');
spyOn(webSocket, 'fireEvent');
// When
webSocket.onSocketClose({code: 1000});
// Then
expect(webSocket.logDebug).toHaveBeenCalled();
expect(webSocket.startReconnect).toHaveBeenCalled();
expect(webSocket.fireEvent).toHaveBeenCalledWith('stop');
});
});
describe('onSocketMessage', () => {
beforeEach(() => {
spyOn(console, 'error');
});
it('gracefully handles JSON parse error', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
spyOn(webSocket, 'notify');
// When
webSocket.onSocketMessage({data: 'spam'});
// Then
expect(console.error).toHaveBeenCalledWith(
jasmine.any(String), jasmine.any(Error)
);
expect(webSocket.notify).not.toHaveBeenCalled();
});
it('parses the event data and notifies the subscribers', () => {
// Given
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
spyOn(webSocket, 'notify');
const message = {
type: 'TESTING',
data: {
spam: true,
},
};
// When
webSocket.onSocketMessage({data: JSON.stringify(message)});
// Then
expect(webSocket.notify).toHaveBeenCalledWith(message);
});
});
});
});

File diff suppressed because it is too large Load Diff