Skip to content

Commit

Permalink
Downloads tests and improvements (#1116)
Browse files Browse the repository at this point in the history
* Convert components to typescript and create a snapshot test

* More tests for downloads container
nukeop authored Dec 5, 2021
1 parent 86fd3b2 commit d64590e
Showing 16 changed files with 643 additions and 248 deletions.
1 change: 1 addition & 0 deletions packages/app/__mocks__/@nuclear/core.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ module.exports = {
React: jest.fn(),
ReactDOM: jest.fn()
}),
setOption: jest.fn(),
getOption: () => '',
rest: {
LastFmApi: class {
14 changes: 7 additions & 7 deletions packages/app/app/actions/downloads.ts
Original file line number Diff line number Diff line change
@@ -15,13 +15,13 @@ export const DOWNLOAD_ERROR = 'DOWNLOAD_ERROR';
export const DOWNLOAD_REMOVED = 'DOWNLOAD_REMOVED';
export const CLEAR_FINISHED_DOWNLOADS = 'CLEAR_FINISHED_DOWNLOADS';

export const DownloadStatus = {
WAITING: 'Waiting',
STARTED: 'Started',
PAUSED: 'Paused',
FINISHED: 'Finished',
ERROR: 'Error'
};
export enum DownloadStatus {
WAITING = 'Waiting',
STARTED = 'Started',
PAUSED = 'Paused',
FINISHED = 'Finished',
ERROR = 'Error'
}

const changePropertyForItem = ({downloads, uuid, propertyName='status', value}) => {
const changedItem = _.find(downloads, (item) => item.track.uuid === uuid);
2 changes: 1 addition & 1 deletion packages/app/app/actions/settings.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export function setBooleanOption(option, state, fromMain?) {
};
}

export function setStringOption(option, state, fromMain) {
export function setStringOption(option, state, fromMain?) {
setOption(option, state);

return {
67 changes: 0 additions & 67 deletions packages/app/app/components/Downloads/DownloadsHeader/index.js

This file was deleted.

57 changes: 57 additions & 0 deletions packages/app/app/components/Downloads/DownloadsHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useCallback } from 'react';
import _ from 'lodash';
import {
Button,
Icon,
Segment
} from 'semantic-ui-react';
import { remote } from 'electron';
import { useTranslation } from 'react-i18next';

import { setStringOption } from '../../../actions/settings';
import styles from './styles.scss';

type DownloadsHeaderProps = {
directory: string;
setStringOption: typeof setStringOption;
};

const DownloadsHeader: React.FC<DownloadsHeaderProps> = ({
directory,
setStringOption
}) => {
const { t } = useTranslation('settings');
const setDirectory = useCallback(async () => {
const dialogResult = await remote.dialog.showOpenDialog({
properties: ['openDirectory']
});
if (!dialogResult.canceled && !_.isEmpty(dialogResult.filePaths)) {
setStringOption(
'downloads.dir',
_.head(dialogResult.filePaths)
);
}
}, [setStringOption]);

return (
<Segment className={styles.downloads_header}>
<span className={styles.label}>
{t('saving-in')}
<span className={styles.directory}>
{_.isEmpty(directory) ? remote.app.getPath('downloads') : directory}
</span>
</span>
<Button
icon
inverted
labelPosition='left'
onClick={setDirectory}
>
<Icon name='folder open' />
{t('downloads-dir-button')}
</Button>
</Segment>
);
};

export default DownloadsHeader;
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { Icon, Table } from 'semantic-ui-react';
import { Icon, SemanticICONS, Table } from 'semantic-ui-react';
import _ from 'lodash';

import { DownloadStatus } from '../../../actions/downloads';
import styles from './styles.scss';

const StatusIcon = props => {
switch (props.status) {
export type DownloadsItemProps = {
item: {
status: DownloadStatus;
completion: number;
track: {
uuid: string;
name: string;
artist: string | {
name: string;
};
}
};

resumeDownload: (id: string) => void;
pauseDownload: (id: string) => void;
removeDownload: (id: string) => void;
}

type StatusIconProps = Pick<DownloadsItemProps['item'], 'status'>;

const StatusIcon: React.FC<StatusIconProps> = ({ status }) => {
switch (status) {
case 'Waiting':
return <Icon name='hourglass start' />;
case 'Paused':
@@ -21,10 +41,11 @@ const StatusIcon = props => {
}
};

const renderAction = (name, callback) => (
const renderAction = (name: SemanticICONS, callback: React.MouseEventHandler) => (
<a
onClick={callback}
data-testid='download-action'
href='#'
onClick={callback}
>
<Icon fitted name={name} />
</a>
@@ -44,22 +65,8 @@ const ActionIcon = props => {
}
};

StatusIcon.propTypes = {
status: PropTypes.string.isRequired
};

ActionIcon.propTypes = {
item: PropTypes.PropTypes.shape({
status: PropTypes.string.isRequired,
track: PropTypes.PropTypes.shape({
uuid: PropTypes.string.isRequired
})
}),
resumeDownload: PropTypes.func.isRequired,
pauseDownload: PropTypes.func.isRequired
};

const DownloadsItem = ({
const DownloadsItem: React.FC<DownloadsItemProps> = ({
item,
resumeDownload,
pauseDownload,
@@ -84,27 +91,21 @@ const DownloadsItem = ({
{_.round(item.completion * 100, 0) + '%'}
</Table.Cell>
<Table.Cell className={styles.item_buttons}>
<ActionIcon resumeDownload={onResumeClick} pauseDownload={onPauseClick} item={item} />
<a href='#' onClick={onRemoveClick}>
<ActionIcon
resumeDownload={onResumeClick}
pauseDownload={onPauseClick}
item={item}
/>
<a
data-testid='remove-download'
href='#'
onClick={onRemoveClick}
>
<Icon fitted name='times' />
</a>
</Table.Cell>
</Table.Row>
);
};

DownloadsItem.propTypes = {
item: PropTypes.shape({

}),
resumeDownload: PropTypes.func.isRequired,
pauseDownload: PropTypes.func.isRequired
};

DownloadsItem.defaultProps = {
item: {},
pauseDownload: () => { },
resumeDownload: () => { }
};

export default DownloadsItem;
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button, Icon, Segment, Table } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';

import DownloadsItem from '../DownloadsItem';

import DownloadsItem, { DownloadsItemProps } from '../DownloadsItem';
import styles from './styles.scss';

const DownloadsList = ({
type DownloadsListProps = {
items: DownloadsItemProps['item'][];
clearFinishedTracks: React.MouseEventHandler;
resumeDownload: (id: string) => void;
pauseDownload: (id: string) => void;
removeDownload: (id: string) => void;
};

const DownloadsList: React.FC<DownloadsListProps> = ({
items,
clearFinishedTracks,
pauseDownload,
resumeDownload,
removeDownload
}) => {
const [sortAsc, setSort] = useState(true);
const [sortAsc, setSortAsc] = useState(true);
const { t } = useTranslation('downloads');

const onSort = () => {
const sortResult = items.sort((a, b) => (a.track.name.toLowerCase() > b.track.name.toLowerCase())
? sortAsc ? 1 : -1
: sortAsc ? -1 : 1
);
setSortAsc(!sortAsc);
return sortResult;
};

return (
<Segment inverted>
<Button primary onClick={clearFinishedTracks}>
<Icon name='trash'/>
<Icon name='trash' />
{t('clear')}
</Button>
<Table inverted className={styles.downloads_list}>
<Table.Header>
<Table.Row>
<Table.HeaderCell>{t('status')}</Table.HeaderCell>
<Table.HeaderCell onClick={() => {
if (sortAsc){
items.sort((a, b) => {
return a.track.name.toLowerCase() > b.track.name.toLowerCase();
});
setSort(false);
} else {
items.sort((a, b) => {
return a.track.name.toLowerCase() < b.track.name.toLowerCase();
});
setSort(true);
}
}
}>{t('name')} {
<Table.HeaderCell
onClick={onSort}
>
{t('name')} {
sortAsc ? <Icon name='caret up' /> : <Icon name='caret down' />
}
</Table.HeaderCell>
@@ -67,18 +73,4 @@ const DownloadsList = ({
);
};

DownloadsList.propTypes = {
items: PropTypes.array,
clearFinishedTracks: PropTypes.func,
pauseDownload: PropTypes.func.isRequired,
resumeDownload: PropTypes.func.isRequired
};

DownloadsList.defaultProps = {
items: [],
clearFinishedTracks: () => {},
pauseDownload: () => {},
resumeDownload: () => {}
};

export default DownloadsList;
Loading

0 comments on commit d64590e

Please sign in to comment.