@@ -49,3 +49,5 @@ export default function Footer () {
);
}
+
+export default Footer;
\ No newline at end of file
diff --git a/ui/src/app/core/components/Footer.test.js b/ui/src/app/core/components/Footer.test.js
new file mode 100644
index 000000000..8f9453338
--- /dev/null
+++ b/ui/src/app/core/components/Footer.test.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Footer } from './Footer';
+
+jest.mock('../../i18n/hooks', () => ({
+ useTranslation: (value) => value
+}));
+
+it('should display the footer', () => {
+ render(
);
+ expect(screen.getByText('brand.footer.text')).toBeInTheDocument();
+});
diff --git a/ui/src/app/core/components/Header.test.js b/ui/src/app/core/components/Header.test.js
new file mode 100644
index 000000000..f154789b2
--- /dev/null
+++ b/ui/src/app/core/components/Header.test.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { Header } from './Header';
+
+jest.mock('../../i18n/hooks', () => ({
+ useTranslator: () => (value) => value,
+ useTranslation: (value) => value
+}));
+
+const mockIsAdmin = jest.fn();
+
+jest.mock('../user/UserContext', () => ({
+ useIsAdmin: () => mockIsAdmin()
+}));
+
+describe('header for admins', () => {
+ beforeEach(() => {
+ mockIsAdmin.mockReturnValue(true);
+ });
+
+ it('should display logo and navigation', () => {
+ render(
);
+ expect(screen.getByText('brand.logo-link-label')).toBeInTheDocument();
+ });
+});
+
diff --git a/ui/src/app/core/components/UserConfirmation.js b/ui/src/app/core/components/UserConfirmation.js
index cf0688b6b..2b264f625 100644
--- a/ui/src/app/core/components/UserConfirmation.js
+++ b/ui/src/app/core/components/UserConfirmation.js
@@ -3,7 +3,7 @@ import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
import Translate from '../../i18n/components/translate';
import { useHistory } from 'react-router-dom';
-
+import { reload } from '../utility/window';
export function UserConfirmation({children}) {
const [confirm, setConfirm] = React.useState(false);
@@ -27,7 +27,7 @@ export function ConfirmWindow ({message, confirm, setConfirm, confirmCallback})
setConfirm(false);
confirmCallback(true);
if (history.location.pathname.includes('provider/new')) {
- window.location.reload();
+ reload();
}
}
diff --git a/ui/src/app/core/components/UserConfirmation.test.js b/ui/src/app/core/components/UserConfirmation.test.js
new file mode 100644
index 000000000..9c2835d9b
--- /dev/null
+++ b/ui/src/app/core/components/UserConfirmation.test.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import { act, render, fireEvent, waitFor, screen } from '@testing-library/react';
+import { UserConfirmation, ConfirmWindow } from './UserConfirmation';
+
+jest.mock('../../i18n/hooks', () => ({
+ useTranslator: () => (value) => value,
+ useTranslation: (value) => value
+}));
+
+const mockReload = jest.fn();
+
+jest.mock('../utility/window', () => ({
+ reload: () => mockReload()
+}))
+
+const mockIncludes = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ useHistory: () => ({
+ location: {
+ pathname: {
+ includes: mockIncludes
+ }
+ }
+ })
+}));
+
+const TestWindow = ({getConfirmation, message}) => {
+ React.useEffect(() => getConfirmation('foo', jest.fn()), [])
+ return
{message};
+}
+
+describe('user confirmation context', () => {
+ it('should render its children with a function', () => {
+ render(
+ {(message, confirm, confirmCallback, setConfirm, getConfirmation) => hi there
}
+ );
+ expect(screen.getByText('hi there')).toBeInTheDocument();
+ expect(screen.getByText('foo')).toBeInTheDocument();
+ });
+});
+
+describe('confirmation window', () => {
+
+ it('should provide buttons to confirm', async () => {
+
+ const message = 'hi there';
+ const confirm = true;
+ const confirmCallback = jest.fn();
+ const setConfirm = jest.fn();
+
+ render(
);
+ expect(screen.getByText('hi there')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText('action.discard-changes'));
+
+ expect(confirmCallback).toHaveBeenCalledWith(true);
+
+ });
+
+ it('should provide buttons to stop', async () => {
+
+ const message = 'hi there';
+ const confirm = true;
+ const confirmCallback = jest.fn();
+ const setConfirm = jest.fn();
+
+ render(
);
+ expect(screen.getByText('hi there')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText('action.cancel'));
+
+ expect(confirmCallback).toHaveBeenCalledWith(false);
+
+ });
+
+ it('should redirect if on a provider', () => {
+ mockIncludes.mockReturnValue(true);
+
+ const message = 'hi there';
+ const confirm = true;
+ const confirmCallback = jest.fn();
+ const setConfirm = jest.fn();
+
+ render(
);
+ expect(screen.getByText('hi there')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText('action.discard-changes'));
+
+ expect(confirmCallback).toHaveBeenCalledWith(true);
+
+ expect(mockReload).toHaveBeenCalled();
+ })
+});
+
diff --git a/ui/src/app/core/hooks/utils.js b/ui/src/app/core/hooks/utils.js
deleted file mode 100644
index 8ba7e8020..000000000
--- a/ui/src/app/core/hooks/utils.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import {uuid} from '../utility/uuid';
-
-export function useGuid() {
- return uuid();
-}
\ No newline at end of file
diff --git a/ui/src/app/core/user/SessionModal.test.js b/ui/src/app/core/user/SessionModal.test.js
new file mode 100644
index 000000000..4314d7077
--- /dev/null
+++ b/ui/src/app/core/user/SessionModal.test.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { SessionModal } from './SessionModal';
+
+jest.mock('../../i18n/hooks', () => ({
+ useTranslator: () => (value) => value,
+ useTranslation: (value) => value
+}));
+
+describe('session modal', () => {
+
+ it('should provide buttons to confirm', async () => {
+
+ render(
);
+ expect(screen.getByText('message.session-timeout-heading')).toBeInTheDocument();
+
+ });
+});
\ No newline at end of file
diff --git a/ui/src/app/core/user/UserContext.test.js b/ui/src/app/core/user/UserContext.test.js
new file mode 100644
index 000000000..d5e2ce1cb
--- /dev/null
+++ b/ui/src/app/core/user/UserContext.test.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { UserProvider, useIsAdmin } from './UserContext';
+
+const getFn = jest.fn();
+const okFn = jest.fn();
+
+const mockUseFetch = {
+ get: getFn,
+ response: {}
+};
+
+Object.defineProperty(mockUseFetch.response, 'ok', {
+ get: okFn
+});
+
+jest.mock('use-http', () => () => mockUseFetch);
+
+const TestAdmin = () => {
+ const isAdmin = useIsAdmin();
+ return (<>{ isAdmin ? 'yes' : 'no' }>)
+}
+
+describe('User Context defined', () => {
+
+ beforeEach(() => {
+ getFn.mockReturnValue(Promise.resolve({
+ role: 'ROLE_ADMIN'
+ }));
+ okFn.mockReturnValueOnce(true);
+ });
+
+ it('should render the component children', async () => {
+ render(
+ {test
}
+ );
+ await waitFor(() => expect(screen.getByText('test')).toBeInTheDocument());
+ });
+
+ it('should provide the user context', async () => {
+ render(
+
+ );
+
+ await waitFor(() => expect(screen.getByText('yes')).toBeInTheDocument());
+ });
+});
diff --git a/ui/src/app/core/utility/array_move.test.js b/ui/src/app/core/utility/array_move.test.js
index f8770970e..bfcc3e8ef 100644
--- a/ui/src/app/core/utility/array_move.test.js
+++ b/ui/src/app/core/utility/array_move.test.js
@@ -6,4 +6,8 @@ it('shifts an item in an array by +1 index', () => {
it('shifts an item in an array by -1 index', () => {
expect(array_move([1, 2], 1, 0)).toEqual([2, 1]);
+});
+
+it('shifts an item in an array by +1 index if greater than length', () => {
+ expect(array_move([1, 2], 0, 2)).toEqual([2, undefined, 1]);
});
\ No newline at end of file
diff --git a/ui/src/app/core/utility/is_valid_regex.test.js b/ui/src/app/core/utility/is_valid_regex.test.js
index 7365b5818..a8b3112a1 100644
--- a/ui/src/app/core/utility/is_valid_regex.test.js
+++ b/ui/src/app/core/utility/is_valid_regex.test.js
@@ -6,4 +6,8 @@ it('should return false for a malformed regular expression', () => {
it('should return true for a well-formed regular expression', () => {
expect(isValidRegex(`[a-z0-9][a-z0-9-]{0,31}:`)).toBe(true)
+});
+
+it('should return false if no expression is provided', () => {
+ expect(isValidRegex(null)).toBe(false)
});
\ No newline at end of file
diff --git a/ui/src/app/core/utility/read_file_contents.js b/ui/src/app/core/utility/read_file_contents.js
index 99d8a175a..d8edd901c 100644
--- a/ui/src/app/core/utility/read_file_contents.js
+++ b/ui/src/app/core/utility/read_file_contents.js
@@ -1,9 +1,6 @@
-
-
export function readFileContents(file) {
return new Promise(function (resolve, reject) {
const fileReader = new FileReader();
-
fileReader.onload = (evt) => {
const reader = evt.target;
const txt = reader.result;
diff --git a/ui/src/app/core/utility/read_file_contents.test.js b/ui/src/app/core/utility/read_file_contents.test.js
new file mode 100644
index 000000000..290c48387
--- /dev/null
+++ b/ui/src/app/core/utility/read_file_contents.test.js
@@ -0,0 +1,46 @@
+import { readFileContents } from './read_file_contents';
+const FileReaderMock = {
+ DONE: FileReader.DONE,
+ EMPTY: FileReader.EMPTY,
+ LOADING: FileReader.LOADING,
+ readyState: 0,
+ error: null,
+ result: null,
+ abort: jest.fn(),
+ addEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ onabort:jest.fn(),
+ onerror:jest.fn(),
+ onload: jest.fn(),
+ onloadend:jest.fn(),
+ onloadprogress:jest.fn(),
+ onloadstart: jest.fn(),
+ onprogress:jest.fn(),
+ readAsArrayBuffer: jest.fn(),
+ readAsBinaryString: jest.fn(),
+ readAsDataURL: jest.fn(),
+ readAsText: jest.fn(),
+ removeEventListener: jest.fn()
+}
+
+xdescribe('read_file_contents', () => {
+ const file = new File([new ArrayBuffer(1)], 'file.jpg');
+ const fileReader = FileReaderMock;
+ jest.spyOn(window, 'FileReader').mockImplementation(() => fileReader);
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should resolve file as data URL', async () => {
+ fileReader.result = 'file content';
+ fileReader.addEventListener.mockImplementation((_, fn) => fn());
+
+ const content = await readFileContents(file);
+
+ expect(content).toBe('file content');
+ expect(fileReader.readAsText).toHaveBeenCalledTimes(1);
+ expect(fileReader.readAsText).toHaveBeenCalledWith(file);
+ });
+})
+
diff --git a/ui/src/app/core/utility/window.js b/ui/src/app/core/utility/window.js
new file mode 100644
index 000000000..e9277a108
--- /dev/null
+++ b/ui/src/app/core/utility/window.js
@@ -0,0 +1,3 @@
+export function reload() {
+ window.location.reload();
+}
\ No newline at end of file
diff --git a/ui/src/app/i18n/components/translate.test.js b/ui/src/app/i18n/components/translate.test.js
new file mode 100644
index 000000000..0cfbdeebf
--- /dev/null
+++ b/ui/src/app/i18n/components/translate.test.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Translate } from './translate';
+
+
+jest.mock('../hooks', () => ({
+ useTranslation: () => 'success'
+}));
+
+it('should display translated text', () => {
+ render(
);
+ expect(screen.getByText('success')).toBeInTheDocument();
+});
diff --git a/ui/src/app/i18n/context/I18n.provider.js b/ui/src/app/i18n/context/I18n.provider.js
index 42c33e921..929a30acf 100644
--- a/ui/src/app/i18n/context/I18n.provider.js
+++ b/ui/src/app/i18n/context/I18n.provider.js
@@ -11,22 +11,30 @@ const path = '/messages';
/*eslint-disable react-hooks/exhaustive-deps*/
function I18nProvider ({ children }) {
+ const [error, setError] = React.useState();
+
const { get, response } = useFetch(`${API_BASE_PATH}`, {
cacheLife: 10000,
cachePolicy: 'cache-first'
});
- React.useEffect(() => { loadMessages() }, []);
+ React.useEffect(() => {
+ loadMessages()
+ }, []);
async function loadMessages() {
const msgs = await get(`${path}`);
- if (response.ok) setMessages(msgs);
+ if (response.ok) {
+ setMessages(msgs);
+ } else {
+ setError('no messages found');
+ }
}
const [messages, setMessages] = React.useState({});
return (
<>
- {Object.keys(messages).length > 1 &&
{children}}
+ {Object.keys(messages).length >= 1 ?
{children} : error}
>
);
}
diff --git a/ui/src/app/i18n/context/I18n.provider.test.js b/ui/src/app/i18n/context/I18n.provider.test.js
new file mode 100644
index 000000000..e63d5e62e
--- /dev/null
+++ b/ui/src/app/i18n/context/I18n.provider.test.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { I18nProvider } from './I18n.provider';
+
+const getFn = jest.fn();
+const okFn = jest.fn();
+
+const mockUseFetch = {
+ get: getFn,
+ response: {}
+};
+
+Object.defineProperty(mockUseFetch.response, 'ok', {
+ get: okFn
+});
+
+jest.mock('use-http', () => () => mockUseFetch);
+
+describe('i18nProvider messages defined', () => {
+
+ beforeEach(() => {
+ getFn.mockReturnValue(Promise.resolve({
+ 'foo.bar': 'bar baz'
+ }));
+ okFn.mockReturnValueOnce(true);
+ });
+
+ it('should display translated text', async () => {
+ render(
+ test
+ );
+
+ await waitFor(() => expect(screen.getByText('test')).toBeInTheDocument());
+ });
+});
+
+describe('i18nProvider messages empty', () => {
+
+ beforeEach(() => {
+ getFn.mockReturnValue(Promise.resolve());
+ okFn.mockReturnValueOnce(false);
+ });
+
+ it('should NOT display translated text', async () => {
+ render(
+ test
+ );
+
+ await waitFor(() => expect(screen.getByText('no messages found')).toBeInTheDocument());
+ });
+});
diff --git a/ui/src/app/i18n/hooks.test.js b/ui/src/app/i18n/hooks.test.js
index 23f41c145..8dcaf32fd 100644
--- a/ui/src/app/i18n/hooks.test.js
+++ b/ui/src/app/i18n/hooks.test.js
@@ -8,6 +8,14 @@ describe('getMessage', () => {
it('should return the expected message based on the key', () => {
expect(getMessage('foo', msgs)).toEqual('bar { baz }');
});
+
+ it('should return an empty string if messages are empty', () => {
+ expect(getMessage('foo', {})).toEqual('');
+ });
+
+ it('should return provided string if message is not found', () => {
+ expect(getMessage('foo', {bar: 'baz'})).toEqual('foo');
+ });
})
describe('translate', () => {
@@ -41,7 +49,10 @@ describe('useTranslation hook', () => {
useContextMock.mockReturnValue(msgs);
expect(useTranslation('foo', { baz: 'baz' })).toBe('bar baz');
+
+ expect(useTranslation('foo')).toBe('bar { baz }');
});
+
//jest.mock('useContext')
it('should translate the provided message', () => {
diff --git a/ui/src/app/metadata/view/MetadataUpload.js b/ui/src/app/metadata/view/MetadataUpload.js
index d00be91fe..76ab28c5a 100644
--- a/ui/src/app/metadata/view/MetadataUpload.js
+++ b/ui/src/app/metadata/view/MetadataUpload.js
@@ -22,8 +22,6 @@ export function MetadataUpload() {
async function save({serviceProviderName, file, url}) {
- console.log(serviceProviderName, file);
-
setSaving(true);
const f = file?.length > 0 ? file[0] : null;