Migrated to TS all servers helpers

This commit is contained in:
Alejandro Celaya 2020-08-29 13:51:53 +02:00
parent 8cc0695ee9
commit aee4c2d02f
14 changed files with 129 additions and 137 deletions

View file

@ -3,10 +3,10 @@ module.exports = {
collectCoverageFrom: [ collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}', 'src/**/*.{js,jsx,ts,tsx}',
'!src/registerServiceWorker.js', '!src/registerServiceWorker.js',
'!src/index.js', '!src/index.ts',
'!src/reducers/index.js', '!src/reducers/index.ts',
'!src/**/provideServices.js', '!src/**/provideServices.ts',
'!src/container/*.js', '!src/container/*.ts',
], ],
resolver: 'jest-pnp-resolver', resolver: 'jest-pnp-resolver',
setupFiles: [ setupFiles: [

View file

@ -1,7 +1,5 @@
export declare global { declare module 'event-source-polyfill' {
declare module '*.png' export const EventSourcePolyfill: any;
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
}
} }
declare module '*.png'

View file

@ -4,7 +4,7 @@ import { save, load, RLSOptions } from 'redux-localstorage-simple';
import reducers from '../reducers'; import reducers from '../reducers';
const isProduction = process.env.NODE_ENV !== 'production'; const isProduction = process.env.NODE_ENV !== 'production';
const composeEnhancers: Function = !isProduction && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const localStorageConfig: RLSOptions = { const localStorageConfig: RLSOptions = {
states: [ 'settings', 'servers' ], states: [ 'settings', 'servers' ],

View file

@ -1,28 +0,0 @@
import { useEffect } from 'react';
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
export const bindToMercureTopic = (mercureInfo, topic, onMessage, onTokenExpired) => () => {
const { mercureHubUrl, token, loading, error } = mercureInfo;
if (loading || error) {
return undefined;
}
const hubUrl = new URL(mercureHubUrl);
hubUrl.searchParams.append('topic', topic);
const es = new EventSource(hubUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
});
es.onmessage = ({ data }) => onMessage(JSON.parse(data));
es.onerror = ({ status }) => status === 401 && onTokenExpired();
return () => es.close();
};
export const useMercureTopicBinding = (mercureInfo, topic, onMessage, onTokenExpired) => {
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
};

View file

@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
import { MercureInfo } from '../reducers/mercureInfo';
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => () => { // eslint-disable-line max-len
const { mercureHubUrl, token, loading, error } = mercureInfo;
if (loading || error || !mercureHubUrl) {
return undefined;
}
const hubUrl = new URL(mercureHubUrl);
hubUrl.searchParams.append('topic', topic);
const es = new EventSource(hubUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
});
es.onmessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
es.onerror = ({ status }: { status: number }) => status === 401 && onTokenExpired();
return () => es.close();
};
export const useMercureTopicBinding = <T>(
mercureInfo: MercureInfo,
topic: string,
onMessage: (message: T) => void,
onTokenExpired: Function,
) => {
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
};

View file

@ -1,30 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { serverType } from '../prop-types';
import { versionMatch } from '../../utils/helpers/version';
const propTypes = {
minVersion: PropTypes.string,
maxVersion: PropTypes.string,
selectedServer: serverType,
children: PropTypes.node.isRequired,
};
const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => {
if (!selectedServer) {
return null;
}
const { version } = selectedServer;
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
if (!matchesVersion) {
return null;
}
return <React.Fragment>{children}</React.Fragment>;
};
ForServerVersion.propTypes = propTypes;
export default ForServerVersion;

View file

@ -0,0 +1,24 @@
import React, { FC } from 'react';
import { versionMatch, Versions } from '../../utils/helpers/version';
import { isReachableServer, SelectedServer } from '../data';
interface ForServerVersionProps extends Versions {
selectedServer: SelectedServer;
}
const ForServerVersion: FC<ForServerVersionProps> = ({ minVersion, maxVersion, selectedServer, children }) => {
if (!isReachableServer(selectedServer)) {
return null;
}
const { version } = selectedServer;
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
if (!matchesVersion) {
return null;
}
return <React.Fragment>{children}</React.Fragment>;
};
export default ForServerVersion;

View file

@ -1,19 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup'; import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
import { handleEventPreventingDefault } from '../../utils/utils'; import { handleEventPreventingDefault } from '../../utils/utils';
import { ServerData } from '../data';
const propTypes = { interface ServerFormProps {
onSubmit: PropTypes.func.isRequired, onSubmit: (server: ServerData) => void;
initialValues: PropTypes.shape({ initialValues?: ServerData;
name: PropTypes.string.isRequired, }
url: PropTypes.string.isRequired,
apiKey: PropTypes.string.isRequired,
}),
children: PropTypes.node.isRequired,
};
export const ServerForm = ({ onSubmit, initialValues, children }) => { export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children }) => {
const [ name, setName ] = useState(''); const [ name, setName ] = useState('');
const [ url, setUrl ] = useState(''); const [ url, setUrl ] = useState('');
const [ apiKey, setApiKey ] = useState(''); const [ apiKey, setApiKey ] = useState('');
@ -35,5 +30,3 @@ export const ServerForm = ({ onSubmit, initialValues, children }) => {
</form> </form>
); );
}; };
ServerForm.propTypes = propTypes;

View file

@ -1,35 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Message from '../../utils/Message';
import { serverType } from '../prop-types';
const propTypes = {
selectServer: PropTypes.func,
selectedServer: serverType,
match: PropTypes.object,
};
export const withSelectedServer = (WrappedComponent, ServerError) => {
const Component = (props) => {
const { selectServer, selectedServer, match } = props;
const { params: { serverId } } = match;
useEffect(() => {
selectServer(serverId);
}, [ serverId ]);
if (!selectedServer) {
return <Message loading />;
}
if (selectedServer.serverNotFound) {
return <ServerError />;
}
return <WrappedComponent {...props} />;
};
Component.propTypes = propTypes;
return Component;
};

View file

@ -0,0 +1,29 @@
import React, { FC, useEffect } from 'react';
import { RouteChildrenProps } from 'react-router';
import Message from '../../utils/Message';
import { isReachableServer, SelectedServer } from '../data';
interface WithSelectedServerProps extends RouteChildrenProps<{ serverId: string }> {
selectServer: (serverId: string) => void;
selectedServer: SelectedServer;
}
export const withSelectedServer = (WrappedComponent: FC<WithSelectedServerProps>, ServerError: FC) => (
props: WithSelectedServerProps,
) => {
const { selectServer, selectedServer, match } = props;
useEffect(() => {
match?.params?.serverId && selectServer(match?.params.serverId);
}, [ match?.params.serverId ]);
if (!selectedServer) {
return <Message loading />;
}
if (!isReachableServer(selectedServer)) {
return <ServerError />;
}
return <WrappedComponent {...props} />;
};

View file

