From 8f0871ab8cb736c97a4d63d7ad62967034a2255f Mon Sep 17 00:00:00 2001 From: Anas WS Date: Thu, 29 Feb 2024 11:51:05 +0530 Subject: [PATCH] Added loading in reducer and customstyled added --- app/components/TrackCard/index.js | 82 ++++++--- app/containers/TrackContainer/index.js | 43 ++--- app/containers/TrackContainer/reducer.js | 6 +- app/containers/TrackContainer/selectors.js | 6 +- .../tests/__snapshots__/index.test.js.snap | 126 ------------- .../TrackContainer/tests/index.test.js | 167 ------------------ .../TrackContainer/tests/reducer.test.js | 54 ------ .../TrackContainer/tests/saga.test.js | 50 ------ .../TrackContainer/tests/selectors.test.js | 41 ----- 9 files changed, 89 insertions(+), 486 deletions(-) delete mode 100644 app/containers/TrackContainer/tests/__snapshots__/index.test.js.snap delete mode 100644 app/containers/TrackContainer/tests/index.test.js delete mode 100644 app/containers/TrackContainer/tests/reducer.test.js delete mode 100644 app/containers/TrackContainer/tests/saga.test.js delete mode 100644 app/containers/TrackContainer/tests/selectors.test.js diff --git a/app/components/TrackCard/index.js b/app/components/TrackCard/index.js index b2e565e..0f71d54 100644 --- a/app/components/TrackCard/index.js +++ b/app/components/TrackCard/index.js @@ -7,6 +7,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Card } from '@mui/material'; +import styled from '@emotion/styled'; import T from '@components/T'; import If from '@components/If'; import isEmpty from 'lodash/isEmpty'; @@ -18,38 +19,75 @@ import Typography from '@mui/material/Typography'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; +const TrackCustomCard = styled(Card)` + && { + display: flex; + margin: 1rem; + width: 32rem; + justify-content: space-between; + :hover { + box-shadow: 5; + } + transition: all 0.25s linear; + } +`; + +const TrackContentBox = styled(Box)` + && { + display: flex; + flex-direction: column; + } +`; + +const TrackMedia = styled(CardMedia)` + && { + width: 10rem; + } +`; + +const TrackContentHeaderBox = styled(TrackContentBox)` + && { + flex-direction: row; + align-items: center; + padding-bottom: 1rem; + } +`; + +const TrackPauseIcon = styled(PauseIcon)` + && { + height: 2rem; + width: 2rem; + } +`; + +const TrackPlayIcon = styled(PlayArrowIcon)` + && { + height: 2rem; + width: 2rem; + } +`; + export function TrackCard({ collectionName, artistName, shortDescription, artworkUrl100, previewUrl }) { const [playTrack, setPlayTrack] = useState(false); const shortDesc = shortDescription && shortDescription.length > 38 ? `${shortDescription.substring(0, 38)}...` : shortDescription; return ( - - - - + + + + setPlayTrack(!playTrack)}> - }> - + }> + - {collectionName.length > 18 ? `${collectionName.substring(0, 18)}...` : collectionName} + {collectionName && collectionName.length > 18 ? `${collectionName.substring(0, 18)}...` : collectionName} - + - - + + - + ); } diff --git a/app/containers/TrackContainer/index.js b/app/containers/TrackContainer/index.js index f526cce..1666e92 100644 --- a/app/containers/TrackContainer/index.js +++ b/app/containers/TrackContainer/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, memo, useState } from 'react'; +import React, { useEffect, memo } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; @@ -16,11 +16,12 @@ import If from '@components/If'; import For from '@components/For'; import TrackCard from '@components/TrackCard'; import colors from '@app/themes/colors'; -import { selectReposData, selectReposError, selectRepoName } from './selectors'; +import { selectReposData, selectReposError, selectRepoName, selectRepoLoading } from './selectors'; import { trackContainerCreators } from './reducer'; import trackContainerSaga from './saga'; import { translate } from '@app/utils/index'; +// Custom Styling const CustomCard = styled(Card)` && { margin: 1.25rem 0; @@ -56,13 +57,11 @@ const RightContent = styled.div` display: flex; align-self: flex-end; `; - const StyledT = styled(T)` && { color: ${colors.gotoStories}; } `; - const StyledOutlinedInput = styled(OutlinedInput)` legend { display: none; @@ -79,37 +78,29 @@ export function TrackContainer({ tracksError, trackName, maxwidth, - padding + padding, + loading }) { - const [loading, setLoading] = useState(false); const history = useHistory(); - useEffect(() => { - const loaded = get(tracksData, 'results', null) || tracksError; - if (loaded) { - setLoading(false); - } - }, [tracksData]); - useEffect(() => { if (trackName && !tracksData?.results?.length) { dispatchItunesTracks(trackName); - setLoading(true); } }, []); - const searchRepos = (rName) => { - dispatchItunesTracks(rName); - setLoading(true); + const searchTracks = (tName) => { + dispatchItunesTracks(tName); }; - const handleOnChange = (rName) => { - if (!isEmpty(rName)) { - searchRepos(rName); + const handleOnChange = (tName) => { + if (!isEmpty(tName)) { + searchTracks(tName); } else { dispatchClearItunesTracks(); } }; + const debouncedHandleOnChange = debounce(handleOnChange, 200); const renderSkeleton = () => { @@ -151,6 +142,7 @@ export function TrackContainer({ ); }; + const renderErrorState = () => { let trackError; if (tracksError) { @@ -197,7 +189,7 @@ export function TrackContainer({ data-testid="search-icon" aria-label="search tracks" type="button" - onClick={() => searchRepos(trackName)} + onClick={() => searchTracks(trackName)} > @@ -223,20 +215,23 @@ TrackContainer.propTypes = { trackName: PropTypes.string, history: PropTypes.object, maxwidth: PropTypes.number, - padding: PropTypes.number + padding: PropTypes.number, + loading: PropTypes.boolean }; TrackContainer.defaultProps = { maxwidth: 500, padding: 20, tracksData: {}, - tracksError: null + tracksError: null, + loading: false }; const mapStateToProps = createStructuredSelector({ tracksData: selectReposData(), tracksError: selectReposError(), - trackName: selectRepoName() + trackName: selectRepoName(), + loading: selectRepoLoading() }); export function mapDispatchToProps(dispatch) { diff --git a/app/containers/TrackContainer/reducer.js b/app/containers/TrackContainer/reducer.js index 48b2c45..6be2eca 100644 --- a/app/containers/TrackContainer/reducer.js +++ b/app/containers/TrackContainer/reducer.js @@ -13,7 +13,7 @@ export const { Types: trackContainerTypes, Creators: trackContainerCreators } = failureGetItunesTracks: ['error'], clearItunesTracks: {} }); -export const initialState = { trackName: null, tracksData: {}, tracksError: null }; +export const initialState = { trackName: null, tracksData: {}, tracksError: null, loading: false }; /* eslint-disable default-case, no-param-reassign */ export const trackContainerReducer = (state = initialState, action) => @@ -21,17 +21,21 @@ export const trackContainerReducer = (state = initialState, action) => switch (action.type) { case trackContainerTypes.REQUEST_GET_ITUNES_TRACKS: draft.trackName = action.trackName; + draft.loading = true; break; case trackContainerTypes.CLEAR_ITUNES_TRACKS: draft.trackName = null; draft.tracksError = null; draft.tracksData = {}; + draft.loading = false; break; case trackContainerTypes.SUCCESS_GET_ITUNES_TRACKS: draft.tracksData = action.data; + draft.loading = false; break; case trackContainerTypes.FAILURE_GET_ITUNES_TRACKS: draft.tracksError = get(action.error, 'message', 'something_went_wrong'); + draft.loading = false; break; } }); diff --git a/app/containers/TrackContainer/selectors.js b/app/containers/TrackContainer/selectors.js index 5e12bdb..edafde4 100644 --- a/app/containers/TrackContainer/selectors.js +++ b/app/containers/TrackContainer/selectors.js @@ -22,4 +22,8 @@ export const selectReposData = () => export const selectReposError = () => createSelector(selecttrackContainerDomain, (substate) => get(substate, 'tracksError')); -export const selectRepoName = () => createSelector(selecttrackContainerDomain, (substate) => get(substate, 'trackName')); +export const selectRepoName = () => + createSelector(selecttrackContainerDomain, (substate) => get(substate, 'trackName')); + +export const selectRepoLoading = () => + createSelector(selecttrackContainerDomain, (substate) => get(substate, 'loading')); diff --git a/app/containers/TrackContainer/tests/__snapshots__/index.test.js.snap b/app/containers/TrackContainer/tests/__snapshots__/index.test.js.snap deleted file mode 100644 index c980ba1..0000000 --- a/app/containers/TrackContainer/tests/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,126 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` tests should render and match the snapshot 1`] = ` - -
-
-
-

