Add JS tests. Cleanup code and add github workflow to test.

This commit is contained in:
Sudhanshu Gautam 2021-01-12 23:40:38 +05:30
parent f3133a38a0
commit 3f8de60160
20 changed files with 533 additions and 264 deletions

View 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);
});
}
});
});

View file

@ -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>