@ -1,5 +1,8 @@
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
import { Mock } from 'ts-mockery';
import { identity } from 'ramda';
import { bindToMercureTopic } from '../../../src/mercure/helpers'; import { bindToMercureTopic } from '../../../src/mercure/helpers';
import { MercureInfo } from '../../../src/mercure/reducers/mercureInfo';
jest.mock('event-source-polyfill'); jest.mock('event-source-polyfill');
@ -11,11 +14,13 @@ describe('helpers', () => {
const onTokenExpired = jest.fn(); const onTokenExpired = jest.fn();
it.each([ it.each([
[{ loading: true, error: false }], [ Mock.of<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' }) ],
[{ loading: false, error: true }], [ Mock.of<MercureInfo>({ loading: false, error: true, mercureHubUrl: 'foo' }) ],
[{ loading: true, error: true }], [ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: 'foo' }) ],
])('does not bind an EventSource when loading or error', (mercureInfo) => { [ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
bindToMercureTopic(mercureInfo)(); [ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
bindToMercureTopic(mercureInfo, '', identity, identity)();
expect(EventSource).not.toHaveBeenCalled(); expect(EventSource).not.toHaveBeenCalled();
expect(onMessage).not.toHaveBeenCalled(); expect(onMessage).not.toHaveBeenCalled();
@ -50,7 +55,7 @@ describe('helpers', () => {
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' }); expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
expect(onTokenExpired).toHaveBeenCalled(); expect(onTokenExpired).toHaveBeenCalled();
callback(); callback?.();
expect(es.close).toHaveBeenCalled(); expect(es.close).toHaveBeenCalled();
}); });
}); });

View file

@ -16,6 +16,8 @@ describe('<EditServer />', () => {
name: 'name', name: 'name',
url: 'url', url: 'url',
apiKey: 'apiKey', apiKey: 'apiKey',
printableVersion: 'v1.2.0',
version: '1.2.0'
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount, ReactWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import ForServerVersion from '../../../src/servers/helpers/ForServerVersion'; import ForServerVersion from '../../../src/servers/helpers/ForServerVersion';
import { ReachableServer, SelectedServer } from '../../../src/servers/data';
describe('<ForServerVersion />', () => { describe('<ForServerVersion />', () => {
let wrapped; let wrapped: ReactWrapper;
const renderComponent = (minVersion, maxVersion, selectedServer) => { const renderComponent = (selectedServer: SelectedServer, minVersion?: string, maxVersion?: string) => {
wrapped = mount( wrapped = mount(
<ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}> <ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}>
<span>Hello</span> <span>Hello</span>
@ -15,10 +17,10 @@ describe('<ForServerVersion />', () => {
return wrapped; return wrapped;
}; };
afterEach(() => wrapped && wrapped.unmount()); afterEach(() => wrapped?.unmount());
it('does not render children when current server is empty', () => { it('does not render children when current server is empty', () => {
const wrapped = renderComponent('1'); const wrapped = renderComponent(null, '1');
expect(wrapped.html()).toBeNull(); expect(wrapped.html()).toBeNull();
}); });
@ -28,7 +30,7 @@ describe('<ForServerVersion />', () => {
[ undefined, '1.8.0', '1.8.3' ], [ undefined, '1.8.0', '1.8.3' ],
[ '1.7.0', '1.8.0', '1.8.3' ], [ '1.7.0', '1.8.0', '1.8.3' ],
])('does not render children when current version does not match requirements', (min, max, version) => { ])('does not render children when current version does not match requirements', (min, max, version) => {
const wrapped = renderComponent(min, max, { version }); const wrapped = renderComponent(Mock.of<ReachableServer>({ version, printableVersion: version }), min, max);
expect(wrapped.html()).toBeNull(); expect(wrapped.html()).toBeNull();
}); });
@ -40,7 +42,7 @@ describe('<ForServerVersion />', () => {
[ undefined, '1.8.0', '1.7.1' ], [ undefined, '1.8.0', '1.7.1' ],
[ '1.7.0', '1.8.0', '1.7.3' ], [ '1.7.0', '1.8.0', '1.7.3' ],
])('renders children when current version matches requirements', (min, max, version) => { ])('renders children when current version matches requirements', (min, max, version) => {
const wrapped = renderComponent(min, max, { version }); const wrapped = renderComponent(Mock.of<ReachableServer>({ version, printableVersion: version }), min, max);
expect(wrapped.html()).toContain('<span>Hello</span>'); expect(wrapped.html()).toContain('<span>Hello</span>');
}); });

View file

@ -1,20 +1,18 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../../src/servers/helpers/ServerForm';
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup'; import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup';
describe('<ServerForm />', () => { describe('<ServerForm />', () => {
let wrapper; let wrapper: ShallowWrapper;
const onSubmit = jest.fn(); const onSubmit = jest.fn();
beforeEach(() => { beforeEach(() => {
wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>); wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>);
}); });
afterEach(() => { afterEach(() => wrapper?.unmount());
jest.resetAllMocks(); afterEach(jest.resetAllMocks);
wrapper && wrapper.unmount();
});
it('renders components', () => { it('renders components', () => {
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3); expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);