- Go to Storybook -

-
-
-
-
- - Repository Search - -
-
-
-

- Get details of tracksitories -

-
- -
- -
- -
-
-
-
-
- - Repository List - -
-
-
-

- Search for a tracksitory by entering it's name in the search box -

-
-
-
- -`; diff --git a/app/containers/TrackContainer/tests/index.test.js b/app/containers/TrackContainer/tests/index.test.js deleted file mode 100644 index ea841b7..0000000 --- a/app/containers/TrackContainer/tests/index.test.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * - * Tests for TrackContainer - * - */ - -import React from 'react'; -import { Router } from 'react-router'; -import { useHistory } from 'react-router-dom'; -import { fireEvent } from '@testing-library/dom'; -import { timeout, renderProvider } from '@utils/testUtils'; -import { trackContainerTest as TrackContainer, mapDispatchToProps } from '../index'; -import { trackContainerTypes } from '../reducer'; -import { createBrowserHistory } from 'history'; -import { translate } from '@app/utils/index'; - -describe(' tests', () => { - let submitSpy; - - beforeEach(() => { - submitSpy = jest.fn(); - }); - it('should render and match the snapshot', () => { - const { baseElement } = renderProvider(); - expect(baseElement).toMatchSnapshot(); - }); - - it('should call dispatchClearItunesTracks on empty change', async () => { - const getItunesTracksSpy = jest.fn(); - const clearItunesTracksSpy = jest.fn(); - const { getByTestId } = renderProvider( - - ); - fireEvent.change(getByTestId('search-bar'), { - target: { value: 'a' } - }); - await timeout(500); - expect(getItunesTracksSpy).toBeCalled(); - fireEvent.change(getByTestId('search-bar'), { - target: { value: '' } - }); - await timeout(500); - expect(clearItunesTracksSpy).toBeCalled(); - }); - - it('should call dispatchItunesTracks on change and after enter', async () => { - const trackName = 'react-template'; - const { getByTestId } = renderProvider(); - const searchBar = getByTestId('search-bar'); - fireEvent.change(searchBar, { - target: { value: trackName } - }); - await timeout(500); - expect(submitSpy).toBeCalledWith(trackName); - - fireEvent.keyDown(searchBar, { - key: 'Enter', - code: 13, - charCode: 13 - }); - expect(submitSpy).toBeCalledWith(trackName); - }); - - it('should call dispatchItunesTracks on clicking the search icon', async () => { - const trackName = 'react-template'; - const { getByTestId } = renderProvider(); - fireEvent.click(getByTestId('search-icon')); - - await timeout(500); - expect(submitSpy).toBeCalledWith(trackName); - }); - - it('should dispatchItunesTracks on update on mount if trackName is already persisted', async () => { - const trackName = 'react-template'; - renderProvider(); - - await timeout(500); - expect(submitSpy).toBeCalledWith(trackName); - }); - - it('should validate mapDispatchToProps actions', async () => { - const dispatchReposSearchSpy = jest.fn(); - const trackName = 'react-template'; - const actions = { - dispatchItunesTracks: { trackName, type: trackContainerTypes.REQUEST_GET_ITUNES_TRACKS }, - dispatchClearItunesTracks: { type: trackContainerTypes.CLEAR_ITUNES_TRACKS } - }; - - const props = mapDispatchToProps(dispatchReposSearchSpy); - props.dispatchItunesTracks(trackName); - expect(dispatchReposSearchSpy).toHaveBeenCalledWith(actions.dispatchItunesTracks); - - await timeout(500); - props.dispatchClearItunesTracks(); - expect(dispatchReposSearchSpy).toHaveBeenCalledWith(actions.dispatchClearItunesTracks); - }); - - it('should render default error message when search goes wrong', () => { - const defaultError = translate('something_went_wrong'); - const { getByTestId } = renderProvider(); - expect(getByTestId('error-message')).toBeInTheDocument(); - expect(getByTestId('error-message').textContent).toBe(defaultError); - }); - - it('should render the default message when searchBox is empty and tracksError is null', () => { - const defaultMessage = translate('track_search_default'); - const { getByTestId } = renderProvider(); - expect(getByTestId('default-message')).toBeInTheDocument(); - expect(getByTestId('default-message').textContent).toBe(defaultMessage); - }); - - it('should render the data when loading becomes false', () => { - const tracksData = { items: [{ trackOne: 'react-template' }] }; - const { getByTestId } = renderProvider(); - expect(getByTestId('for')).toBeInTheDocument(); - }); - - it('should render exact number of RepoCards as per totalCount in result', () => { - const totalCount = 3; - const tracksData = { - totalCount, - items: [ - { - name: 'react-tempalte', - fullName: 'wednesday-solutions/react-template', - stargazersCount: 200 - }, - { - name: 'react', - fullName: 'wednesday-solutions/react', - stargazersCount: 100 - }, - { - name: 'react-tempalte2', - fullName: 'wednesday-solutions/react-template2', - stargazersCount: 300 - } - ] - }; - const { getAllByTestId } = renderProvider(); - expect(getAllByTestId('track-card').length).toBe(totalCount); - }); - - it('should redirect to /stories when clicked on Clickable component', async () => { - const history = createBrowserHistory(); - const { getByTestId } = renderProvider( - - - - ); - const h = useHistory(); - const historySpy = jest.spyOn(h, 'push'); - fireEvent.click(getByTestId('redirect')); - await timeout(500); - expect(historySpy).toHaveBeenCalledWith('/stories'); - }); - - it('should render Skeleton Comp when "loading" is true', async () => { - const trackName = 'some track'; - const { getByTestId, getAllByTestId } = renderProvider( - - ); - fireEvent.change(getByTestId('search-bar'), { target: { value: trackName } }); - await timeout(500); - expect(getAllByTestId('skeleton').length).toBe(3); - }); -}); diff --git a/app/containers/TrackContainer/tests/reducer.test.js b/app/containers/TrackContainer/tests/reducer.test.js deleted file mode 100644 index 16175af..0000000 --- a/app/containers/TrackContainer/tests/reducer.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import { trackContainerReducer, initialState, trackContainerTypes } from '../reducer'; - -/* eslint-disable default-case, no-param-reassign */ -describe('HomContainer reducer tests', () => { - let state; - beforeEach(() => { - state = initialState; - }); - - it('should return the initial state', () => { - expect(trackContainerReducer(undefined, {})).toEqual(state); - }); - - it('should return the initial state when an action of type FETCH_USER is dispatched', () => { - const trackName = 'Mohammed Ali Chherawalla'; - const expectedResult = { ...state, trackName }; - expect( - trackContainerReducer(state, { - type: trackContainerTypes.REQUEST_GET_ITUNES_TRACKS, - trackName - }) - ).toEqual(expectedResult); - }); - - it('should ensure that the user data is present and userLoading = false when FETCH_USER_SUCCESS is dispatched', () => { - const data = { name: 'Mohammed Ali Chherawalla' }; - const expectedResult = { ...state, tracksData: data }; - expect( - trackContainerReducer(state, { - type: trackContainerTypes.SUCCESS_GET_ITUNES_TRACKS, - data - }) - ).toEqual(expectedResult); - }); - - it('should ensure that the userErrorMessage has some data and userLoading = false when FETCH_USER_FAILURE is dispatched', () => { - const error = 'something_went_wrong'; - const expectedResult = { ...state, tracksError: error }; - expect( - trackContainerReducer(state, { - type: trackContainerTypes.FAILURE_GET_ITUNES_TRACKS, - error - }) - ).toEqual(expectedResult); - }); - - it('should return the initial state when CLEAR_ITUNES_TRACKS is dispatched', () => { - expect( - trackContainerReducer(state, { - type: trackContainerTypes.CLEAR_ITUNES_TRACKS - }) - ).toEqual(initialState); - }); -}); diff --git a/app/containers/TrackContainer/tests/saga.test.js b/app/containers/TrackContainer/tests/saga.test.js deleted file mode 100644 index a10428a..0000000 --- a/app/containers/TrackContainer/tests/saga.test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Test trackContainer sagas - */ - -/* eslint-disable redux-saga/yield-effects */ -import { takeLatest, call, put } from 'redux-saga/effects'; -import { getTracks } from '@services/trackApi'; -import { apiResponseGenerator } from '@utils/testUtils'; -import trackContainerSaga, { getItunesTracks } from '../saga'; -import { trackContainerTypes } from '../reducer'; - -describe('TrackContainer saga tests', () => { - const generator = trackContainerSaga(); - const trackName = 'mac'; - let getItunesTracksGenerator = getItunesTracks({ trackName }); - - it('should start task to watch for REQUEST_GET_ITUNES_TRACKS action', () => { - expect(generator.next().value).toEqual(takeLatest(trackContainerTypes.REQUEST_GET_ITUNES_TRACKS, getItunesTracks)); - }); - - it('should ensure that the action FAILURE_GET_ITUNES_TRACKS is dispatched when the api call fails', () => { - const res = getItunesTracksGenerator.next().value; - expect(res).toEqual(call(getTracks, trackName)); - const errorResponse = { - errorMessage: 'There was an error while fetching track informations.' - }; - expect(getItunesTracksGenerator.next(apiResponseGenerator(false, errorResponse)).value).toEqual( - put({ - type: trackContainerTypes.FAILURE_GET_ITUNES_TRACKS, - error: errorResponse - }) - ); - }); - - it('should ensure that the action SUCCESS_GET_ITUNES_TRACKS is dispatched when the api call succeeds', () => { - getItunesTracksGenerator = getItunesTracks({ trackName }); - const res = getItunesTracksGenerator.next().value; - expect(res).toEqual(call(getTracks, trackName)); - const tracksResponse = { - totalCount: 1, - items: [{ tracksitoryName: trackName }] - }; - expect(getItunesTracksGenerator.next(apiResponseGenerator(true, tracksResponse)).value).toEqual( - put({ - type: trackContainerTypes.SUCCESS_GET_ITUNES_TRACKS, - data: tracksResponse - }) - ); - }); -}); diff --git a/app/containers/TrackContainer/tests/selectors.test.js b/app/containers/TrackContainer/tests/selectors.test.js deleted file mode 100644 index b228bb4..0000000 --- a/app/containers/TrackContainer/tests/selectors.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { selecttrackContainerDomain, selectRepoName, selectReposData, selectReposError } from '../selectors'; -import { initialState } from '../reducer'; -describe('TrackContainer selector tests', () => { - let mockedState; - let trackName; - let tracksData; - let tracksError; - - beforeEach(() => { - trackName = 'mac'; - tracksData = { totalCount: 1, items: [{ trackName }] }; - tracksError = 'There was some error while fetching the tracksitory details'; - - mockedState = { - trackContainer: { - trackName, - tracksData, - tracksError - } - }; - }); - it('should select the trackName', () => { - const trackSelector = selectRepoName(); - expect(trackSelector(mockedState)).toEqual(trackName); - }); - - it('should select tracksData', () => { - const tracksDataSelector = selectReposData(); - expect(tracksDataSelector(mockedState)).toEqual(tracksData); - }); - - it('should select the tracksError', () => { - const tracksErrorSelector = selectReposError(); - expect(tracksErrorSelector(mockedState)).toEqual(tracksError); - }); - - it('should select the global state', () => { - const selector = selecttrackContainerDomain(initialState); - expect(selector).toEqual(initialState); - }); -});