From fa4f629485b44cb69ed65503086805d2190664d7 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 15:43:50 +0200 Subject: [PATCH 1/8] LUN-357: Make vocabulary list reusable --- .../VocabularyList.tsx} | 29 ++++++--------- .../components/VocabularyListItem.tsx | 10 +++--- .../components/VocabularyListModal.tsx | 16 ++++----- .../__tests__/VocabularyListItem.spec.tsx | 6 ++-- .../__tests__/VocabularyListModal.spec.tsx | 6 ++-- src/navigation/Navigator.tsx | 2 +- src/routes/ResultDetailScreen.tsx | 2 +- src/routes/VocabularyListScreen.tsx | 17 +++++++++ .../__tests__/VocabularyListScreen.spec.tsx | 35 +++++-------------- 9 files changed, 57 insertions(+), 66 deletions(-) rename src/{routes/vocabulary-list/VocabularyListScreen.tsx => components/VocabularyList.tsx} (61%) rename src/{routes/vocabulary-list => }/components/VocabularyListItem.tsx (85%) rename src/{routes/vocabulary-list => }/components/VocabularyListModal.tsx (86%) rename src/{routes/vocabulary-list => }/components/__tests__/VocabularyListItem.spec.tsx (86%) rename src/{routes/vocabulary-list => }/components/__tests__/VocabularyListModal.spec.tsx (89%) create mode 100644 src/routes/VocabularyListScreen.tsx rename src/routes/{vocabulary-list => }/__tests__/VocabularyListScreen.spec.tsx (62%) diff --git a/src/routes/vocabulary-list/VocabularyListScreen.tsx b/src/components/VocabularyList.tsx similarity index 61% rename from src/routes/vocabulary-list/VocabularyListScreen.tsx rename to src/components/VocabularyList.tsx index 9751d892a..71d458dab 100644 --- a/src/routes/vocabulary-list/VocabularyListScreen.tsx +++ b/src/components/VocabularyList.tsx @@ -1,15 +1,12 @@ -import { RouteProp } from '@react-navigation/native' -import { StackNavigationProp } from '@react-navigation/stack' -import React, { ComponentType, useState } from 'react' +import React, { useState } from 'react' import { FlatList } from 'react-native' import styled from 'styled-components/native' -import Title from '../../components/Title' -import { Document } from '../../constants/endpoints' -import labels from '../../constants/labels.json' -import { RoutesParams } from '../../navigation/NavigationTypes' -import VocabularyListItem from './components/VocabularyListItem' -import VocabularyListModal from './components/VocabularyListModal' +import { Document } from '../constants/endpoints' +import labels from '../constants/labels.json' +import Title from './Title' +import VocabularyListItem from './VocabularyListItem' +import VocabularyListModal from './VocabularyListModal' const Root = styled.View` background-color: ${props => props.theme.colors.background}; @@ -18,17 +15,11 @@ const Root = styled.View` padding: 0 ${props => props.theme.spacings.sm}; ` -const StyledList = styled(FlatList)` - width: 100%; -` as ComponentType as new () => FlatList - interface VocabularyListScreenProps { - route: RouteProp - navigation: StackNavigationProp + documents: Document[] } -const VocabularyListScreen = ({ route }: VocabularyListScreenProps): JSX.Element => { - const { documents } = route.params +const VocabularyList = ({ documents }: VocabularyListScreenProps): JSX.Element => { const [isModalVisible, setIsModalVisible] = useState(false) const [selectedDocumentIndex, setSelectedDocumentIndex] = useState(0) @@ -58,7 +49,7 @@ const VocabularyListScreen = ({ route }: VocabularyListScreenProps): JSX.Element description={`${documents.length} ${documents.length === 1 ? labels.general.word : labels.general.words}`} /> - `${item.id}`} @@ -68,4 +59,4 @@ const VocabularyListScreen = ({ route }: VocabularyListScreenProps): JSX.Element ) } -export default VocabularyListScreen +export default VocabularyList diff --git a/src/routes/vocabulary-list/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx similarity index 85% rename from src/routes/vocabulary-list/components/VocabularyListItem.tsx rename to src/components/VocabularyListItem.tsx index 1514aaff8..0055e339c 100644 --- a/src/routes/vocabulary-list/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -2,11 +2,11 @@ import React, { ReactElement } from 'react' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' -import AudioPlayer from '../../../components/AudioPlayer' -import ListItem from '../../../components/ListItem' -import { ContentTextLight } from '../../../components/text/Content' -import { Document } from '../../../constants/endpoints' -import { getArticleColor } from '../../../services/helpers' +import { Document } from '../constants/endpoints' +import { getArticleColor } from '../services/helpers' +import AudioPlayer from './AudioPlayer' +import ListItem from './ListItem' +import { ContentTextLight } from './text/Content' const StyledImage = styled.Image` margin-right: ${props => props.theme.spacings.sm}; diff --git a/src/routes/vocabulary-list/components/VocabularyListModal.tsx b/src/components/VocabularyListModal.tsx similarity index 86% rename from src/routes/vocabulary-list/components/VocabularyListModal.tsx rename to src/components/VocabularyListModal.tsx index 2625b9faf..c27b7e8d0 100644 --- a/src/routes/vocabulary-list/components/VocabularyListModal.tsx +++ b/src/components/VocabularyListModal.tsx @@ -3,14 +3,14 @@ import { Modal, SafeAreaView } from 'react-native' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' -import { CloseCircleIconWhite, ArrowRightIcon } from '../../../../assets/images' -import AudioPlayer from '../../../components/AudioPlayer' -import Button from '../../../components/Button' -import ImageCarousel from '../../../components/ImageCarousel' -import WordItem from '../../../components/WordItem' -import { BUTTONS_THEME } from '../../../constants/data' -import { Document } from '../../../constants/endpoints' -import labels from '../../../constants/labels.json' +import { CloseCircleIconWhite, ArrowRightIcon } from '../../assets/images' +import { BUTTONS_THEME } from '../constants/data' +import { Document } from '../constants/endpoints' +import labels from '../constants/labels.json' +import AudioPlayer from './AudioPlayer' +import Button from './Button' +import ImageCarousel from './ImageCarousel' +import WordItem from './WordItem' const ModalContainer = styled.View` background-color: ${props => props.theme.colors.background}; diff --git a/src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx b/src/components/__tests__/VocabularyListItem.spec.tsx similarity index 86% rename from src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx rename to src/components/__tests__/VocabularyListItem.spec.tsx index a2ea20c39..8ddf0cbe5 100644 --- a/src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx +++ b/src/components/__tests__/VocabularyListItem.spec.tsx @@ -1,9 +1,9 @@ import React from 'react' import 'react-native' -import { ARTICLES } from '../../../../constants/data' -import { Document } from '../../../../constants/endpoints' -import render from '../../../../testing/render' +import { ARTICLES } from '../../constants/data' +import { Document } from '../../constants/endpoints' +import render from '../../testing/render' import VocabularyListItem from '../VocabularyListItem' jest.mock('../../../../components/AudioPlayer', () => () => null) diff --git a/src/routes/vocabulary-list/components/__tests__/VocabularyListModal.spec.tsx b/src/components/__tests__/VocabularyListModal.spec.tsx similarity index 89% rename from src/routes/vocabulary-list/components/__tests__/VocabularyListModal.spec.tsx rename to src/components/__tests__/VocabularyListModal.spec.tsx index f39157271..eb36ff615 100644 --- a/src/routes/vocabulary-list/components/__tests__/VocabularyListModal.spec.tsx +++ b/src/components/__tests__/VocabularyListModal.spec.tsx @@ -1,9 +1,9 @@ import { fireEvent } from '@testing-library/react-native' import React from 'react' -import labels from '../../../../constants/labels.json' -import DocumentBuilder from '../../../../testing/DocumentBuilder' -import render from '../../../../testing/render' +import labels from '../../constants/labels.json' +import DocumentBuilder from '../../testing/DocumentBuilder' +import render from '../../testing/render' import VocabularyListModal from '../VocabularyListModal' jest.mock('../../../../components/AudioPlayer', () => { diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index e8bd6ddf3..6573d24fe 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -7,10 +7,10 @@ import labels from '../constants/labels.json' import { useTabletHeaderHeight } from '../hooks/useTabletHeaderHeight' import ResultDetailScreen from '../routes/ResultDetailScreen' import ResultScreen from '../routes/ResultScreen' +import VocabularyListScreen from '../routes/VocabularyListScreen' import ArticleChoiceExerciseScreen from '../routes/choice-exercises/ArticleChoiceExerciseScreen' import WordChoiceExerciseScreen from '../routes/choice-exercises/WordChoiceExerciseScreen' import ExerciseFinishedScreen from '../routes/exercise-finished/ExerciseFinishedScreen' -import VocabularyListScreen from '../routes/vocabulary-list/VocabularyListScreen' import WriteExerciseScreen from '../routes/write-exercise/WriteExerciseScreen' import BottomTabNavigator from './BottomTabNavigator' import { RoutesParams } from './NavigationTypes' diff --git a/src/routes/ResultDetailScreen.tsx b/src/routes/ResultDetailScreen.tsx index f3c2790cf..6677479fb 100644 --- a/src/routes/ResultDetailScreen.tsx +++ b/src/routes/ResultDetailScreen.tsx @@ -9,10 +9,10 @@ import { DoubleCheckCircleIconWhite, ArrowRightIcon, RepeatIcon } from '../../as import Button from '../components/Button' import Loading from '../components/Loading' import Title from '../components/Title' +import VocabularyListItem from '../components/VocabularyListItem' import { BUTTONS_THEME, ExerciseKeys, RESULTS } from '../constants/data' import labels from '../constants/labels.json' import { DocumentResult, RoutesParams } from '../navigation/NavigationTypes' -import VocabularyListItem from './vocabulary-list/components/VocabularyListItem' const Root = styled.View` background-color: ${prop => prop.theme.colors.background}; diff --git a/src/routes/VocabularyListScreen.tsx b/src/routes/VocabularyListScreen.tsx new file mode 100644 index 000000000..2d05f94cf --- /dev/null +++ b/src/routes/VocabularyListScreen.tsx @@ -0,0 +1,17 @@ +import { RouteProp } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' +import React from 'react' + +import VocabularyList from '../components/VocabularyList' +import { RoutesParams } from '../navigation/NavigationTypes' + +interface VocabularyListScreenProps { + route: RouteProp + navigation: StackNavigationProp +} + +const VocabularyListScreen = ({ route }: VocabularyListScreenProps): JSX.Element => ( + +) + +export default VocabularyListScreen diff --git a/src/routes/vocabulary-list/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx similarity index 62% rename from src/routes/vocabulary-list/__tests__/VocabularyListScreen.spec.tsx rename to src/routes/__tests__/VocabularyListScreen.spec.tsx index b30462a45..03e6d9844 100644 --- a/src/routes/vocabulary-list/__tests__/VocabularyListScreen.spec.tsx +++ b/src/routes/__tests__/VocabularyListScreen.spec.tsx @@ -1,15 +1,12 @@ -import { CommonActions, RouteProp } from '@react-navigation/native' import { fireEvent } from '@testing-library/react-native' import React, { ComponentProps } from 'react' -import labels from '../../../constants/labels.json' -import { RoutesParams } from '../../../navigation/NavigationTypes' -import DocumentBuilder from '../../../testing/DocumentBuilder' -import createNavigationMock from '../../../testing/createNavigationPropMock' -import { mockUseLoadAsyncWithData } from '../../../testing/mockUseLoadFromEndpoint' -import render from '../../../testing/render' -import VocabularyListScreen from '../VocabularyListScreen' -import VocabularyListModal from '../components/VocabularyListModal' +import VocabularyList from '../../components/VocabularyList' +import VocabularyListModal from '../../components/VocabularyListModal' +import labels from '../../constants/labels.json' +import DocumentBuilder from '../../testing/DocumentBuilder' +import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' +import render from '../../testing/render' jest.mock('../../../components/AudioPlayer', () => { const Text = require('react-native').Text @@ -38,27 +35,13 @@ jest.mock('../components/VocabularyListModal', () => { ) }) -describe('VocabularyListScreen', () => { +describe('VocabularyList', () => { const documents = new DocumentBuilder(2).build() - const route: RouteProp = { - key: '', - name: 'VocabularyList', - params: { - documents, - disciplineId: 1, - disciplineTitle: 'My discipline title', - closeExerciseAction: CommonActions.goBack() - } - } - - const navigation = createNavigationMock<'VocabularyList'>() it('should display vocabulary list', () => { mockUseLoadAsyncWithData(documents) - const { getByText, getAllByText, getAllByTestId } = render( - - ) + const { getByText, getAllByText, getAllByTestId } = render() expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() @@ -71,7 +54,7 @@ describe('VocabularyListScreen', () => { }) it('should open vocabulary modal when pressing vocabulary', () => { - const { getByText } = render() + const { getByText } = render() const vocabulary = getByText('Auto') fireEvent.press(vocabulary) From c4dfe41f28cdefb7584d5ee31be148decaeee668 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle Date: Mon, 30 May 2022 17:20:02 +0200 Subject: [PATCH 2/8] LUN-357: Use own vocabulary detail route instead of modal --- src/components/VocabularyList.tsx | 26 +---- src/components/VocabularyListItem.tsx | 8 +- src/components/VocabularyListModal.tsx | 101 ------------------ .../__tests__/VocabularyList.spec.tsx | 55 ++++++++++ .../__tests__/VocabularyListItem.spec.tsx | 17 ++- .../__tests__/VocabularyListModal.spec.tsx | 51 --------- src/navigation/NavigationTypes.ts | 9 ++ src/navigation/Navigator.tsx | 6 ++ src/routes/ResultDetailScreen.tsx | 7 +- src/routes/VocabularyDetailScreen.tsx | 62 +++++++++++ src/routes/VocabularyListScreen.tsx | 8 +- .../__tests__/VocabularyListScreen.spec.tsx | 65 ----------- src/routes/home/HomeScreen.tsx | 12 ++- src/routes/home/components/DisciplineCard.tsx | 7 +- .../__tests__/DisciplineCard.spec.tsx | 17 ++- 15 files changed, 191 insertions(+), 260 deletions(-) delete mode 100644 src/components/VocabularyListModal.tsx create mode 100644 src/components/__tests__/VocabularyList.spec.tsx delete mode 100644 src/components/__tests__/VocabularyListModal.spec.tsx create mode 100644 src/routes/VocabularyDetailScreen.tsx delete mode 100644 src/routes/__tests__/VocabularyListScreen.spec.tsx diff --git a/src/components/VocabularyList.tsx b/src/components/VocabularyList.tsx index 71d458dab..de7cefa6d 100644 --- a/src/components/VocabularyList.tsx +++ b/src/components/VocabularyList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { FlatList } from 'react-native' import styled from 'styled-components/native' @@ -6,7 +6,6 @@ import { Document } from '../constants/endpoints' import labels from '../constants/labels.json' import Title from './Title' import VocabularyListItem from './VocabularyListItem' -import VocabularyListModal from './VocabularyListModal' const Root = styled.View` background-color: ${props => props.theme.colors.background}; @@ -17,33 +16,16 @@ const Root = styled.View` interface VocabularyListScreenProps { documents: Document[] + onItemPress: (index: number) => void } -const VocabularyList = ({ documents }: VocabularyListScreenProps): JSX.Element => { - const [isModalVisible, setIsModalVisible] = useState(false) - const [selectedDocumentIndex, setSelectedDocumentIndex] = useState(0) - +const VocabularyList = ({ documents, onItemPress }: VocabularyListScreenProps): JSX.Element => { const renderItem = ({ item, index }: { item: Document; index: number }): JSX.Element => ( - { - setIsModalVisible(true) - setSelectedDocumentIndex(index) - }} - /> + onItemPress(index)} /> ) return ( - {documents[selectedDocumentIndex] && ( - - )} void + onPress: () => void } -const VocabularyListItem = ({ document, setIsModalVisible }: VocabularyListItemProp): ReactElement => { +const VocabularyListItem = ({ document, onPress }: VocabularyListItemProp): ReactElement => { const { article, word, document_image: documentImage } = document const title = <StyledTitle articleColor={getArticleColor(article)}>{article.value}</StyledTitle> @@ -43,13 +43,11 @@ const VocabularyListItem = ({ document, setIsModalVisible }: VocabularyListItemP <StyledImage testID='image' source={{ uri: documentImage[0].image }} width={24} height={24} /> ) : undefined - const noop = () => undefined - return ( <ListItem title={title} description={word} - onPress={setIsModalVisible ?? noop} + onPress={onPress} icon={icon} rightChildren={ <Speaker> diff --git a/src/components/VocabularyListModal.tsx b/src/components/VocabularyListModal.tsx deleted file mode 100644 index c27b7e8d0..000000000 --- a/src/components/VocabularyListModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { ReactElement } from 'react' -import { Modal, SafeAreaView } from 'react-native' -import { widthPercentageToDP as wp } from 'react-native-responsive-screen' -import styled from 'styled-components/native' - -import { CloseCircleIconWhite, ArrowRightIcon } from '../../assets/images' -import { BUTTONS_THEME } from '../constants/data' -import { Document } from '../constants/endpoints' -import labels from '../constants/labels.json' -import AudioPlayer from './AudioPlayer' -import Button from './Button' -import ImageCarousel from './ImageCarousel' -import WordItem from './WordItem' - -const ModalContainer = styled.View` - background-color: ${props => props.theme.colors.background}; - flex: 1; -` - -const ModalHeader = styled.View` - display: flex; - align-items: flex-end; - padding: ${props => props.theme.spacings.xs}; - border-bottom-color: ${props => props.theme.colors.disabled}; - border-bottom-width: 1px; - margin-bottom: ${props => props.theme.spacings.xs}; -` - -const ItemContainer = styled.View` - margin: ${props => props.theme.spacings.xl} 0; - height: 10%; - width: 85%; - align-self: center; -` - -const ButtonContainer = styled.View` - display: flex; - align-self: center; -` - -interface VocabularyListModalProps { - documents: Document[] - isModalVisible: boolean - setIsModalVisible: (isModalVisible: boolean) => void - selectedDocumentIndex: number - setSelectedDocumentIndex: (selectedDocumentIndex: number) => void -} - -const VocabularyListModal = ({ - documents, - isModalVisible, - setIsModalVisible, - selectedDocumentIndex, - setSelectedDocumentIndex -}: VocabularyListModalProps): ReactElement => { - const goToNextWord = (): void => { - if (selectedDocumentIndex + 1 < documents.length) { - setSelectedDocumentIndex(selectedDocumentIndex + 1) - } - } - - return ( - <Modal animationType='slide' transparent visible={isModalVisible} onRequestClose={() => setIsModalVisible(false)}> - <ModalContainer> - <SafeAreaView> - <ModalHeader> - <CloseCircleIconWhite onPress={() => setIsModalVisible(false)} width={wp('7%')} height={wp('7%')} /> - </ModalHeader> - <ImageCarousel images={documents[selectedDocumentIndex].document_image} /> - <AudioPlayer document={documents[selectedDocumentIndex]} disabled={false} /> - <ItemContainer> - <WordItem - answer={{ - word: documents[selectedDocumentIndex].word, - article: documents[selectedDocumentIndex].article - }} - /> - </ItemContainer> - <ButtonContainer> - {documents.length > selectedDocumentIndex + 1 ? ( - <Button - label={labels.exercises.next} - iconRight={ArrowRightIcon} - onPress={goToNextWord} - buttonTheme={BUTTONS_THEME.contained} - /> - ) : ( - <Button - label={labels.general.header.cancelExercise} - onPress={() => setIsModalVisible(false)} - buttonTheme={BUTTONS_THEME.contained} - /> - )} - </ButtonContainer> - </SafeAreaView> - </ModalContainer> - </Modal> - ) -} - -export default VocabularyListModal diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx new file mode 100644 index 000000000..04affd344 --- /dev/null +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -0,0 +1,55 @@ +import { fireEvent } from '@testing-library/react-native' +import React from 'react' + +import labels from '../../constants/labels.json' +import DocumentBuilder from '../../testing/DocumentBuilder' +import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' +import render from '../../testing/render' +import VocabularyList from '../VocabularyList' + +jest.mock('../AudioPlayer', () => { + const Text = require('react-native').Text + return () => <Text>AudioPlayer</Text> +}) + +describe('VocabularyList', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const onItemPress = jest.fn() + const documents = new DocumentBuilder(2).build() + + it('should display vocabulary list', () => { + mockUseLoadAsyncWithData(documents) + + const { getByText, getAllByText, getAllByTestId } = render( + <VocabularyList documents={documents} onItemPress={onItemPress} /> + ) + + expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() + expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() + expect(getByText('Der')).toBeTruthy() + expect(getByText('Spachtel')).toBeTruthy() + expect(getByText('Das')).toBeTruthy() + expect(getByText('Auto')).toBeTruthy() + expect(getAllByText('AudioPlayer')).toHaveLength(2) + expect(getAllByTestId('image')).toHaveLength(2) + }) + + it('should call onItemPress with correct index', () => { + const { getByText } = render(<VocabularyList documents={documents} onItemPress={onItemPress} />) + + expect(onItemPress).toHaveBeenCalledTimes(0) + + fireEvent.press(getByText('Auto')) + + expect(onItemPress).toHaveBeenCalledTimes(1) + expect(onItemPress).toHaveBeenCalledWith(1) + + fireEvent.press(getByText('Spachtel')) + + expect(onItemPress).toHaveBeenCalledTimes(2) + expect(onItemPress).toHaveBeenCalledWith(0) + }) +}) diff --git a/src/components/__tests__/VocabularyListItem.spec.tsx b/src/components/__tests__/VocabularyListItem.spec.tsx index 8ddf0cbe5..ed668dc66 100644 --- a/src/components/__tests__/VocabularyListItem.spec.tsx +++ b/src/components/__tests__/VocabularyListItem.spec.tsx @@ -1,3 +1,4 @@ +import { fireEvent } from '@testing-library/react-native' import React from 'react' import 'react-native' @@ -6,9 +7,11 @@ import { Document } from '../../constants/endpoints' import render from '../../testing/render' import VocabularyListItem from '../VocabularyListItem' -jest.mock('../../../../components/AudioPlayer', () => () => null) +jest.mock('../AudioPlayer', () => () => null) describe('VocabularyListItem', () => { + const onPress = jest.fn() + const document: Document = { article: ARTICLES[1], audio: '', @@ -19,17 +22,21 @@ describe('VocabularyListItem', () => { } it('should display image passed to it', () => { - const { getByTestId } = render(<VocabularyListItem document={document} />) + const { getByTestId } = render(<VocabularyListItem document={document} onPress={onPress} />) expect(getByTestId('image')).toHaveProp('source', { uri: document.document_image[0].image }) }) it('should display article passed to it', () => { - const { queryByText } = render(<VocabularyListItem document={document} />) + const { queryByText } = render(<VocabularyListItem document={document} onPress={onPress} />) expect(queryByText(document.article.value)).toBeTruthy() }) it('should display word passed to it', () => { - const { queryByText } = render(<VocabularyListItem document={document} />) - expect(queryByText(document.word)).toBeTruthy() + const { getByText } = render(<VocabularyListItem document={document} onPress={onPress} />) + expect(getByText(document.word)).toBeTruthy() + + expect(onPress).toHaveBeenCalledTimes(0) + fireEvent.press(getByText(document.word)) + expect(onPress).toHaveBeenCalledTimes(1) }) }) diff --git a/src/components/__tests__/VocabularyListModal.spec.tsx b/src/components/__tests__/VocabularyListModal.spec.tsx deleted file mode 100644 index eb36ff615..000000000 --- a/src/components/__tests__/VocabularyListModal.spec.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import React from 'react' - -import labels from '../../constants/labels.json' -import DocumentBuilder from '../../testing/DocumentBuilder' -import render from '../../testing/render' -import VocabularyListModal from '../VocabularyListModal' - -jest.mock('../../../../components/AudioPlayer', () => { - const Text = require('react-native').Text - return () => <Text>AudioPlayer</Text> -}) - -describe('VocabularyListModal', () => { - const documents = new DocumentBuilder(2).build() - - const setIsModalVisible = jest.fn() - const setSelectedDocumentIndex = jest.fn() - - it('should update current document', () => { - const { getByText } = render( - <VocabularyListModal - documents={documents} - isModalVisible - setIsModalVisible={setIsModalVisible} - selectedDocumentIndex={0} - setSelectedDocumentIndex={setSelectedDocumentIndex} - /> - ) - const button = getByText(labels.exercises.next) - expect(button).toBeDefined() - fireEvent.press(button) - expect(setSelectedDocumentIndex).toHaveBeenCalledTimes(1) - }) - - it('should close modal for last word', () => { - const { getByText } = render( - <VocabularyListModal - documents={documents} - isModalVisible - setIsModalVisible={setIsModalVisible} - selectedDocumentIndex={1} - setSelectedDocumentIndex={setSelectedDocumentIndex} - /> - ) - const button = getByText(labels.general.header.cancelExercise) - expect(button).toBeDefined() - fireEvent.press(button) - expect(setIsModalVisible).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/navigation/NavigationTypes.ts b/src/navigation/NavigationTypes.ts index 874bcde4a..0d71cf206 100644 --- a/src/navigation/NavigationTypes.ts +++ b/src/navigation/NavigationTypes.ts @@ -16,6 +16,14 @@ interface ExerciseParams { closeExerciseAction: CommonNavigationAction } +interface DetailExerciseParams { + disciplineId: number + disciplineTitle: string + documents: Document[] + documentIndex: number + closeExerciseAction: CommonNavigationAction +} + export interface ExercisesParams extends Omit<ExerciseParams, 'documents' | 'closeExerciseAction'> { discipline: Discipline documents: Document[] | null @@ -47,6 +55,7 @@ export type RoutesParams = { discipline: Discipline initialSelection: boolean } + VocabularyDetail: DetailExerciseParams Exercises: ExercisesParams VocabularyList: ExerciseParams WordChoiceExercise: ExerciseParams diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index 6573d24fe..fbdb9515e 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -7,6 +7,7 @@ import labels from '../constants/labels.json' import { useTabletHeaderHeight } from '../hooks/useTabletHeaderHeight' import ResultDetailScreen from '../routes/ResultDetailScreen' import ResultScreen from '../routes/ResultScreen' +import VocabularyDetailScreen from '../routes/VocabularyDetailScreen' import VocabularyListScreen from '../routes/VocabularyListScreen' import ArticleChoiceExerciseScreen from '../routes/choice-exercises/ArticleChoiceExerciseScreen' import WordChoiceExerciseScreen from '../routes/choice-exercises/WordChoiceExerciseScreen' @@ -33,6 +34,11 @@ const HomeStackNavigator = (): JSX.Element | null => { component={VocabularyListScreen} options={({ navigation }) => options(overviewExercises, navigation, false)} /> + <Stack.Screen + name='VocabularyDetail' + component={VocabularyDetailScreen} + options={({ navigation }) => options('', navigation, true)} + /> <Stack.Screen name='WordChoiceExercise' component={WordChoiceExerciseScreen} diff --git a/src/routes/ResultDetailScreen.tsx b/src/routes/ResultDetailScreen.tsx index 6677479fb..e671c2349 100644 --- a/src/routes/ResultDetailScreen.tsx +++ b/src/routes/ResultDetailScreen.tsx @@ -73,7 +73,12 @@ const ResultDetailScreen = ({ route, navigation }: ResultScreenProps): JSX.Eleme /> ) - const Item = ({ item }: { item: DocumentResult }): JSX.Element => <VocabularyListItem document={item.document} /> + const Item = ({ item, index }: { item: DocumentResult; index: number }): JSX.Element => ( + <VocabularyListItem + document={item.document} + onPress={() => navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index })} + /> + ) const repeatIncorrectEntries = (): void => navigation.navigate('WriteExercise', { diff --git a/src/routes/VocabularyDetailScreen.tsx b/src/routes/VocabularyDetailScreen.tsx new file mode 100644 index 000000000..00eb6fa47 --- /dev/null +++ b/src/routes/VocabularyDetailScreen.tsx @@ -0,0 +1,62 @@ +import { RouteProp } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' +import React, { ReactElement } from 'react' +import styled from 'styled-components/native' + +import { ArrowRightIcon } from '../../assets/images' +import AudioPlayer from '../components/AudioPlayer' +import Button from '../components/Button' +import ImageCarousel from '../components/ImageCarousel' +import WordItem from '../components/WordItem' +import { BUTTONS_THEME } from '../constants/data' +import labels from '../constants/labels.json' +import { RoutesParams } from '../navigation/NavigationTypes' + +const ItemContainer = styled.View` + margin: ${props => props.theme.spacings.xl} 0; + height: 10%; + width: 85%; + align-self: center; +` + +const ButtonContainer = styled.View` + display: flex; + align-self: center; +` + +interface VocabularyDetailScreenProps { + route: RouteProp<RoutesParams, 'VocabularyDetail'> + navigation: StackNavigationProp<RoutesParams, 'VocabularyDetail'> +} + +const VocabularyDetailScreen = ({ route, navigation }: VocabularyDetailScreenProps): ReactElement => { + const { documents, documentIndex } = route.params + const document = documents[documentIndex] + const { word, article, document_image: image } = document + const hasNextDocument = documentIndex + 1 < documents.length + + const goToNextWord = () => + navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: documentIndex + 1 }) + + return ( + <> + <ImageCarousel images={image} /> + <AudioPlayer document={document} disabled={false} /> + <ItemContainer> + <WordItem answer={{ word, article }} /> + </ItemContainer> + <ButtonContainer> + {hasNextDocument && ( + <Button + label={labels.exercises.next} + iconRight={ArrowRightIcon} + onPress={goToNextWord} + buttonTheme={BUTTONS_THEME.contained} + /> + )} + </ButtonContainer> + </> + ) +} + +export default VocabularyDetailScreen diff --git a/src/routes/VocabularyListScreen.tsx b/src/routes/VocabularyListScreen.tsx index 2d05f94cf..80e9fce0d 100644 --- a/src/routes/VocabularyListScreen.tsx +++ b/src/routes/VocabularyListScreen.tsx @@ -10,8 +10,10 @@ interface VocabularyListScreenProps { navigation: StackNavigationProp<RoutesParams, 'VocabularyList'> } -const VocabularyListScreen = ({ route }: VocabularyListScreenProps): JSX.Element => ( - <VocabularyList documents={route.params.documents} /> -) +const VocabularyListScreen = ({ route, navigation }: VocabularyListScreenProps): JSX.Element => { + const onItemPress = (index: number) => + navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index }) + return <VocabularyList documents={route.params.documents} onItemPress={onItemPress} /> +} export default VocabularyListScreen diff --git a/src/routes/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx deleted file mode 100644 index 03e6d9844..000000000 --- a/src/routes/__tests__/VocabularyListScreen.spec.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import React, { ComponentProps } from 'react' - -import VocabularyList from '../../components/VocabularyList' -import VocabularyListModal from '../../components/VocabularyListModal' -import labels from '../../constants/labels.json' -import DocumentBuilder from '../../testing/DocumentBuilder' -import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' -import render from '../../testing/render' - -jest.mock('../../../components/AudioPlayer', () => { - const Text = require('react-native').Text - return () => <Text>AudioPlayer</Text> -}) - -// fix that the modal appears in the test tree even though it is not visible -jest.mock('react-native/Libraries/Modal/Modal', () => { - const Modal = jest.requireActual('react-native/Libraries/Modal/Modal') - // @ts-expect-error test - return props => <Modal {...props} /> -}) - -// the modal always lies on top of the route meaning you'll also find the texts below -// therefore prefix the word with modal_ -// requireing the actual modal fixes that the modal appears in the test tree -// even though it is not visible - -jest.mock('../components/VocabularyListModal', () => { - const Modal = jest.requireActual('react-native/Libraries/Modal/Modal') - const Text = require('react-native').Text - return ({ isModalVisible, documents, selectedDocumentIndex }: ComponentProps<typeof VocabularyListModal>) => ( - <Modal visible={isModalVisible}> - <Text>{`modal_${documents[selectedDocumentIndex].word}`}</Text> - </Modal> - ) -}) - -describe('VocabularyList', () => { - const documents = new DocumentBuilder(2).build() - - it('should display vocabulary list', () => { - mockUseLoadAsyncWithData(documents) - - const { getByText, getAllByText, getAllByTestId } = render(<VocabularyList documents={documents} />) - - expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() - expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() - expect(getByText('Der')).toBeTruthy() - expect(getByText('Spachtel')).toBeTruthy() - expect(getByText('Das')).toBeTruthy() - expect(getByText('Auto')).toBeTruthy() - expect(getAllByText('AudioPlayer')).toHaveLength(2) - expect(getAllByTestId('image')).toHaveLength(2) - }) - - it('should open vocabulary modal when pressing vocabulary', () => { - const { getByText } = render(<VocabularyList documents={documents} />) - - const vocabulary = getByText('Auto') - fireEvent.press(vocabulary) - - // see mock above - expect(getByText('modal_Auto')).toBeTruthy() - }) -}) diff --git a/src/routes/home/HomeScreen.tsx b/src/routes/home/HomeScreen.tsx index 57f31947e..ae092e8eb 100644 --- a/src/routes/home/HomeScreen.tsx +++ b/src/routes/home/HomeScreen.tsx @@ -7,6 +7,7 @@ import styled from 'styled-components/native' import HeaderWithMenu from '../../components/HeaderWithMenu' import { ContentSecondary } from '../../components/text/Content' import { Heading } from '../../components/text/Heading' +import { Discipline } from '../../constants/endpoints' import labels from '../../constants/labels.json' import useReadCustomDisciplines from '../../hooks/useReadCustomDisciplines' import useReadSelectedProfessions from '../../hooks/useReadSelectedProfessions' @@ -48,16 +49,25 @@ const HomeScreen = ({ navigation }: HomeScreenProps): JSX.Element => { navigation.navigate('AddCustomDiscipline') } + const navigateToDisciplineSelection = (discipline: Discipline): void => { + navigation.navigate('DisciplineSelection', { discipline }) + } + const customDisciplineItems = customDisciplines?.map(customDiscipline => ( <DisciplineCard key={customDiscipline} identifier={{ apiKey: customDiscipline }} refresh={refreshCustomDisciplines} + onPress={navigateToDisciplineSelection} /> )) const selectedProfessionItems = selectedProfessions?.map(profession => ( - <DisciplineCard key={profession} identifier={{ disciplineId: profession }} /> + <DisciplineCard + key={profession} + identifier={{ disciplineId: profession }} + onPress={navigateToDisciplineSelection} + /> )) return ( diff --git a/src/routes/home/components/DisciplineCard.tsx b/src/routes/home/components/DisciplineCard.tsx index 78203f7f0..7c9ebbdde 100644 --- a/src/routes/home/components/DisciplineCard.tsx +++ b/src/routes/home/components/DisciplineCard.tsx @@ -7,7 +7,7 @@ import Loading from '../../../components/Loading' import { ContentSecondary, ContentSecondaryLight } from '../../../components/text/Content' import { Subheading } from '../../../components/text/Subheading' import { BUTTONS_THEME } from '../../../constants/data' -import { ForbiddenError } from '../../../constants/endpoints' +import { Discipline, ForbiddenError } from '../../../constants/endpoints' import labels from '../../../constants/labels.json' import { isTypeLoadProtected } from '../../../hooks/helpers' import { RequestParams, useLoadDiscipline } from '../../../hooks/useLoadDiscipline' @@ -40,9 +40,10 @@ export const ButtonContainer = styled.View` interface PropsType { identifier: RequestParams refresh?: () => void + onPress: (discipline: Discipline) => void } -const DisciplineCard = ({ identifier, refresh: refreshHome }: PropsType): JSX.Element => { +const DisciplineCard = ({ identifier, refresh: refreshHome, onPress }: PropsType): JSX.Element => { const { data: discipline, loading, error, refresh } = useLoadDiscipline(identifier) if (loading) { @@ -79,7 +80,7 @@ const DisciplineCard = ({ identifier, refresh: refreshHome }: PropsType): JSX.El } return ( - <Card heading={discipline.title} icon={discipline.icon}> + <Card heading={discipline.title} icon={discipline.icon} onPress={() => onPress(discipline)}> {isTypeLoadProtected(identifier) ? ( <CustomDisciplineDetails discipline={discipline} /> ) : ( diff --git a/src/routes/home/components/__tests__/DisciplineCard.spec.tsx b/src/routes/home/components/__tests__/DisciplineCard.spec.tsx index 83eb766fb..49d1765fe 100644 --- a/src/routes/home/components/__tests__/DisciplineCard.spec.tsx +++ b/src/routes/home/components/__tests__/DisciplineCard.spec.tsx @@ -1,5 +1,5 @@ import { NavigationContainer } from '@react-navigation/native' -import { RenderAPI } from '@testing-library/react-native' +import { fireEvent, RenderAPI } from '@testing-library/react-native' import React from 'react' import { ForbiddenError, NetworkError } from '../../../../constants/endpoints' @@ -16,10 +16,16 @@ import DisciplineCard from '../DisciplineCard' jest.useFakeTimers() describe('DisciplineCard', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const onPress = jest.fn() + const renderDisciplineCard = (): RenderAPI => render( <NavigationContainer> - <DisciplineCard identifier={{ disciplineId: 1 }} /> + <DisciplineCard identifier={{ disciplineId: 1 }} onPress={onPress} /> </NavigationContainer> ) @@ -29,6 +35,11 @@ describe('DisciplineCard', () => { expect(getByText(mockDisciplines()[0].title)).toBeDefined() expect(getByTestId('progress-circle')).toBeDefined() await expect(findByText(labels.home.continue)).toBeDefined() + + fireEvent.press(getByText(mockDisciplines()[0].title)) + + expect(onPress).toHaveBeenCalledTimes(1) + expect(onPress).toHaveBeenCalledWith(mockDisciplines()[0]) }) it('should display loading', () => { @@ -47,7 +58,7 @@ describe('DisciplineCard', () => { mockUseLoadAsyncWithError(ForbiddenError) const { getByText } = render( <NavigationContainer> - <DisciplineCard identifier={{ apiKey: 'abc' }} /> + <DisciplineCard identifier={{ apiKey: 'abc' }} onPress={onPress} /> </NavigationContainer> ) expect(getByText(`${labels.home.errorLoadCustomDiscipline} abc`)).toBeDefined() From 48e872e36f9e2151a2b5371e2224ac374fb97ca7 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Mon, 30 May 2022 17:25:42 +0200 Subject: [PATCH 3/8] LUN-357: Reuse type --- src/navigation/NavigationTypes.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/navigation/NavigationTypes.ts b/src/navigation/NavigationTypes.ts index 0d71cf206..b9233c397 100644 --- a/src/navigation/NavigationTypes.ts +++ b/src/navigation/NavigationTypes.ts @@ -16,12 +16,8 @@ interface ExerciseParams { closeExerciseAction: CommonNavigationAction } -interface DetailExerciseParams { - disciplineId: number - disciplineTitle: string - documents: Document[] +type DetailExerciseParams = ExerciseParams & { documentIndex: number - closeExerciseAction: CommonNavigationAction } export interface ExercisesParams extends Omit<ExerciseParams, 'documents' | 'closeExerciseAction'> { From 9e2b0cebf03ad4fa8b06018d5329868064a81c07 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 31 May 2022 11:53:26 +0200 Subject: [PATCH 4/8] LUN-357: Revert changes --- src/components/VocabularyListItem.tsx | 4 ++-- src/routes/VocabularyDetailScreen.tsx | 8 +++++++- src/routes/home/HomeScreen.tsx | 12 +----------- src/routes/home/components/DisciplineCard.tsx | 7 +++---- .../components/__tests__/DisciplineCard.spec.tsx | 13 +++---------- 5 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx index 287ead256..d3caf3a46 100644 --- a/src/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -29,12 +29,12 @@ const Speaker = styled.View` padding-top: ${props => props.theme.spacings.sm}; ` -export interface VocabularyListItemProp { +interface VocabularyListItemProps { document: Document onPress: () => void } -const VocabularyListItem = ({ document, onPress }: VocabularyListItemProp): ReactElement => { +const VocabularyListItem = ({ document, onPress }: VocabularyListItemProps): ReactElement => { const { article, word, document_image: documentImage } = document const title = <StyledTitle articleColor={getArticleColor(article)}>{article.value}</StyledTitle> diff --git a/src/routes/VocabularyDetailScreen.tsx b/src/routes/VocabularyDetailScreen.tsx index 00eb6fa47..e55bf74f6 100644 --- a/src/routes/VocabularyDetailScreen.tsx +++ b/src/routes/VocabularyDetailScreen.tsx @@ -46,13 +46,19 @@ const VocabularyDetailScreen = ({ route, navigation }: VocabularyDetailScreenPro <WordItem answer={{ word, article }} /> </ItemContainer> <ButtonContainer> - {hasNextDocument && ( + {hasNextDocument ? ( <Button label={labels.exercises.next} iconRight={ArrowRightIcon} onPress={goToNextWord} buttonTheme={BUTTONS_THEME.contained} /> + ) : ( + <Button + label={labels.general.header.cancelExercise} + onPress={navigation.goBack} + buttonTheme={BUTTONS_THEME.contained} + /> )} </ButtonContainer> </> diff --git a/src/routes/home/HomeScreen.tsx b/src/routes/home/HomeScreen.tsx index ae092e8eb..57f31947e 100644 --- a/src/routes/home/HomeScreen.tsx +++ b/src/routes/home/HomeScreen.tsx @@ -7,7 +7,6 @@ import styled from 'styled-components/native' import HeaderWithMenu from '../../components/HeaderWithMenu' import { ContentSecondary } from '../../components/text/Content' import { Heading } from '../../components/text/Heading' -import { Discipline } from '../../constants/endpoints' import labels from '../../constants/labels.json' import useReadCustomDisciplines from '../../hooks/useReadCustomDisciplines' import useReadSelectedProfessions from '../../hooks/useReadSelectedProfessions' @@ -49,25 +48,16 @@ const HomeScreen = ({ navigation }: HomeScreenProps): JSX.Element => { navigation.navigate('AddCustomDiscipline') } - const navigateToDisciplineSelection = (discipline: Discipline): void => { - navigation.navigate('DisciplineSelection', { discipline }) - } - const customDisciplineItems = customDisciplines?.map(customDiscipline => ( <DisciplineCard key={customDiscipline} identifier={{ apiKey: customDiscipline }} refresh={refreshCustomDisciplines} - onPress={navigateToDisciplineSelection} /> )) const selectedProfessionItems = selectedProfessions?.map(profession => ( - <DisciplineCard - key={profession} - identifier={{ disciplineId: profession }} - onPress={navigateToDisciplineSelection} - /> + <DisciplineCard key={profession} identifier={{ disciplineId: profession }} /> )) return ( diff --git a/src/routes/home/components/DisciplineCard.tsx b/src/routes/home/components/DisciplineCard.tsx index 7c9ebbdde..78203f7f0 100644 --- a/src/routes/home/components/DisciplineCard.tsx +++ b/src/routes/home/components/DisciplineCard.tsx @@ -7,7 +7,7 @@ import Loading from '../../../components/Loading' import { ContentSecondary, ContentSecondaryLight } from '../../../components/text/Content' import { Subheading } from '../../../components/text/Subheading' import { BUTTONS_THEME } from '../../../constants/data' -import { Discipline, ForbiddenError } from '../../../constants/endpoints' +import { ForbiddenError } from '../../../constants/endpoints' import labels from '../../../constants/labels.json' import { isTypeLoadProtected } from '../../../hooks/helpers' import { RequestParams, useLoadDiscipline } from '../../../hooks/useLoadDiscipline' @@ -40,10 +40,9 @@ export const ButtonContainer = styled.View` interface PropsType { identifier: RequestParams refresh?: () => void - onPress: (discipline: Discipline) => void } -const DisciplineCard = ({ identifier, refresh: refreshHome, onPress }: PropsType): JSX.Element => { +const DisciplineCard = ({ identifier, refresh: refreshHome }: PropsType): JSX.Element => { const { data: discipline, loading, error, refresh } = useLoadDiscipline(identifier) if (loading) { @@ -80,7 +79,7 @@ const DisciplineCard = ({ identifier, refresh: refreshHome, onPress }: PropsType } return ( - <Card heading={discipline.title} icon={discipline.icon} onPress={() => onPress(discipline)}> + <Card heading={discipline.title} icon={discipline.icon}> {isTypeLoadProtected(identifier) ? ( <CustomDisciplineDetails discipline={discipline} /> ) : ( diff --git a/src/routes/home/components/__tests__/DisciplineCard.spec.tsx b/src/routes/home/components/__tests__/DisciplineCard.spec.tsx index 49d1765fe..30d6d8cc1 100644 --- a/src/routes/home/components/__tests__/DisciplineCard.spec.tsx +++ b/src/routes/home/components/__tests__/DisciplineCard.spec.tsx @@ -1,5 +1,5 @@ import { NavigationContainer } from '@react-navigation/native' -import { fireEvent, RenderAPI } from '@testing-library/react-native' +import { RenderAPI } from '@testing-library/react-native' import React from 'react' import { ForbiddenError, NetworkError } from '../../../../constants/endpoints' @@ -20,12 +20,10 @@ describe('DisciplineCard', () => { jest.clearAllMocks() }) - const onPress = jest.fn() - const renderDisciplineCard = (): RenderAPI => render( <NavigationContainer> - <DisciplineCard identifier={{ disciplineId: 1 }} onPress={onPress} /> + <DisciplineCard identifier={{ disciplineId: 1 }} /> </NavigationContainer> ) @@ -35,11 +33,6 @@ describe('DisciplineCard', () => { expect(getByText(mockDisciplines()[0].title)).toBeDefined() expect(getByTestId('progress-circle')).toBeDefined() await expect(findByText(labels.home.continue)).toBeDefined() - - fireEvent.press(getByText(mockDisciplines()[0].title)) - - expect(onPress).toHaveBeenCalledTimes(1) - expect(onPress).toHaveBeenCalledWith(mockDisciplines()[0]) }) it('should display loading', () => { @@ -58,7 +51,7 @@ describe('DisciplineCard', () => { mockUseLoadAsyncWithError(ForbiddenError) const { getByText } = render( <NavigationContainer> - <DisciplineCard identifier={{ apiKey: 'abc' }} onPress={onPress} /> + <DisciplineCard identifier={{ apiKey: 'abc' }} /> </NavigationContainer> ) expect(getByText(`${labels.home.errorLoadCustomDiscipline} abc`)).toBeDefined() From 2aa4801f4312c3a72fa6a8e5c6618e9be7b04e8f Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 31 May 2022 12:49:19 +0200 Subject: [PATCH 5/8] LUN-357: Readd saving of progress and tests --- src/routes/VocabularyListScreen.tsx | 12 +++- .../__tests__/VocabularyListScreen.spec.tsx | 58 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/routes/__tests__/VocabularyListScreen.spec.tsx diff --git a/src/routes/VocabularyListScreen.tsx b/src/routes/VocabularyListScreen.tsx index 80e9fce0d..efcf3b9a9 100644 --- a/src/routes/VocabularyListScreen.tsx +++ b/src/routes/VocabularyListScreen.tsx @@ -1,9 +1,12 @@ import { RouteProp } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' -import React from 'react' +import React, { useEffect } from 'react' import VocabularyList from '../components/VocabularyList' +import { ExerciseKeys } from '../constants/data' import { RoutesParams } from '../navigation/NavigationTypes' +import AsyncStorage from '../services/AsyncStorage' +import { reportError } from '../services/sentry' interface VocabularyListScreenProps { route: RouteProp<RoutesParams, 'VocabularyList'> @@ -11,8 +14,15 @@ interface VocabularyListScreenProps { } const VocabularyListScreen = ({ route, navigation }: VocabularyListScreenProps): JSX.Element => { + const { disciplineId } = route.params + + useEffect(() => { + AsyncStorage.setExerciseProgress(disciplineId, ExerciseKeys.vocabularyList, 1).catch(reportError) + }, [disciplineId]) + const onItemPress = (index: number) => navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index }) + return <VocabularyList documents={route.params.documents} onItemPress={onItemPress} /> } diff --git a/src/routes/__tests__/VocabularyListScreen.spec.tsx b/src/routes/__tests__/VocabularyListScreen.spec.tsx new file mode 100644 index 000000000..56cf03483 --- /dev/null +++ b/src/routes/__tests__/VocabularyListScreen.spec.tsx @@ -0,0 +1,58 @@ +import { CommonActions, RouteProp } from '@react-navigation/native' +import React from 'react' + +import labels from '../../constants/labels.json' +import { RoutesParams } from '../../navigation/NavigationTypes' +import AsyncStorage from '../../services/AsyncStorage' +import DocumentBuilder from '../../testing/DocumentBuilder' +import createNavigationMock from '../../testing/createNavigationPropMock' +import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' +import render from '../../testing/render' +import VocabularyListScreen from '../VocabularyListScreen' + +jest.mock('../../services/AsyncStorage', () => ({ + setExerciseProgress: jest.fn(() => Promise.resolve()) +})) + +jest.mock('../../components/AudioPlayer', () => { + const Text = require('react-native').Text + return () => <Text>AudioPlayer</Text> +}) + +describe('VocabularyListScreen', () => { + const documents = new DocumentBuilder(2).build() + const route: RouteProp<RoutesParams, 'VocabularyList'> = { + key: '', + name: 'VocabularyList', + params: { + documents, + disciplineId: 1, + disciplineTitle: 'My discipline title', + closeExerciseAction: CommonActions.goBack() + } + } + + const navigation = createNavigationMock<'VocabularyList'>() + + it('should save progress', () => { + render(<VocabularyListScreen route={route} navigation={navigation} />) + expect(AsyncStorage.setExerciseProgress).toHaveBeenCalledWith(1, 0, 1) + }) + + it('should display vocabulary list', () => { + mockUseLoadAsyncWithData(documents) + + const { getByText, getAllByText, getAllByTestId } = render( + <VocabularyListScreen route={route} navigation={navigation} /> + ) + + expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() + expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() + expect(getByText('Der')).toBeTruthy() + expect(getByText('Spachtel')).toBeTruthy() + expect(getByText('Das')).toBeTruthy() + expect(getByText('Auto')).toBeTruthy() + expect(getAllByText('AudioPlayer')).toHaveLength(2) + expect(getAllByTestId('image')).toHaveLength(2) + }) +}) From f5b89903625e0c098eea306bebe5c4fe9b0553f1 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 31 May 2022 16:00:31 +0200 Subject: [PATCH 6/8] LUN-357: Readd feedback to vocabulary detail and vocabulary list --- src/components/ExerciseHeader.tsx | 46 +++++++++++++++++---------- src/routes/VocabularyDetailScreen.tsx | 7 ++++ src/routes/VocabularyListScreen.tsx | 8 ++++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/components/ExerciseHeader.tsx b/src/components/ExerciseHeader.tsx index 5c64cc23a..91b5f2c10 100644 --- a/src/components/ExerciseHeader.tsx +++ b/src/components/ExerciseHeader.tsx @@ -3,13 +3,16 @@ import React, { useEffect, useState } from 'react' import { BackHandler } from 'react-native' import { ProgressBar as RNProgressBar } from 'react-native-paper' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' +// import { HiddenItem } from 'react-navigation-header-buttons' import styled, { useTheme } from 'styled-components/native' +// import { MenuIcon } from '../../assets/images' import labels from '../constants/labels.json' -import { RoutesParams } from '../navigation/NavigationTypes' +import { Route, RoutesParams } from '../navigation/NavigationTypes' import CustomModal from './CustomModal' import FeedbackModal from './FeedbackModal' import NavigationHeaderLeft from './NavigationHeaderLeft' +// import OverflowMenu from './OverflowMenu' import { ContentSecondary } from './text/Content' const ProgressBar = styled(RNProgressBar)` @@ -25,22 +28,29 @@ const HeaderRightContainer = styled.View` const ProgressText = styled(ContentSecondary)` margin-right: ${props => props.theme.spacings.sm}; ` -// /* TODO Remove comment when LUN-269 is ready */ +// TODO Remove comment when LUN-269 is ready // const MenuIconPrimary = styled(MenuIcon)` // color: ${props => props.theme.colors.primary}; // ` interface ExerciseHeaderProps { - navigation: StackNavigationProp<RoutesParams, 'WordChoiceExercise' | 'ArticleChoiceExercise' | 'WriteExercise'> - currentWord: number - numberOfWords: number + navigation: StackNavigationProp<RoutesParams, Route> + currentWord?: number + numberOfWords?: number + confirmClose?: boolean } -const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHeaderProps): JSX.Element => { +const ExerciseHeader = ({ + navigation, + currentWord, + numberOfWords, + confirmClose = true +}: ExerciseHeaderProps): JSX.Element => { const [isModalVisible, setIsModalVisible] = useState(false) const [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false) const theme = useTheme() - const progressText = numberOfWords !== 0 ? `${currentWord + 1} / ${numberOfWords}` : '' + const showProgress = numberOfWords !== undefined && numberOfWords > 0 && currentWord !== undefined + const progressText = showProgress ? `${currentWord + 1} / ${numberOfWords}` : '' useEffect( () => @@ -71,12 +81,14 @@ const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHead useEffect(() => { const showModal = (): boolean => { - setIsModalVisible(true) - return true + if (confirmClose) { + setIsModalVisible(true) + return true + } + return false } - const bEvent = BackHandler.addEventListener('hardwareBackPress', showModal) - return bEvent.remove - }, []) + return BackHandler.addEventListener('hardwareBackPress', showModal).remove + }, [confirmClose]) const goBack = (): void => { setIsModalVisible(false) @@ -85,10 +97,12 @@ const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHead return ( <> - <ProgressBar - progress={numberOfWords > 0 ? currentWord / numberOfWords : 0} - color={theme.colors.progressIndicator} - /> + {showProgress && ( + <ProgressBar + progress={numberOfWords > 0 ? currentWord / numberOfWords : 0} + color={theme.colors.progressIndicator} + /> + )} <CustomModal testID='customModal' diff --git a/src/routes/VocabularyDetailScreen.tsx b/src/routes/VocabularyDetailScreen.tsx index e55bf74f6..e3d873359 100644 --- a/src/routes/VocabularyDetailScreen.tsx +++ b/src/routes/VocabularyDetailScreen.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components/native' import { ArrowRightIcon } from '../../assets/images' import AudioPlayer from '../components/AudioPlayer' import Button from '../components/Button' +import ExerciseHeader from '../components/ExerciseHeader' import ImageCarousel from '../components/ImageCarousel' import WordItem from '../components/WordItem' import { BUTTONS_THEME } from '../constants/data' @@ -40,6 +41,12 @@ const VocabularyDetailScreen = ({ route, navigation }: VocabularyDetailScreenPro return ( <> + <ExerciseHeader + navigation={navigation} + currentWord={documentIndex} + numberOfWords={documents.length} + confirmClose={false} + /> <ImageCarousel images={image} /> <AudioPlayer document={document} disabled={false} /> <ItemContainer> diff --git a/src/routes/VocabularyListScreen.tsx b/src/routes/VocabularyListScreen.tsx index efcf3b9a9..9da9bdc60 100644 --- a/src/routes/VocabularyListScreen.tsx +++ b/src/routes/VocabularyListScreen.tsx @@ -2,6 +2,7 @@ import { RouteProp } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import React, { useEffect } from 'react' +import ExerciseHeader from '../components/ExerciseHeader' import VocabularyList from '../components/VocabularyList' import { ExerciseKeys } from '../constants/data' import { RoutesParams } from '../navigation/NavigationTypes' @@ -23,7 +24,12 @@ const VocabularyListScreen = ({ route, navigation }: VocabularyListScreenProps): const onItemPress = (index: number) => navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index }) - return <VocabularyList documents={route.params.documents} onItemPress={onItemPress} /> + return ( + <> + <ExerciseHeader navigation={navigation} confirmClose={false} /> + <VocabularyList documents={route.params.documents} onItemPress={onItemPress} /> + </> + ) } export default VocabularyListScreen From b102c084d2ef5217c148f3564d82dbc6cfeedc79 Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 31 May 2022 16:06:30 +0200 Subject: [PATCH 7/8] LUN-357: Fix scrolling --- src/components/VocabularyList.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/VocabularyList.tsx b/src/components/VocabularyList.tsx index de7cefa6d..63b7626ca 100644 --- a/src/components/VocabularyList.tsx +++ b/src/components/VocabularyList.tsx @@ -26,12 +26,13 @@ const VocabularyList = ({ documents, onItemPress }: VocabularyListScreenProps): return ( <Root> - <Title - title={labels.exercises.vocabularyList.title} - description={`${documents.length} ${documents.length === 1 ? labels.general.word : labels.general.words}`} - /> - <FlatList + ListHeaderComponent={ + <Title + title={labels.exercises.vocabularyList.title} + description={`${documents.length} ${documents.length === 1 ? labels.general.word : labels.general.words}`} + /> + } data={documents} renderItem={renderItem} keyExtractor={item => `${item.id}`} From df5fb048cc5dd0b833235d803e0a9b346cd183fc Mon Sep 17 00:00:00 2001 From: Steffen Kleinle <steffen.kleinle@mailbox.org> Date: Tue, 31 May 2022 17:10:12 +0200 Subject: [PATCH 8/8] LUN-357: Fix modal --- src/components/ExerciseHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ExerciseHeader.tsx b/src/components/ExerciseHeader.tsx index 91b5f2c10..2ba5342df 100644 --- a/src/components/ExerciseHeader.tsx +++ b/src/components/ExerciseHeader.tsx @@ -58,7 +58,7 @@ const ExerciseHeader = ({ headerLeft: () => ( <NavigationHeaderLeft title={labels.general.header.cancelExercise} - onPress={() => setIsModalVisible(true)} + onPress={confirmClose ? () => setIsModalVisible(true) : navigation.goBack} isCloseButton /> ), @@ -76,7 +76,7 @@ const ExerciseHeader = ({ maxWidth: wp('25%') } }), - [navigation, progressText, setIsModalVisible, setIsFeedbackModalVisible] + [navigation, progressText, setIsModalVisible, setIsFeedbackModalVisible, confirmClose] ) useEffect(() => {