diff --git a/src/components/Exploration/ExplorationError.spec.tsx b/src/components/Exploration/ExplorationError.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..84528c4e7f10ac68a5fa21b0922a74fc38de4e72 --- /dev/null +++ b/src/components/Exploration/ExplorationError.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { shallow } from 'enzyme' +import DuelError from 'components/Duel/DuelError' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +describe('DuelError component', () => { + it('should be rendered correctly', () => { + const component = shallow(<DuelError />).getElement + expect(component).toMatchSnapshot() + }) +}) diff --git a/src/components/Exploration/ExplorationError.tsx b/src/components/Exploration/ExplorationError.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9f1728e483f45a1e39a07e85e38e5d5246eb19c0 --- /dev/null +++ b/src/components/Exploration/ExplorationError.tsx @@ -0,0 +1,29 @@ +import React, { useCallback } from 'react' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import { useHistory } from 'react-router-dom' +import './explorationError.scss' +import StyledStopButton from 'components/CommonKit/Button/StyledStopButton' + +const ExplorationError: React.FC = () => { + const { t } = useI18n() + const history = useHistory() + + const goBack = useCallback(() => { + history.goBack() + }, [history]) + + return ( + <div className="exploration-error-container"> + <div className="exploration-error-message"> + {t('exploration.global_error')} + </div> + <div className="exploration-error-button"> + <StyledStopButton color="secondary" onClick={goBack}> + {t('exploration.error_go_back')} + </StyledStopButton> + </div> + </div> + ) +} + +export default ExplorationError diff --git a/src/components/Exploration/ExplorationOngoing.spec.tsx b/src/components/Exploration/ExplorationOngoing.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..35823c9b0e372ca95646832055253b0bf527bd35 --- /dev/null +++ b/src/components/Exploration/ExplorationOngoing.spec.tsx @@ -0,0 +1,126 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' +import { act } from 'react-dom/test-utils' +import configureStore from 'redux-mock-store' +import DuelOngoing from 'components/Duel/DuelOngoing' +import { UserChallenge } from 'models' +import { DateTime } from 'luxon' +import { UserChallengeState } from 'enum/userChallenge.enum' +import { globalStateData } from '../../../test/__mocks__/globalStateData.mock' +import { userChallengeData } from '../../../test/__mocks__/userChallengeData.mock' +import { challengeStateData } from '../../../test/__mocks__/challengeStateData.mock' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockUserChallengeUpdateFlag = jest.fn() +const mockIsChallengeDone = jest.fn() +jest.mock('services/challenge.service', () => { + return jest.fn(() => { + return { + updateUserChallenge: mockUserChallengeUpdateFlag, + isChallengeDone: mockIsChallengeDone, + } + }) +}) + +const mockHistoryPush = jest.fn() +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})) + +jest.mock('components/Duel/DuelChart', () => 'mock-duelchart') + +const mockStore = configureStore([]) + +describe('DuelOngoing component', () => { + it('should be rendered correctly', () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + challenge: challengeStateData, + }, + }) + mockIsChallengeDone.mockResolvedValueOnce({ isDone: false, isWin: false }) + const wrapper = mount( + <Provider store={store}> + <DuelOngoing userChallenge={userChallengeData[0]} /> + </Provider> + ) + expect(wrapper.find('.duel-title').text()).toEqual( + userChallengeData[1].duel.title + ) + expect(wrapper.find('.duel-goal').exists()).toBeTruthy() + expect(wrapper.find('.duel-consumption').exists()).toBeTruthy() + expect(wrapper.find('.duel-chart').exists()).toBeTruthy() + expect(wrapper.find('.caption-icon').exists()).toBeTruthy() + }) + + it('should display the result modal', async () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + challenge: challengeStateData, + }, + }) + const updatedUserChallenge: UserChallenge = { + ...userChallengeData[0], + state: UserChallengeState.DUEL, + startDate: DateTime.local().setZone('utc', { keepLocalTime: true }), + } + mockIsChallengeDone.mockResolvedValue({ isDone: true, isWin: true }) + const wrapper = mount( + <Provider store={store}> + <DuelOngoing userChallenge={updatedUserChallenge} /> + </Provider> + ) + await act(async () => { + await new Promise(resolve => setTimeout(resolve)) + wrapper.update() + }) + expect(wrapper.find('DuelResultModal').props().open).toBeTruthy() + }) + + it('should be redirected to challenges when modal button is clicked', async () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + challenge: challengeStateData, + }, + }) + const updatedUserChallenge: UserChallenge = { + ...userChallengeData[0], + state: UserChallengeState.DUEL, + startDate: DateTime.local().setZone('utc', { keepLocalTime: true }), + } + mockIsChallengeDone.mockResolvedValue({ isDone: true, isWin: true }) + mockUserChallengeUpdateFlag.mockResolvedValue(updatedUserChallenge) + const wrapper = mount( + <Provider store={store}> + <DuelOngoing userChallenge={updatedUserChallenge} /> + </Provider> + ) + await act(async () => { + await new Promise(resolve => setTimeout(resolve)) + wrapper.update() + }) + wrapper.find('StyledStopButton').simulate('click') + + await act(async () => { + await new Promise(resolve => setTimeout(resolve)) + wrapper.update() + }) + expect(mockHistoryPush).toHaveBeenCalledWith('/challenges') + }) +}) diff --git a/src/components/Exploration/ExplorationOngoing.tsx b/src/components/Exploration/ExplorationOngoing.tsx new file mode 100644 index 0000000000000000000000000000000000000000..57e3270abe633c76fbc8fc0f8241fcecd4509b0d --- /dev/null +++ b/src/components/Exploration/ExplorationOngoing.tsx @@ -0,0 +1,38 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useHistory } from 'react-router-dom' +import './explorationOngoing.scss' +import { Client, useClient } from 'cozy-client' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import { useDispatch, useSelector } from 'react-redux' +import { AppStore } from 'store' + +import { UserExploration, UserChallenge } from 'models' +import { UserChallengeUpdateFlag } from 'enum/userChallenge.enum' + +interface ExplorationOngoingProps { + userChallenge: UserChallenge +} + +const ExplorationOngoing: React.FC<ExplorationOngoingProps> = ({ + userChallenge, +}: ExplorationOngoingProps) => { + const client: Client = useClient() + const { t } = useI18n() + const { currentDataload } = useSelector( + (state: AppStore) => state.ecolyo.challenge + ) + const dispatch = useDispatch() + const history = useHistory() + + // useEffect(() => { + + // }, []) + + return ( + <> + <div className="exploration-ongoing-container"></div> + </> + ) +} + +export default ExplorationOngoing diff --git a/src/components/Exploration/ExplorationView.spec.tsx b/src/components/Exploration/ExplorationView.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8bfbfca09281fd632ada8ea3940da5053e8dc114 --- /dev/null +++ b/src/components/Exploration/ExplorationView.spec.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { shallow } from 'enzyme' +import * as reactRedux from 'react-redux' +import DuelView from 'components/Duel/DuelView' +import { UserChallengeState } from 'enum/userChallenge.enum' +import { challengeStateData } from '../../../test/__mocks__/challengeStateData.mock' +import { userChallengeData } from '../../../test/__mocks__/userChallengeData.mock' +import DuelError from './DuelError' +import DuelUnlocked from './DuelUnlocked' +import DuelOngoing from './DuelOngoing' +import { UserDuelState } from 'enum/userDuel.enum' + +const mockUseSelector = jest.spyOn(reactRedux, 'useSelector') + +describe('DuelView component', () => { + it('should be rendered with DuelError component when no current challenge', () => { + mockUseSelector.mockReturnValue(challengeStateData) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelError).exists()).toBeTruthy() + }) + + it('should be rendered with DuelError component when current challenge with state = duel and duel state = done', () => { + const updatedUserChallenge = { + ...userChallengeData[1], + state: UserChallengeState.DUEL, + duel: { ...userChallengeData[1].duel, state: UserDuelState.DONE }, + } + const updatedChallengeState = { + ...challengeStateData, + currentChallenge: updatedUserChallenge, + } + mockUseSelector.mockReturnValue(updatedChallengeState) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelError).exists()).toBeTruthy() + }) + + it('should be rendered with DuelError component when current challenge with state = duel and duel state = locked', () => { + const updatedUserChallenge = { + ...userChallengeData[1], + state: UserChallengeState.DUEL, + duel: { ...userChallengeData[1].duel, state: UserDuelState.LOCKED }, + } + const updatedChallengeState = { + ...challengeStateData, + currentChallenge: updatedUserChallenge, + } + mockUseSelector.mockReturnValue(updatedChallengeState) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelError).exists()).toBeTruthy() + }) + + it('should be rendered with DuelError component when current challenge with state != duel ', () => { + const updatedUserChallenge = { + ...userChallengeData[1], + state: UserChallengeState.ONGOING, + } + const updatedChallengeState = { + ...challengeStateData, + currentChallenge: updatedUserChallenge, + } + mockUseSelector.mockReturnValue(updatedChallengeState) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelError).exists()).toBeTruthy() + }) + + it('should be rendered with DuelUnlocked component when current challenge with state = duel and duel state = unlocked', () => { + const updatedUserChallenge = { + ...userChallengeData[1], + state: UserChallengeState.DUEL, + duel: { ...userChallengeData[1].duel, state: UserDuelState.UNLOCKED }, + } + const updatedChallengeState = { + ...challengeStateData, + currentChallenge: updatedUserChallenge, + } + mockUseSelector.mockReturnValue(updatedChallengeState) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelUnlocked).exists()).toBeTruthy() + }) + + it('should be rendered with DuelOngoing component when current challenge with state = duel and duel state = ongoing', () => { + const updatedUserChallenge = { + ...userChallengeData[1], + state: UserChallengeState.DUEL, + duel: { ...userChallengeData[1].duel, state: UserDuelState.ONGOING }, + } + const updatedChallengeState = { + ...challengeStateData, + currentChallenge: updatedUserChallenge, + } + mockUseSelector.mockReturnValue(updatedChallengeState) + const wrapper = shallow(<DuelView />) + expect(wrapper.find(DuelOngoing).exists()).toBeTruthy() + }) +}) diff --git a/src/components/Exploration/ExplorationView.tsx b/src/components/Exploration/ExplorationView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..80be928f8e49be36a3818f6cd8b8a5ff83a48044 --- /dev/null +++ b/src/components/Exploration/ExplorationView.tsx @@ -0,0 +1,57 @@ +import React, { useCallback, useState } from 'react' +import { useSelector } from 'react-redux' +import { AppStore } from 'store' +import CozyBar from 'components/Header/CozyBar' +import Content from 'components/Content/Content' +import Header from 'components/Header/Header' +import { UserChallengeState } from 'enum/userChallenge.enum' + +import { UserChallenge } from 'models' + +import { useHistory } from 'react-router-dom' +import { UserExplorationState } from 'enum/userExploration.enum' +import ExplorationError from './ExplorationError' +import ExplorationOngoing from './ExplorationOngoing' + +const ExplorationView: React.FC = () => { + const [headerHeight, setHeaderHeight] = useState<number>(0) + const { currentChallenge } = useSelector( + (state: AppStore) => state.ecolyo.challenge + ) + const history = useHistory() + const defineHeaderHeight = useCallback((height: number) => { + setHeaderHeight(height) + }, []) + + const renderExploration = (challenge: UserChallenge) => { + switch (challenge.exploration.state) { + case UserExplorationState.ONGOING: + return <ExplorationOngoing userChallenge={challenge} /> + default: + return <ExplorationError /> + } + } + + return ( + <> + <CozyBar titleKey={'COMMON.APP_DUEL_TITLE'} displayBackArrow={true} /> + <Header + setHeaderHeight={defineHeaderHeight} + desktopTitleKey={'COMMON.APP_DUEL_TITLE'} + displayBackArrow={true} + ></Header> + <Content height={headerHeight}> + <div> + {currentChallenge && + currentChallenge.state === UserChallengeState.DUEL ? ( + renderExploration(currentChallenge) + ) : ( + <ExplorationError /> + )} + </div> + </Content> + </> + ) +} + +export default ExplorationView diff --git a/src/components/Exploration/explorationError.scss b/src/components/Exploration/explorationError.scss new file mode 100644 index 0000000000000000000000000000000000000000..de5b9d02f9676afbd041a997cdf0cf52f2e0968d --- /dev/null +++ b/src/components/Exploration/explorationError.scss @@ -0,0 +1,19 @@ +@import '../../styles/base/typography'; + +.exploration-error-container{ + display: flex; + min-height: 60vh; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem 1.5rem; + color: $grey-bright; +} +.exploration-error-message{ + margin-top: 3rem; + text-align: center; +} +.exploration-error-button{ + margin-top: 3rem; + width: 7.5rem; +} \ No newline at end of file diff --git a/src/components/Exploration/explorationOngoing.scss b/src/components/Exploration/explorationOngoing.scss new file mode 100644 index 0000000000000000000000000000000000000000..0f414f8cfce76feae0e01664eed3432e93667e10 --- /dev/null +++ b/src/components/Exploration/explorationOngoing.scss @@ -0,0 +1,47 @@ +@import '../../styles/base/typography'; + +.duel-ongoing-container{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.duel-title{ + color: $soft-grey; + margin-top: 1rem; +} +.duel-goal{ + color: $grey-bright; + margin: 1rem 3rem; + text-align: center; +} +.duel-consumption{ + color: $grey-bright; + margin: 1rem auto; + .consumption{ + color: $gold-light; + + } +} +.duel-chart{ + height: 15.625rem; + display: flex; + align-items: center; + justify-content: center; + width: 80%; +} +.duel-chart-caption{ + display: flex; + flex-direction: column; + align-self: flex-start; + .duel-caption{ + display: flex; + margin-top: 0.75rem; + .caption-icon{ + margin: auto 1.5rem; + } + .caption-label{ + color: $grey-bright; + } + } +} \ No newline at end of file diff --git a/src/components/Exploration/explorationView.scss b/src/components/Exploration/explorationView.scss new file mode 100644 index 0000000000000000000000000000000000000000..6708d11d3c270338fc2f69a8bb15f815da5b7134 --- /dev/null +++ b/src/components/Exploration/explorationView.scss @@ -0,0 +1 @@ +@import '../../styles/base/typography'; \ No newline at end of file diff --git a/src/locales/fr.json b/src/locales/fr.json index 1482329dbc2d7db4f8e3677314016ece6570d809..2c442e8fbbb05ac131c9dd32e6f355501d335a14 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -373,5 +373,9 @@ "confirm": "Valider", "next": "Suivant", "consumption_question": "Question sur votre consommation" + }, + "exploration": { + "global_error": "Oups.. Une erreur est parvenue. Veuillez retourner à l'écran des défis", + "go_back": "Retour" } }