diff --git a/jest.config.js b/jest.config.js index 0387f86d..1460edba 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,10 +3,10 @@ module.exports = { collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/registerServiceWorker.js', - '!src/index.js', - '!src/reducers/index.js', - '!src/**/provideServices.js', - '!src/container/*.js', + '!src/index.ts', + '!src/reducers/index.ts', + '!src/**/provideServices.ts', + '!src/container/*.ts', ], resolver: 'jest-pnp-resolver', setupFiles: [ diff --git a/shlink-web-client.d.ts b/shlink-web-client.d.ts index 52526860..06f04f6c 100644 --- a/shlink-web-client.d.ts +++ b/shlink-web-client.d.ts @@ -1,7 +1,5 @@ -export declare global { - declare module '*.png' - - interface Window { - __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function; - } +declare module 'event-source-polyfill' { + export const EventSourcePolyfill: any; } + +declare module '*.png' diff --git a/src/container/store.ts b/src/container/store.ts index 70ea604e..3196d6c2 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -4,7 +4,7 @@ import { save, load, RLSOptions } from 'redux-localstorage-simple'; import reducers from '../reducers'; 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 = { states: [ 'settings', 'servers' ], diff --git a/src/mercure/helpers/index.js b/src/mercure/helpers/index.js deleted file mode 100644 index ac824cc5..00000000 --- a/src/mercure/helpers/index.js +++ /dev/null @@ -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 ]); -}; diff --git a/src/mercure/helpers/index.ts b/src/mercure/helpers/index.ts new file mode 100644 index 00000000..8f6eec78 --- /dev/null +++ b/src/mercure/helpers/index.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; +import { MercureInfo } from '../reducers/mercureInfo'; + +export const bindToMercureTopic = (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 = ( + mercureInfo: MercureInfo, + topic: string, + onMessage: (message: T) => void, + onTokenExpired: Function, +) => { + useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]); +}; diff --git a/src/servers/helpers/ForServerVersion.js b/src/servers/helpers/ForServerVersion.js deleted file mode 100644 index 0b3c6fba..00000000 --- a/src/servers/helpers/ForServerVersion.js +++ /dev/null @@ -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 {children}; -}; - -ForServerVersion.propTypes = propTypes; - -export default ForServerVersion; diff --git a/src/servers/helpers/ForServerVersion.tsx b/src/servers/helpers/ForServerVersion.tsx new file mode 100644 index 00000000..3fe61e1a --- /dev/null +++ b/src/servers/helpers/ForServerVersion.tsx @@ -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 = ({ minVersion, maxVersion, selectedServer, children }) => { + if (!isReachableServer(selectedServer)) { + return null; + } + + const { version } = selectedServer; + const matchesVersion = versionMatch(version, { maxVersion, minVersion }); + + if (!matchesVersion) { + return null; + } + + return {children}; +}; + +export default ForServerVersion; diff --git a/src/servers/helpers/ServerForm.js b/src/servers/helpers/ServerForm.tsx similarity index 67% rename from src/servers/helpers/ServerForm.js rename to src/servers/helpers/ServerForm.tsx index 03c60868..cf9afa1f 100644 --- a/src/servers/helpers/ServerForm.js +++ b/src/servers/helpers/ServerForm.tsx @@ -1,19 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { FC, useEffect, useState } from 'react'; import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup'; import { handleEventPreventingDefault } from '../../utils/utils'; +import { ServerData } from '../data'; -const propTypes = { - onSubmit: PropTypes.func.isRequired, - initialValues: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - apiKey: PropTypes.string.isRequired, - }), - children: PropTypes.node.isRequired, -}; +interface ServerFormProps { + onSubmit: (server: ServerData) => void; + initialValues?: ServerData; +} -export const ServerForm = ({ onSubmit, initialValues, children }) => { +export const ServerForm: FC = ({ onSubmit, initialValues, children }) => { const [ name, setName ] = useState(''); const [ url, setUrl ] = useState(''); const [ apiKey, setApiKey ] = useState(''); @@ -35,5 +30,3 @@ export const ServerForm = ({ onSubmit, initialValues, children }) => { ); }; - -ServerForm.propTypes = propTypes; diff --git a/src/servers/helpers/withSelectedServer.js b/src/servers/helpers/withSelectedServer.js deleted file mode 100644 index 737f9894..00000000 --- a/src/servers/helpers/withSelectedServer.js +++ /dev/null @@ -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 ; - } - - if (selectedServer.serverNotFound) { - return ; - } - - return ; - }; - - Component.propTypes = propTypes; - - return Component; -}; diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx new file mode 100644 index 00000000..2215ded3 --- /dev/null +++ b/src/servers/helpers/withSelectedServer.tsx @@ -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, ServerError: FC) => ( + props: WithSelectedServerProps, +) => { + const { selectServer, selectedServer, match } = props; + + useEffect(() => { + match?.params?.serverId && selectServer(match?.params.serverId); + }, [ match?.params.serverId ]); + + if (!selectedServer) { + return ; + } + + if (!isReachableServer(selectedServer)) { + return ; + } + + return ; +}; diff --git a/test/mercure/helpers/index.test.js b/test/mercure/helpers/index.test.tsx similarity index 65% rename from test/mercure/helpers/index.test.js rename to test/mercure/helpers/index.test.tsx index 1e6fd3df..3a7f897f 100644 --- a/test/mercure/helpers/index.test.js +++ b/test/mercure/helpers/index.test.tsx @@ -1,5 +1,8 @@ import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; +import { Mock } from 'ts-mockery'; +import { identity } from 'ramda'; import { bindToMercureTopic } from '../../../src/mercure/helpers'; +import { MercureInfo } from '../../../src/mercure/reducers/mercureInfo'; jest.mock('event-source-polyfill'); @@ -11,11 +14,13 @@ describe('helpers', () => { const onTokenExpired = jest.fn(); it.each([ - [{ loading: true, error: false }], - [{ loading: false, error: true }], - [{ loading: true, error: true }], - ])('does not bind an EventSource when loading or error', (mercureInfo) => { - bindToMercureTopic(mercureInfo)(); + [ Mock.of({ loading: true, error: false, mercureHubUrl: 'foo' }) ], + [ Mock.of({ loading: false, error: true, mercureHubUrl: 'foo' }) ], + [ Mock.of({ loading: true, error: true, mercureHubUrl: 'foo' }) ], + [ Mock.of({ loading: false, error: false, mercureHubUrl: undefined }) ], + [ Mock.of({ 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(onMessage).not.toHaveBeenCalled(); @@ -50,7 +55,7 @@ describe('helpers', () => { expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' }); expect(onTokenExpired).toHaveBeenCalled(); - callback(); + callback?.(); expect(es.close).toHaveBeenCalled(); }); }); diff --git a/test/servers/EditServer.test.js b/test/servers/EditServer.test.js index 366dd24f..0dd0f155 100644 --- a/test/servers/EditServer.test.js +++ b/test/servers/EditServer.test.js @@ -16,6 +16,8 @@ describe('', () => { name: 'name', url: 'url', apiKey: 'apiKey', + printableVersion: 'v1.2.0', + version: '1.2.0' }; beforeEach(() => { diff --git a/test/servers/helpers/ForServerVersion.test.js b/test/servers/helpers/ForServerVersion.test.tsx similarity index 64% rename from test/servers/helpers/ForServerVersion.test.js rename to test/servers/helpers/ForServerVersion.test.tsx index 171d2feb..daaadc24 100644 --- a/test/servers/helpers/ForServerVersion.test.js +++ b/test/servers/helpers/ForServerVersion.test.tsx @@ -1,11 +1,13 @@ 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 { ReachableServer, SelectedServer } from '../../../src/servers/data'; describe('', () => { - let wrapped; + let wrapped: ReactWrapper; - const renderComponent = (minVersion, maxVersion, selectedServer) => { + const renderComponent = (selectedServer: SelectedServer, minVersion?: string, maxVersion?: string) => { wrapped = mount( Hello @@ -15,10 +17,10 @@ describe('', () => { return wrapped; }; - afterEach(() => wrapped && wrapped.unmount()); + afterEach(() => wrapped?.unmount()); it('does not render children when current server is empty', () => { - const wrapped = renderComponent('1'); + const wrapped = renderComponent(null, '1'); expect(wrapped.html()).toBeNull(); }); @@ -28,7 +30,7 @@ describe('', () => { [ undefined, '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) => { - const wrapped = renderComponent(min, max, { version }); + const wrapped = renderComponent(Mock.of({ version, printableVersion: version }), min, max); expect(wrapped.html()).toBeNull(); }); @@ -40,7 +42,7 @@ describe('', () => { [ undefined, '1.8.0', '1.7.1' ], [ '1.7.0', '1.8.0', '1.7.3' ], ])('renders children when current version matches requirements', (min, max, version) => { - const wrapped = renderComponent(min, max, { version }); + const wrapped = renderComponent(Mock.of({ version, printableVersion: version }), min, max); expect(wrapped.html()).toContain('Hello'); }); diff --git a/test/servers/helpers/ServerForm.test.js b/test/servers/helpers/ServerForm.test.tsx similarity index 84% rename from test/servers/helpers/ServerForm.test.js rename to test/servers/helpers/ServerForm.test.tsx index 855e8c1e..52020f5c 100644 --- a/test/servers/helpers/ServerForm.test.js +++ b/test/servers/helpers/ServerForm.test.tsx @@ -1,20 +1,18 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; const onSubmit = jest.fn(); beforeEach(() => { wrapper = shallow(Something); }); - afterEach(() => { - jest.resetAllMocks(); - wrapper && wrapper.unmount(); - }); + afterEach(() => wrapper?.unmount()); + afterEach(jest.resetAllMocks); it('renders components', () => { expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);