mirror of
https://git.netzspielplatz.de/docker-multiarch/openwrt-firmware-selector.git
synced 2025-11-08 23:39:37 +00:00
Add JS tests. Cleanup code and add github workflow to test.
This commit is contained in:
parent
f3133a38a0
commit
3f8de60160
20 changed files with 533 additions and 264 deletions
|
|
@ -1,9 +1,18 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (k: string) => k,
|
||||
i18n: {
|
||||
changeLanguage: (l: string) => {},
|
||||
language: 'en',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
test('renders the app container', () => {
|
||||
const { container } = render(<App />);
|
||||
expect(container.querySelector('div.App')).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
14
src/App.tsx
14
src/App.tsx
|
|
@ -5,10 +5,10 @@ import './App.scss';
|
|||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import { ThemeProvider } from '@material-ui/styles';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import { Paper, Toolbar } from '@material-ui/core';
|
||||
import Header from './components/Header';
|
||||
import Home from './containers/home/home';
|
||||
import NotFound from './containers/not-found/not-found';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
|
|
@ -33,17 +33,7 @@ const App: FunctionComponent = () => {
|
|||
<Route default component={NotFound} />
|
||||
</Switch>
|
||||
</Router>
|
||||
<Toolbar hidden />
|
||||
<Paper elevation={4} className="report-problem-container">
|
||||
<span>
|
||||
If you come across any issue, feel free to report{' '}
|
||||
<a href="https://github.com/aparcar/attendedsysupgrade-server/issues">here</a>.
|
||||
</span>
|
||||
<span className="report-link">
|
||||
For contributions, go to{' '}
|
||||
<a href="https://github.com/sudhanshu16/openwrt-firmware-selector/">Github</a>
|
||||
</span>
|
||||
</Paper>
|
||||
<Footer />
|
||||
</div>
|
||||
</React.Suspense>
|
||||
</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
cancelHandler: () => void;
|
||||
acceptHandler: () => void;
|
||||
body: React.ReactElement;
|
||||
title: React.ReactElement;
|
||||
cancelComponent: React.ReactElement;
|
||||
acceptComponent: React.ReactElement;
|
||||
};
|
||||
|
||||
const AlertDialog: FunctionComponent<Props> = ({
|
||||
open,
|
||||
cancelHandler,
|
||||
acceptHandler,
|
||||
body,
|
||||
title,
|
||||
cancelComponent,
|
||||
acceptComponent,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={cancelHandler}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">{body}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{acceptHandler && (
|
||||
<Button onClick={acceptHandler} color="primary">
|
||||
{acceptComponent}
|
||||
</Button>
|
||||
)}
|
||||
{cancelHandler && (
|
||||
<Button onClick={cancelHandler} color="secondary" variant="contained" autoFocus>
|
||||
{cancelComponent}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertDialog;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import { IconButton, makeStyles, Snackbar, SnackbarContent } from '@material-ui/core';
|
||||
import ErrorIcon from '@material-ui/icons/Error';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
const SnackBarStyles = makeStyles((theme) => ({
|
||||
error: {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
message: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
marginRight: '20px',
|
||||
fontSize: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
closeHandle: () => void;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
const ErrorSnackbar: FunctionComponent<Props> = ({ open, closeHandle, errorMessage }) => {
|
||||
const classes = SnackBarStyles();
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={open}
|
||||
autoHideDuration={6000}
|
||||
onClose={closeHandle}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
}}
|
||||
>
|
||||
<SnackbarContent
|
||||
className={classes.error}
|
||||
aria-describedby="client-snackbar"
|
||||
message={
|
||||
<span id="client-snackbar" className={classes.message}>
|
||||
<ErrorIcon className={classes.icon} />
|
||||
{errorMessage || 'An unexpected error occurred. Please try again'}
|
||||
</span>
|
||||
}
|
||||
action={[
|
||||
<IconButton key="close" aria-label="Close" color="inherit" onClick={closeHandle}>
|
||||
<CloseIcon />
|
||||
</IconButton>,
|
||||
]}
|
||||
/>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorSnackbar;
|
||||
22
src/components/Footer.tsx
Normal file
22
src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Paper, Toolbar } from '@material-ui/core';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
const Footer: FunctionComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbar hidden />
|
||||
<Paper elevation={4} className="report-problem-container">
|
||||
<span>
|
||||
If you come across any issue, feel free to report{' '}
|
||||
<a href="https://github.com/aparcar/attendedsysupgrade-server/issues">here</a>.
|
||||
</span>
|
||||
<span className="report-link">
|
||||
For contributions, go to{' '}
|
||||
<a href="https://github.com/sudhanshu16/openwrt-firmware-selector/">Github</a>
|
||||
</span>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
149
src/containers/home/components/ProfileDetails.test.tsx
Normal file
149
src/containers/home/components/ProfileDetails.test.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import ProfileDetails from './ProfileDetails';
|
||||
|
||||
import { Profile } from '../../../types/profile';
|
||||
|
||||
const mockAxios = new MockAdapter(axios);
|
||||
|
||||
const testVersion = 'TEST_VERSION';
|
||||
|
||||
const testProfile1: Profile = {
|
||||
build_at: '2020-12-08 13:51:01',
|
||||
target: 'TEST_TARGET',
|
||||
version_code: 'TEST_VERSION_CODE',
|
||||
version_number: 'TEST_VERSION_NUMBER',
|
||||
id: 'TEST_ID',
|
||||
titles: [
|
||||
{
|
||||
title: 'TEST_TITLE1',
|
||||
},
|
||||
{
|
||||
model: 'TEST_MODEL',
|
||||
vendor: 'TEST_VENDOR',
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
name: 'testimage',
|
||||
type: 'sysupgrade',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const testProfile2: Profile = {
|
||||
build_at: '2020-12-08 13:51:01',
|
||||
target: 'TEST_TARGET',
|
||||
version_code: 'TEST_VERSION_CODE',
|
||||
version_number: 'TEST_VERSION_NUMBER',
|
||||
id: 'TEST_ID',
|
||||
titles: [
|
||||
{
|
||||
title: 'TEST_TITLE2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const testProfiles = [testProfile1, testProfile2];
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (k: string) => k,
|
||||
i18n: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
changeLanguage: (l: string) => {},
|
||||
language: 'en',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Profile Details', () => {
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
});
|
||||
|
||||
it('renders the component, sends a get request and displays and shows the profile target', async () => {
|
||||
render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
id: 'TEST_ID1',
|
||||
target: 'TEST_TARGET',
|
||||
}}
|
||||
selectedVersion={testVersion}
|
||||
/>
|
||||
);
|
||||
|
||||
mockAxios.onGet().replyOnce(200);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAxios.history.get).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders titles correctly', async () => {
|
||||
let { container } = render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
id: 'TEST_ID1',
|
||||
target: 'TEST_TARGET',
|
||||
}}
|
||||
selectedVersion={testVersion}
|
||||
/>
|
||||
);
|
||||
|
||||
mockAxios.onGet().replyOnce(200, testProfile1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('#title')).toHaveTextContent(
|
||||
/^TEST_TITLE1, TEST_VENDOR TEST_MODEL$/
|
||||
);
|
||||
});
|
||||
|
||||
({ container } = render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
id: 'TEST_ID2',
|
||||
target: 'TEST_TARGET',
|
||||
}}
|
||||
selectedVersion={testVersion}
|
||||
/>
|
||||
));
|
||||
|
||||
mockAxios.onGet().replyOnce(200, testProfile2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('#title')).toHaveTextContent(/^TEST_TITLE2$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders download links correctly', async () => {
|
||||
for (const p of testProfiles) {
|
||||
render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
id: 'TEST_ID1',
|
||||
target: 'TEST_TARGET',
|
||||
}}
|
||||
selectedVersion={testVersion}
|
||||
/>
|
||||
);
|
||||
|
||||
mockAxios.onGet().replyOnce(200, p);
|
||||
|
||||
await waitFor(() => {
|
||||
const downloadLinks = screen.getAllByTestId('download_link');
|
||||
let expectedItems = p.images?.map((i) => i.type) || [];
|
||||
downloadLinks.forEach((downloadLink) => {
|
||||
expectedItems = expectedItems.filter((i) => i !== downloadLink.textContent);
|
||||
});
|
||||
|
||||
expect(expectedItems).toHaveLength(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import React, { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
|
@ -34,7 +34,6 @@ const profilesData: { [key: string]: Profile } = {};
|
|||
|
||||
const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedProfile }) => {
|
||||
const [profile, setProfileData] = useState<Profile>();
|
||||
const [working, toggleWorking] = useState<boolean>(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getHelpKey = (type: string) => {
|
||||
|
|
@ -63,8 +62,6 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
const getProfileData = useCallback(async () => {
|
||||
let profileData = profilesData[selectedProfile.id];
|
||||
|
||||
toggleWorking(true);
|
||||
|
||||
if (!profileData) {
|
||||
const response = await Axios.get<Profile>(
|
||||
`${process.env.PUBLIC_URL}/data/${selectedVersion}/${selectedProfile.target}/${selectedProfile.id}.json`
|
||||
|
|
@ -73,20 +70,23 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
profilesData[selectedProfile.id] = profileData;
|
||||
}
|
||||
|
||||
toggleWorking(false);
|
||||
|
||||
return profileData;
|
||||
}, [selectedVersion, selectedProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
if (selectedVersion && selectedProfile) {
|
||||
getProfileData().then((_profileData) => {
|
||||
if (!isEqual(profile, _profileData)) setProfileData(_profileData);
|
||||
if (mounted && !isEqual(profile, _profileData)) setProfileData(_profileData);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [selectedVersion, selectedProfile, getProfileData, profile]);
|
||||
|
||||
if (working || !profile) return <CircularProgress />;
|
||||
if (!profile) return <CircularProgress />;
|
||||
|
||||
const buildAt = new Date(profile.build_at);
|
||||
|
||||
|
|
@ -103,11 +103,13 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{t('tr-model')}</TableCell>
|
||||
<TableCell>{profile.titles?.map((title) => getTitle(title)).join(', ')}</TableCell>
|
||||
<TableCell id="title">
|
||||
{profile.titles?.map((title) => getTitle(title)).join(', ')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{t('tr-target')}</TableCell>
|
||||
<TableCell>{profile.target}</TableCell>
|
||||
<TableCell id="target">{profile.target}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{t('tr-version')}</TableCell>
|
||||
|
|
@ -121,39 +123,37 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Info</TableCell>
|
||||
{profile.titles && (
|
||||
<TableCell>
|
||||
{profile.titles
|
||||
.map<React.ReactNode>((title) => {
|
||||
const titleString = getTitle(title);
|
||||
const infoUrl = config.info_url
|
||||
.replace('{title}', encodeURI(titleString))
|
||||
.replace('{target}', profile.target)
|
||||
.replace('{id}', profile.id)
|
||||
.replace('{version}', profile.version_number);
|
||||
|
||||
return (
|
||||
<Link href={infoUrl}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{profile.titles!.length > 1 && (
|
||||
<Typography component="span">{titleString}</Typography>
|
||||
)}
|
||||
<Launch
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
.reduce((prev, curr) => [
|
||||
prev,
|
||||
<Box display="inline-block" marginRight={2} />,
|
||||
curr,
|
||||
])}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
{profile.titles
|
||||
?.map<React.ReactNode>((title: TitlesEntity) => {
|
||||
const titleString = getTitle(title);
|
||||
const infoUrl = config.info_url.replace('{title}', encodeURI(titleString));
|
||||
return (
|
||||
<Link href={infoUrl} key={titleString}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{profile.titles!.length > 1 && (
|
||||
<Typography component="span">{titleString}</Typography>
|
||||
)}
|
||||
<Launch
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
.reduce((acc: ReactNode, curr: ReactNode) => [
|
||||
acc,
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Box
|
||||
display="inline-block"
|
||||
marginRight={2}
|
||||
key={(acc?.toString() ?? '') + (curr?.toString() ?? '')}
|
||||
/>,
|
||||
curr,
|
||||
])}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
@ -177,9 +177,9 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
.replace('{target}', profile.target)
|
||||
.replace('{version}', profile.version_number)}/${i.name}`;
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRow key={downloadURL}>
|
||||
<TableCell>
|
||||
<Link href={downloadURL} target="_blank">
|
||||
<Link href={downloadURL} target="_blank" data-testid="download_link">
|
||||
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
|
||||
{i.type}
|
||||
</Button>
|
||||
|
|
|
|||
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
|
@ -4,7 +4,7 @@ import './index.css';
|
|||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
import './i18n';
|
||||
import './locales/i18n';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import translations from './locales/translations';
|
||||
import translations from './translations';
|
||||
|
||||
const resources = {
|
||||
ca: {
|
||||
|
|
@ -39,7 +39,7 @@ i18n
|
|||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
debug: (process.env.REACT_APP_I18N_DEBUG || '0') === '1',
|
||||
debug: !!process.env.REACT_APP_I18N_DEBUG,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import config from '../config';
|
||||
|
||||
class DataService {
|
||||
// getVersions = versionsPath => axios.get(versionsPath);
|
||||
//
|
||||
// getOverview = overviewPath => axios.get(overviewPath);
|
||||
//
|
||||
// getDeviceData = devicePath => axios.get(devicePath);
|
||||
//
|
||||
// getDeviceManifest = async manifest_path => {
|
||||
// const manifest = await axios.get(manifest_path);
|
||||
// return manifest.data.split('\n');
|
||||
// };
|
||||
//
|
||||
// getDevicePackages = (version, target, profile) =>
|
||||
// axios.get(
|
||||
// base_api +
|
||||
// 'packages_image?distro=openwrt&version=' +
|
||||
// version.toLowerCase() +
|
||||
// '&target=' +
|
||||
// target +
|
||||
// '&profile=' +
|
||||
// profile.toLowerCase()
|
||||
// );
|
||||
//
|
||||
// buildImage = (board, packages, target, version, uciDefaults) =>
|
||||
// axios.post(base_api + 'build-request', {
|
||||
// profile: board,
|
||||
// board,
|
||||
// defaults: uciDefaults,
|
||||
// distro: 'openwrt',
|
||||
// packages,
|
||||
// target,
|
||||
// version,
|
||||
// });
|
||||
//
|
||||
// buildStatusCheck = request_hash =>
|
||||
// axios.get(base_api + 'build-request/' + request_hash);
|
||||
}
|
||||
|
||||
export default DataService;
|
||||
16
src/tests/utils.ts
Normal file
16
src/tests/utils.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const mocki18n = (): void => {
|
||||
jest.mock('react-i18next', () => ({
|
||||
// this mock makes sure any components using the translate hook can use it without a warning being shown
|
||||
useTranslation: () => {
|
||||
return {
|
||||
t: (str: string) => str,
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
language: 'en',
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export default { mocki18n };
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
export interface Profile {
|
||||
arch_packages: string;
|
||||
arch_packages?: string;
|
||||
build_at: string;
|
||||
default_packages?: string[] | null;
|
||||
device_packages?: string[] | null;
|
||||
id: string;
|
||||
image_prefix: string;
|
||||
image_prefix?: string;
|
||||
images?: ImagesEntity[] | null;
|
||||
metadata_version: number;
|
||||
metadata_version?: number;
|
||||
target: string;
|
||||
titles?: TitlesEntity[] | null;
|
||||
version_code: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue