mirror of
https://git.netzspielplatz.de/docker-multiarch/openwrt-firmware-selector.git
synced 2025-11-08 18:49:26 +00:00
add more tests. Push code coverage to codecov
This commit is contained in:
parent
d6283eca43
commit
7d24fcfd4b
15 changed files with 382 additions and 51 deletions
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
|
|
@ -38,4 +38,7 @@ jobs:
|
|||
run: yarn lint
|
||||
|
||||
- name: Test 🔧
|
||||
run: yarn test
|
||||
run: yarn test -- --coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
|
|
|
|||
60
src/components/Header.test.tsx
Normal file
60
src/components/Header.test.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React from 'react';
|
||||
import { fireEvent, render, RenderResult, waitFor } from '@testing-library/react';
|
||||
|
||||
import Header from './Header';
|
||||
|
||||
const mockChangeLanguage = jest.fn();
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (k: string) => k,
|
||||
i18n: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
changeLanguage: mockChangeLanguage,
|
||||
language: undefined,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Header', () => {
|
||||
it('opens the language menu list when the button is clicked', async () => {
|
||||
const { getByTestId } = render(<Header />);
|
||||
|
||||
const languageMenuToggle = getByTestId('language-menu-toggle');
|
||||
expect(languageMenuToggle).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(languageMenuToggle);
|
||||
|
||||
expect(getByTestId('locale-en')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with open language menu', () => {
|
||||
let component: RenderResult | null;
|
||||
|
||||
beforeEach(() => {
|
||||
component = render(<Header />);
|
||||
const languageMenuToggle = component.getByTestId('language-menu-toggle');
|
||||
fireEvent.click(languageMenuToggle);
|
||||
});
|
||||
|
||||
it('changes the language when an item is clicked', () => {
|
||||
fireEvent.click(component!.getByTestId('locale-en'));
|
||||
|
||||
expect(mockChangeLanguage).toBeCalledWith('en');
|
||||
});
|
||||
|
||||
it('closes the menu with escape', async () => {
|
||||
fireEvent.keyDown(component!.getByTestId('language-menu'), { key: 'Escape' });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
component!.container.querySelector('[data-testid="locale-en"]')
|
||||
).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -37,6 +37,7 @@ const Header: FunctionComponent = () => {
|
|||
<div style={{ flexGrow: 1 }} />
|
||||
<Box position="relative">
|
||||
<Button
|
||||
data-testid="language-menu-toggle"
|
||||
aria-controls="language-menu"
|
||||
aria-haspopup="true"
|
||||
color="secondary"
|
||||
|
|
@ -51,12 +52,18 @@ const Header: FunctionComponent = () => {
|
|||
</Button>
|
||||
<Menu
|
||||
id="language-menu"
|
||||
data-testid="language-menu"
|
||||
open={showLanguageSwitch}
|
||||
anchorEl={languageSwitchAnchorEl.current}
|
||||
onClose={() => toggleLanguageSwitch(false)}
|
||||
>
|
||||
{Object.keys(locales).map((l) => (
|
||||
<MenuItem key={l} value={l} onClick={() => handleLanguageChange(l)}>
|
||||
<MenuItem
|
||||
key={l}
|
||||
value={l}
|
||||
onClick={() => handleLanguageChange(l)}
|
||||
data-testid={`locale-${l}`}
|
||||
>
|
||||
<Checkbox size="small" checked={i18n.language === l} /> {t(locales[l])}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -38,18 +38,58 @@ const testProfile1: Profile = {
|
|||
|
||||
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',
|
||||
target: 'TEST_TARGET_2',
|
||||
version_code: 'TEST_VERSION_CODE_2',
|
||||
version_number: 'TEST_VERSION_NUMBER_2',
|
||||
id: 'TEST_ID_2',
|
||||
titles: [
|
||||
{
|
||||
title: 'TEST_TITLE2',
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
name: 'factorytestimage',
|
||||
type: 'factory',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
{
|
||||
name: 'kerneltestimage',
|
||||
type: 'kernel',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
{
|
||||
name: 'roottestimage',
|
||||
type: 'root',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
{
|
||||
name: 'tftptestimage',
|
||||
type: 'tftp',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
{
|
||||
name: 'sdcardtestimage',
|
||||
type: 'sdcard',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
{
|
||||
name: 'randomtestimage',
|
||||
type: 'random',
|
||||
sha256: 'sha256',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const testProfiles = [testProfile1, testProfile2];
|
||||
const testProfile3: Profile = {
|
||||
build_at: '2020-12-08 13:51:01',
|
||||
target: 'TEST_TARGET_3',
|
||||
version_code: 'TEST_VERSION_CODE_3',
|
||||
version_number: 'TEST_VERSION_NUMBER_3',
|
||||
id: 'TEST_ID_3',
|
||||
};
|
||||
|
||||
const testProfiles = [testProfile1, testProfile2, testProfile3];
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
|
|
@ -67,7 +107,7 @@ describe('Profile Details', () => {
|
|||
mockAxios.reset();
|
||||
});
|
||||
|
||||
it('renders the component, sends a get request and displays and shows the profile target', async () => {
|
||||
it('renders the component and sends a get request', async () => {
|
||||
render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
|
|
@ -123,11 +163,11 @@ describe('Profile Details', () => {
|
|||
|
||||
it('renders download links correctly', async () => {
|
||||
for (const p of testProfiles) {
|
||||
render(
|
||||
const { getAllByTestId } = render(
|
||||
<ProfileDetails
|
||||
selectedProfile={{
|
||||
id: 'TEST_ID1',
|
||||
target: 'TEST_TARGET',
|
||||
id: p.id,
|
||||
target: p.target,
|
||||
}}
|
||||
selectedVersion={testVersion}
|
||||
/>
|
||||
|
|
@ -136,7 +176,7 @@ describe('Profile Details', () => {
|
|||
mockAxios.onGet().replyOnce(200, p);
|
||||
|
||||
await waitFor(() => {
|
||||
const downloadLinks = screen.getAllByTestId('download_link');
|
||||
const downloadLinks = getAllByTestId('download_link');
|
||||
let expectedItems = p.images?.map((i) => i.type) || [];
|
||||
downloadLinks.forEach((downloadLink) => {
|
||||
expectedItems = expectedItems.filter((i) => i !== downloadLink.textContent);
|
||||
|
|
|
|||
|
|
@ -20,16 +20,13 @@ import { useTranslation } from 'react-i18next';
|
|||
import { ProfilesEntity } from '../../../types/overview';
|
||||
import { Profile, TitlesEntity } from '../../../types/profile';
|
||||
import config from '../../../config';
|
||||
import { getTitle } from '../utils/title';
|
||||
|
||||
type Props = {
|
||||
selectedVersion: string;
|
||||
selectedProfile: ProfilesEntity;
|
||||
};
|
||||
|
||||
const getTitle = (title: TitlesEntity) => {
|
||||
return title.title || `${title.vendor} ${title.model}`;
|
||||
};
|
||||
|
||||
const profilesData: { [key: string]: Profile } = {};
|
||||
|
||||
const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedProfile }) => {
|
||||
|
|
@ -75,11 +72,9 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
if (selectedVersion && selectedProfile) {
|
||||
getProfileData().then((_profileData) => {
|
||||
if (mounted && !isEqual(profile, _profileData)) setProfileData(_profileData);
|
||||
});
|
||||
}
|
||||
getProfileData().then((_profileData) => {
|
||||
if (mounted && !isEqual(profile, _profileData)) setProfileData(_profileData);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
|
|
@ -143,14 +138,10 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
</Link>
|
||||
);
|
||||
})
|
||||
.reduce((acc: ReactNode, curr: ReactNode) => [
|
||||
.reduce((acc: ReactNode, curr: ReactNode, i: number) => [
|
||||
acc,
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Box
|
||||
display="inline-block"
|
||||
marginRight={2}
|
||||
key={(acc?.toString() ?? '') + (curr?.toString() ?? '')}
|
||||
/>,
|
||||
<Box display="inline-block" marginRight={2} key={i} />,
|
||||
curr,
|
||||
])}
|
||||
</TableCell>
|
||||
|
|
@ -177,7 +168,7 @@ const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedPro
|
|||
.replace('{target}', profile.target)
|
||||
.replace('{version}', profile.version_number)}/${i.name}`;
|
||||
return (
|
||||
<TableRow key={downloadURL}>
|
||||
<TableRow key={downloadURL + i.type}>
|
||||
<TableCell>
|
||||
<Link href={downloadURL} target="_blank" data-testid="download_link">
|
||||
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
|
||||
|
|
|
|||
118
src/containers/home/components/ProfileSearch.test.tsx
Normal file
118
src/containers/home/components/ProfileSearch.test.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { Overview } from '../../../types/overview';
|
||||
import ProfileSearch from './ProfileSearch';
|
||||
|
||||
const mockAxios = new MockAdapter(axios);
|
||||
|
||||
const testVersion = 'TEST_VERSION';
|
||||
|
||||
const testOverview1: Overview = {
|
||||
image_url: 'TEST_IMAGE_URL',
|
||||
release: 'TEST_RELEASE',
|
||||
profiles: [
|
||||
{
|
||||
id: 'TEST_PROFILE_ID',
|
||||
target: 'TEST_TARGET',
|
||||
titles: [
|
||||
{
|
||||
title: 'TEST_TITLE1',
|
||||
},
|
||||
{
|
||||
title: 'TEST_TITLE2',
|
||||
},
|
||||
{
|
||||
vendor: 'TEST_VENDOR',
|
||||
|
||||
model: 'TEST_MODEL',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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('Search Field', () => {
|
||||
const mockOnProfileChange = jest.fn();
|
||||
const props = {
|
||||
selectedVersion: testVersion,
|
||||
onProfileChange: mockOnProfileChange,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
});
|
||||
|
||||
it('renders the component and sends a get request', async () => {
|
||||
render(<ProfileSearch {...props} />);
|
||||
|
||||
mockAxios.onGet().replyOnce(200, testOverview1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAxios.history.get).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders autocomplete and selects right option', async () => {
|
||||
const { getByTestId } = render(<ProfileSearch {...props} />);
|
||||
mockAxios.onGet().replyOnce(200, testOverview1);
|
||||
|
||||
let autocomplete: HTMLElement | null;
|
||||
await waitFor(() => {
|
||||
autocomplete = getByTestId('search-autocomplete');
|
||||
});
|
||||
const input = autocomplete!.querySelector('input');
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
autocomplete!.focus();
|
||||
fireEvent.change(input!, { target: { value: 'TESTVENMODE' } });
|
||||
fireEvent.keyDown(autocomplete!, { key: 'ArrowDown' });
|
||||
fireEvent.keyDown(autocomplete!, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input!.value).toEqual('TEST_VENDOR TEST_MODEL');
|
||||
});
|
||||
});
|
||||
|
||||
it('clearing the autocomplete should not hide the last selected profile', async () => {
|
||||
const { getByTestId } = render(<ProfileSearch {...props} />);
|
||||
mockAxios.onGet().replyOnce(200, testOverview1);
|
||||
|
||||
let autocomplete: HTMLElement | null;
|
||||
await waitFor(() => {
|
||||
autocomplete = getByTestId('search-autocomplete');
|
||||
});
|
||||
const input = autocomplete!.querySelector('input');
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
autocomplete!.focus();
|
||||
fireEvent.change(input!, { target: { value: 'TESTVENMODE' } });
|
||||
fireEvent.keyPress(input!, { key: 'Enter' });
|
||||
|
||||
expect(mockOnProfileChange).toBeCalled();
|
||||
mockOnProfileChange.mockReset();
|
||||
|
||||
const clearButton = autocomplete!.querySelector('.MuiAutocomplete-clearIndicator');
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
fireEvent.click(clearButton!);
|
||||
|
||||
await waitFor(() => expect(mockOnProfileChange).not.toBeCalled());
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ import { matchSorter } from 'match-sorter';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Overview, ProfilesEntity } from '../../../types/overview';
|
||||
import { getTitle } from '../utils/title';
|
||||
|
||||
type Props = {
|
||||
selectedVersion: string;
|
||||
|
|
@ -17,7 +18,7 @@ type SearchData = { value: ProfilesEntity; search: string; title: string };
|
|||
|
||||
const overviewData: { [key: string]: Overview } = {};
|
||||
|
||||
const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChange }) => {
|
||||
const ProfileSearch: FunctionComponent<Props> = ({ selectedVersion, onProfileChange }) => {
|
||||
const [searchData, setSearchData] = useState<SearchData[]>([]);
|
||||
const [working, toggleWorking] = useState<boolean>(true);
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -38,12 +39,13 @@ const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChang
|
|||
|
||||
toggleWorking(false);
|
||||
|
||||
overview.profiles?.forEach((profile) => {
|
||||
profile.titles?.forEach((title) => {
|
||||
overview.profiles.forEach((profile) => {
|
||||
profile.titles.forEach((titleEntity) => {
|
||||
const title = getTitle(titleEntity);
|
||||
searchDataArray.push({
|
||||
value: profile,
|
||||
search: profile.id + title.title,
|
||||
title: title.title || `${title.vendor} ${title.model}`,
|
||||
search: profile.id + title,
|
||||
title,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -58,8 +60,7 @@ const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChang
|
|||
}, [getSearchData, searchData, selectedVersion]);
|
||||
|
||||
const handleProfileSelect = (_: unknown, searchDataRow: SearchData | null) => {
|
||||
if (!searchDataRow) return;
|
||||
onProfileChange(searchDataRow.value);
|
||||
if (searchDataRow) onProfileChange(searchDataRow.value);
|
||||
};
|
||||
|
||||
const getOptionLabel = (option: SearchData) => option.title;
|
||||
|
|
@ -85,6 +86,7 @@ const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChang
|
|||
|
||||
return (
|
||||
<Autocomplete
|
||||
data-testid="search-autocomplete"
|
||||
options={searchData}
|
||||
getOptionLabel={getOptionLabel}
|
||||
renderInput={renderInput}
|
||||
|
|
@ -94,4 +96,4 @@ const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChang
|
|||
);
|
||||
};
|
||||
|
||||
export default SearchField;
|
||||
export default ProfileSearch;
|
||||
46
src/containers/home/components/VersionSelector.test.tsx
Normal file
46
src/containers/home/components/VersionSelector.test.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import VersionSelector from './VersionSelector';
|
||||
|
||||
jest.mock('../../../config', () => ({
|
||||
versions: { TEST_1: 'data/TEST_1', TEST_2: 'data/TEST_2' },
|
||||
}));
|
||||
|
||||
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('Search Field', () => {
|
||||
const mockOnVersionChange = jest.fn();
|
||||
const props = {
|
||||
selectedVersion: 'TEST_1',
|
||||
onVersionChange: mockOnVersionChange,
|
||||
};
|
||||
|
||||
it('renders the component and sends a get request', async () => {
|
||||
const { getByText, getByRole } = render(<VersionSelector {...props} />);
|
||||
|
||||
const select = await waitFor(() => getByRole('button'));
|
||||
expect(select).toBeInTheDocument();
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
const optionTest2 = await waitFor(() => getByText('TEST_2'));
|
||||
expect(optionTest2).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(optionTest2);
|
||||
|
||||
expect(mockOnVersionChange.mock.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -26,6 +26,7 @@ const VersionSelector: FunctionComponent<Props> = ({ selectedVersion, onVersionC
|
|||
labelId="version-select-label"
|
||||
value={selectedVersion}
|
||||
onChange={handleVersionChange}
|
||||
data-testid="version-select"
|
||||
>
|
||||
{Object.keys(versions).map((version) => (
|
||||
<MenuItem value={version} key={version}>
|
||||
|
|
|
|||
31
src/containers/home/home.test.tsx
Normal file
31
src/containers/home/home.test.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import Home from './home';
|
||||
import VersionSelector from './components/VersionSelector';
|
||||
|
||||
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('Home', () => {
|
||||
it('renders the component and sends a get request', async () => {
|
||||
const { getByTestId, findByRole } = render(<Home />);
|
||||
|
||||
const versionSelect = getByTestId('version-select');
|
||||
fireEvent.change(versionSelect, { value: '1234' });
|
||||
|
||||
expect(versionSelect).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { Container, Paper, Box, Typography, Grid } from '@material-ui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SearchField from './components/SearchField';
|
||||
import ProfileSearch from './components/ProfileSearch';
|
||||
import VersionSelector from './components/VersionSelector';
|
||||
import ProfileDetails from './components/ProfileDetails';
|
||||
import config from '../../config';
|
||||
|
|
@ -12,14 +12,6 @@ const Home: FunctionComponent = () => {
|
|||
const [selectedProfile, setSelectedProfile] = useState<ProfilesEntity | null>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onVersionChange = (version: string) => {
|
||||
setSelectedVersion(version);
|
||||
};
|
||||
|
||||
const onProfileChange = (profile: ProfilesEntity) => {
|
||||
setSelectedProfile(profile);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box paddingY={4}>
|
||||
|
|
@ -37,12 +29,17 @@ const Home: FunctionComponent = () => {
|
|||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs>
|
||||
<SearchField selectedVersion={selectedVersion} onProfileChange={onProfileChange} />
|
||||
<ProfileSearch
|
||||
selectedVersion={selectedVersion}
|
||||
onProfileChange={setSelectedProfile}
|
||||
data-testid="profile-search"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<VersionSelector
|
||||
data-testid="version-selector"
|
||||
selectedVersion={selectedVersion}
|
||||
onVersionChange={onVersionChange}
|
||||
onVersionChange={setSelectedVersion}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
|||
29
src/containers/home/utils/title.test.ts
Normal file
29
src/containers/home/utils/title.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { TitlesEntity } from '../../../types/overview';
|
||||
import { getTitle } from './title';
|
||||
|
||||
describe('getTitle', () => {
|
||||
it('returns the correct title', () => {
|
||||
const data: {
|
||||
title: TitlesEntity;
|
||||
expected: string;
|
||||
}[] = [
|
||||
{
|
||||
title: {
|
||||
title: 'TEST',
|
||||
},
|
||||
expected: 'TEST',
|
||||
},
|
||||
{
|
||||
title: {
|
||||
vendor: 'TEST',
|
||||
model: 'MODEL',
|
||||
},
|
||||
expected: 'TEST MODEL',
|
||||
},
|
||||
];
|
||||
|
||||
data.forEach((element) => {
|
||||
expect(getTitle(element.title)).toStrictEqual(element.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
6
src/containers/home/utils/title.ts
Normal file
6
src/containers/home/utils/title.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { TitlesEntity } from '../../../types/overview';
|
||||
|
||||
export const getTitle = (title: TitlesEntity): string =>
|
||||
title.title || `${title.vendor} ${title.model}`;
|
||||
|
||||
export default { getTitle };
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
export interface Overview {
|
||||
image_url: string;
|
||||
profiles?: ProfilesEntity[] | null;
|
||||
profiles: ProfilesEntity[];
|
||||
release: string;
|
||||
}
|
||||
export interface ProfilesEntity {
|
||||
id: string;
|
||||
target: string;
|
||||
titles?: TitlesEntity[] | null;
|
||||
titles: TitlesEntity[];
|
||||
}
|
||||
export interface TitlesEntity {
|
||||
title?: string;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ export interface Profile {
|
|||
device_packages?: string[] | null;
|
||||
id: string;
|
||||
image_prefix?: string;
|
||||
images?: ImagesEntity[] | null;
|
||||
images: ImagesEntity[];
|
||||
metadata_version?: number;
|
||||
target: string;
|
||||
titles?: TitlesEntity[] | null;
|
||||
titles: TitlesEntity[];
|
||||
version_code: string;
|
||||
version_number: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue