diff options
author | João Ferreiro <ferreiro@pinkroom.dev> | 2023-01-17 16:06:00 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-17 10:06:00 -0600 |
commit | 5abcc8e336b3af11a6c98d0d9e662415856478a0 (patch) | |
tree | 41778f74311f677e1d57526c57fcb1ece08195f7 /src | |
parent | 11c861d2d368ab59d8e65b216c1551729fc140ad (diff) | |
download | voidsky-5abcc8e336b3af11a6c98d0d9e662415856478a0.tar.zst |
Unit Testing (#35)
* add testing lib * remove coverage folder from git * finished basic test setup * fix tests typescript and import paths * add first snapshot * testing utils * rename test files; update script flags; ++tests * testing utils functions * testing downloadAndResize wip * remove download test * specify unwanted coverage paths; remove update snapshots flag * fix strings tests * testing downloadAndResize method * increasing testing * fixing snapshots wip * fixed shell mobile snapshot * adding snapshots for the screens * fix onboard snapshot * fix typescript issues * fix TabsSelector snapshot * Account for testing device's locale in ago() tests * Remove platform detection on regex * mocking store state wip * mocking store state * increasing test coverage * increasing test coverage * increasing test coverage on src/screens * src/screens (except for profile) above 80% cov * testing profile screen wip * increase coverage on Menu and TabsSelector * mocking profile ui state wip * mocking profile ui state wip * fixing mobileshell tests wip * snapshots using testing-library * fixing profile tests wip * removing mobile shell tests * src/view/com tests wip * remove unnecessary patch-package * fixed profile test error * clear mocks after every test * fix base mocked store values (getters) * fix base mocked store values (hasLoaded, nonReplyFeed) * profile screen above 80% coverage * testing custom hooks * improving composer coverage * fix tests after merge * finishing composer coverage * improving src/com/discover coverage * improve src/view/com/login coverage fix SuggestedFollows tests adding some comments * fix SuggestedFollows tests * improve src/view/com/profile coverage extra minor fixes * improve src/view/com/notifications coverage * update coverage ignore patterns * rename errorMessageTryAgainButton increase SuggestedFollows converage * improve src/view/com/posts coverage * improve src/view/com/onboard coverage * update snapshot * improve src/view/com/post coverage * improve src/view/com/post-thread coverage rename ErrorMessage tests test Debug and Log components * init testing state * testing root-store * updating comments * small fixes * removed extra console logs * improve src/state/models coverage refactor rootStore rename some spies * adding cleanup method after tests * improve src/state/models coverage * improve src/state/models coverage * improve src/state/models coverage * improve src/state/models coverage * test setInterval in setupState * Clean up tests and update Home screen state management * Remove some tests we dont need * Remove snapshot tests * Remove any tests that dont demonstrate clear value * Cleanup Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Diffstat (limited to 'src')
29 files changed, 182 insertions, 89 deletions
diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx index 4ee527ee8..2ccd05653 100644 --- a/src/view/com/composer/Autocomplete.tsx +++ b/src/view/com/composer/Autocomplete.tsx @@ -46,6 +46,7 @@ export function Autocomplete({ <Animated.View style={[styles.outer, pal.view, pal.border, topAnimStyle]}> {items.map((item, i) => ( <TouchableOpacity + testID="autocompleteButton" key={i} style={[pal.border, styles.item]} onPress={() => onSelect(item.handle)}> diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index dc0bec135..790e0f784 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -56,11 +56,12 @@ export const ComposePost = observer(function ComposePost({ const [isSelectingPhotos, setIsSelectingPhotos] = useState(false) const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]) - const autocompleteView = useMemo<UserAutocompleteViewModel>( + // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment + const autocompleteView = React.useMemo<UserAutocompleteViewModel>( () => new UserAutocompleteViewModel(store), [store], ) - const localPhotos = useMemo<UserLocalPhotosModel>( + const localPhotos = React.useMemo<UserLocalPhotosModel>( () => new UserLocalPhotosModel(store), [store], ) @@ -179,11 +180,14 @@ export const ComposePost = observer(function ComposePost({ return ( <KeyboardAvoidingView + testID="composePostView" behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={[pal.view, styles.outer]}> <SafeAreaView style={s.flex1}> <View style={styles.topbar}> - <TouchableOpacity onPress={onPressCancel}> + <TouchableOpacity + testID="composerCancelButton" + onPress={onPressCancel}> <Text style={[pal.link, s.f18]}>Cancel</Text> </TouchableOpacity> <View style={s.flex1} /> @@ -192,7 +196,9 @@ export const ComposePost = observer(function ComposePost({ <ActivityIndicator /> </View> ) : canPost ? ( - <TouchableOpacity onPress={onPressPublish}> + <TouchableOpacity + testID="composerPublishButton" + onPress={onPressPublish}> <LinearGradient colors={[gradients.primary.start, gradients.primary.end]} start={{x: 0, y: 0}} @@ -257,6 +263,7 @@ export const ComposePost = observer(function ComposePost({ size={50} /> <TextInput + testID="composerTextInput" ref={textInput} multiline scrollEnabled @@ -283,6 +290,7 @@ export const ComposePost = observer(function ComposePost({ )} <View style={[pal.border, styles.bottomBar]}> <TouchableOpacity + testID="composerSelectPhotosButton" onPress={onPressSelectPhotos} style={[s.pl5]} hitSlop={HITSLOP}> diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx index 6c6cd0a47..12dac5825 100644 --- a/src/view/com/composer/PhotoCarouselPicker.tsx +++ b/src/view/com/composer/PhotoCarouselPicker.tsx @@ -85,21 +85,25 @@ export const PhotoCarouselPicker = ({ return ( <ScrollView + testID="photoCarouselPickerView" horizontal style={[pal.view, styles.photosContainer]} showsHorizontalScrollIndicator={false}> <TouchableOpacity + testID="openCameraButton" style={[styles.galleryButton, pal.border, styles.photo]} onPress={handleOpenCamera}> <FontAwesomeIcon icon="camera" size={24} style={pal.link} /> </TouchableOpacity> <TouchableOpacity + testID="openGalleryButton" style={[styles.galleryButton, pal.border, styles.photo]} onPress={handleOpenGallery}> <FontAwesomeIcon icon="image" style={pal.link} size={24} /> </TouchableOpacity> {localPhotos.photos.map((item: any, index: number) => ( <TouchableOpacity + testID="openSelectPhotoButton" key={`local-image-${index}`} style={[pal.border, styles.photoButton]} onPress={() => handleSelectPhoto(item.node.image.uri)}> diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 682a9990b..e8f52f84a 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -17,6 +17,7 @@ export function ComposePrompt({ const pal = usePalette('default') return ( <TouchableOpacity + testID="composePromptButton" style={[ pal.view, pal.border, diff --git a/src/view/com/composer/SelectedPhoto.tsx b/src/view/com/composer/SelectedPhoto.tsx index 7711415f6..393c0b573 100644 --- a/src/view/com/composer/SelectedPhoto.tsx +++ b/src/view/com/composer/SelectedPhoto.tsx @@ -25,13 +25,14 @@ export const SelectedPhoto = ({ ) return selectedPhotos.length !== 0 ? ( - <View style={styles.imageContainer}> + <View testID="selectedPhotosView" style={styles.imageContainer}> {selectedPhotos.length !== 0 && selectedPhotos.map((item, index) => ( <View key={`selected-image-${index}`} style={[styles.image, imageStyle]}> <TouchableOpacity + testID="removePhotoButton" onPress={() => handleRemovePhoto(item)} style={styles.removePhotoButton}> <FontAwesomeIcon @@ -41,7 +42,11 @@ export const SelectedPhoto = ({ /> </TouchableOpacity> - <Image style={[styles.image, imageStyle]} source={{uri: item}} /> + <Image + testID="selectedPhotoImage" + style={[styles.image, imageStyle]} + source={{uri: item}} + /> </View> ))} </View> diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index 017bd08c8..07f397447 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useEffect, useState} from 'react' +import React, {useEffect, useState} from 'react' import { ActivityIndicator, FlatList, @@ -36,7 +36,8 @@ export const SuggestedFollows = observer( const store = useStores() const [follows, setFollows] = useState<Record<string, string>>({}) - const view = useMemo<SuggestedActorsViewModel>( + // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment + const view = React.useMemo<SuggestedActorsViewModel>( () => new SuggestedActorsViewModel(store), [], ) diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx index b68d3859e..83d17d374 100644 --- a/src/view/com/login/CreateAccount.tsx +++ b/src/view/com/login/CreateAccount.tsx @@ -171,7 +171,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { const isReady = !!email && !!password && !!handle && is13 return ( - <ScrollView style={{flex: 1}}> + <ScrollView testID="createAccount" style={{flex: 1}}> <KeyboardAvoidingView behavior="padding" style={{flex: 1}}> <View style={styles.logoHero}> <Logo /> @@ -193,6 +193,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { <View style={styles.groupContent}> <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} /> <TouchableOpacity + testID="registerSelectServiceButton" style={styles.textBtn} onPress={onPressSelectService}> <Text style={styles.textBtnLabel}> @@ -235,6 +236,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { style={styles.groupContentIcon} /> <TextInput + testID="registerEmailInput" style={[styles.textInput]} placeholder="Email address" placeholderTextColor={colors.blue0} @@ -248,6 +250,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { <View style={styles.groupContent}> <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} /> <TextInput + testID="registerPasswordInput" style={[styles.textInput]} placeholder="Choose your password" placeholderTextColor={colors.blue0} @@ -273,6 +276,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { <View style={styles.groupContent}> <FontAwesomeIcon icon="at" style={styles.groupContentIcon} /> <TextInput + testID="registerHandleInput" style={[styles.textInput]} placeholder="eg alice" placeholderTextColor={colors.blue0} @@ -317,6 +321,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { </View> <View style={styles.groupContent}> <TouchableOpacity + testID="registerIs13Input" style={styles.textBtn} onPress={() => setIs13(!is13)}> <View style={is13 ? styles.checkboxFilled : styles.checkbox}> @@ -339,7 +344,9 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { </TouchableOpacity> <View style={s.flex1} /> {isReady ? ( - <TouchableOpacity onPress={onPressNext}> + <TouchableOpacity + testID="createAccountButton" + onPress={onPressNext}> {isProcessing ? ( <ActivityIndicator color="#fff" /> ) : ( @@ -347,7 +354,9 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { )} </TouchableOpacity> ) : !serviceDescription && error ? ( - <TouchableOpacity onPress={onPressRetryConnect}> + <TouchableOpacity + testID="registerRetryButton" + onPress={onPressRetryConnect}> <Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text> </TouchableOpacity> ) : !serviceDescription ? ( diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx index 03c634c46..f60b637b7 100644 --- a/src/view/com/login/Signin.tsx +++ b/src/view/com/login/Signin.tsx @@ -69,7 +69,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { const onPressRetryConnect = () => setRetryDescribeTrigger({}) return ( - <KeyboardAvoidingView behavior="padding" style={{flex: 1}}> + <KeyboardAvoidingView testID="signIn" behavior="padding" style={{flex: 1}}> <View style={styles.logoHero}> <Logo /> </View> @@ -194,8 +194,9 @@ const LoginForm = ({ const isReady = !!serviceDescription && !!handle && !!password return ( <> - <View style={styles.group}> + <View testID="loginFormView" style={styles.group}> <TouchableOpacity + testID="loginSelectServiceButton" style={[styles.groupTitle, {paddingRight: 0, paddingVertical: 6}]} onPress={onPressSelectService}> <Text style={[s.flex1, s.white, s.f18, s.bold]} numberOfLines={1}> @@ -213,6 +214,7 @@ const LoginForm = ({ <View style={styles.groupContent}> <FontAwesomeIcon icon="at" style={styles.groupContentIcon} /> <TextInput + testID="loginUsernameInput" style={styles.textInput} placeholder="Username" placeholderTextColor={colors.blue0} @@ -227,6 +229,7 @@ const LoginForm = ({ <View style={styles.groupContent}> <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} /> <TextInput + testID="loginPasswordInput" style={styles.textInput} placeholder="Password" placeholderTextColor={colors.blue0} @@ -238,6 +241,7 @@ const LoginForm = ({ editable={!isProcessing} /> <TouchableOpacity + testID="forgotPasswordButton" style={styles.textInputInnerBtn} onPress={onPressForgotPassword}> <Text style={styles.textInputInnerBtnLabel}>Forgot</Text> @@ -260,7 +264,9 @@ const LoginForm = ({ </TouchableOpacity> <View style={s.flex1} /> {!serviceDescription && error ? ( - <TouchableOpacity onPress={onPressRetryConnect}> + <TouchableOpacity + testID="loginRetryButton" + onPress={onPressRetryConnect}> <Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text> </TouchableOpacity> ) : !serviceDescription ? ( @@ -271,7 +277,7 @@ const LoginForm = ({ ) : isProcessing ? ( <ActivityIndicator color="#fff" /> ) : isReady ? ( - <TouchableOpacity onPress={onPressNext}> + <TouchableOpacity testID="loginNextButton" onPress={onPressNext}> <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text> </TouchableOpacity> ) : undefined} @@ -339,8 +345,9 @@ const ForgotPasswordForm = ({ Enter the email you used to create your account. We'll send you a "reset code" so you can set a new password. </Text> - <View style={styles.group}> + <View testID="forgotPasswordView" style={styles.group}> <TouchableOpacity + testID="forgotPasswordSelectServiceButton" style={[styles.groupContent, {borderTopWidth: 0}]} onPress={onPressSelectService}> <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} /> @@ -359,6 +366,7 @@ const ForgotPasswordForm = ({ <View style={styles.groupContent}> <FontAwesomeIcon icon="envelope" style={styles.groupContentIcon} /> <TextInput + testID="forgotPasswordEmail" style={styles.textInput} placeholder="Email address" placeholderTextColor={colors.blue0} @@ -391,7 +399,7 @@ const ForgotPasswordForm = ({ ) : !email ? ( <Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text> ) : ( - <TouchableOpacity onPress={onPressNext}> + <TouchableOpacity testID="newPasswordButton" onPress={onPressNext}> <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text> </TouchableOpacity> )} @@ -451,10 +459,11 @@ const SetNewPasswordForm = ({ You will receive an email with a "reset code." Enter that code here, then enter your new password. </Text> - <View style={styles.group}> + <View testID="newPasswordView" style={styles.group}> <View style={[styles.groupContent, {borderTopWidth: 0}]}> <FontAwesomeIcon icon="ticket" style={styles.groupContentIcon} /> <TextInput + testID="resetCodeInput" style={[styles.textInput]} placeholder="Reset code" placeholderTextColor={colors.blue0} @@ -469,6 +478,7 @@ const SetNewPasswordForm = ({ <View style={styles.groupContent}> <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} /> <TextInput + testID="newPasswordInput" style={styles.textInput} placeholder="New password" placeholderTextColor={colors.blue0} @@ -501,7 +511,7 @@ const SetNewPasswordForm = ({ ) : !resetCode || !password ? ( <Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text> ) : ( - <TouchableOpacity onPress={onPressNext}> + <TouchableOpacity testID="setNewPasswordButton" onPress={onPressNext}> <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text> </TouchableOpacity> )} diff --git a/src/view/com/notifications/InviteAccepter.tsx b/src/view/com/notifications/InviteAccepter.tsx index eefe7a273..a8789b171 100644 --- a/src/view/com/notifications/InviteAccepter.tsx +++ b/src/view/com/notifications/InviteAccepter.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react' +import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -13,7 +13,8 @@ import {s, colors, gradients} from '../../lib/styles' export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { const store = useStores() - const [confirmationUri, setConfirmationUri] = useState<string>('') + // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment + const [confirmationUri, setConfirmationUri] = React.useState<string>('') const isMember = confirmationUri !== '' || store.me.memberships?.isMemberOf(item.author.did) const onPressAccept = async () => { @@ -54,7 +55,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { return ( <View style={styles.container}> {!isMember ? ( - <TouchableOpacity onPress={onPressAccept}> + <TouchableOpacity testID="acceptInviteButton" onPress={onPressAccept}> <LinearGradient colors={[gradients.primary.start, gradients.primary.end]} start={{x: 0, y: 0}} @@ -64,7 +65,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { </LinearGradient> </TouchableOpacity> ) : ( - <View style={styles.inviteAccepted}> + <View testID="inviteAccepted" style={styles.inviteAccepted}> <FontAwesomeIcon icon="check" size={14} style={s.mr5} /> <Text style={[s.gray5, s.f15]}>Invite accepted</Text> </View> diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index 5dbb6219e..12d5a2177 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' import { @@ -18,7 +18,8 @@ export const PostRepostedBy = observer(function PostRepostedBy({ uri: string }) { const store = useStores() - const [view, setView] = useState<RepostedByViewModel | undefined>() + // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment + const [view, setView] = React.useState<RepostedByViewModel | undefined>() useEffect(() => { if (view?.params.uri === uri) { diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx index 17ed9f9f8..af5bc2475 100644 --- a/src/view/com/post-thread/PostVotedBy.tsx +++ b/src/view/com/post-thread/PostVotedBy.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' import { @@ -20,7 +20,7 @@ export const PostVotedBy = observer(function PostVotedBy({ direction: 'up' | 'down' }) { const store = useStores() - const [view, setView] = useState<VotesViewModel | undefined>() + const [view, setView] = React.useState<VotesViewModel | undefined>() useEffect(() => { if (view?.params.uri === uri) { diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 76f595cd4..f3402428e 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -25,6 +25,7 @@ export const Feed = observer(function Feed({ onPressCompose, onPressTryAgain, onScroll, + testID, }: { feed: FeedModel style?: StyleProp<ViewStyle> @@ -32,6 +33,7 @@ export const Feed = observer(function Feed({ onPressCompose: () => void onPressTryAgain?: () => void onScroll?: OnScrollCb + testID?: string }) { // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your @@ -83,7 +85,7 @@ export const Feed = observer(function Feed({ <View /> ) return ( - <View style={style}> + <View testID={testID} style={style}> {!data && <ComposePrompt onPressCompose={onPressCompose} />} {feed.isLoading && !data && <PostFeedLoadingPlaceholder />} {feed.hasError && ( diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index e6e710ff3..26939c7ce 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' import { @@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({ name: string }) { const store = useStores() - const [view, setView] = useState<UserFollowersViewModel | undefined>() + const [view, setView] = React.useState<UserFollowersViewModel | undefined>() useEffect(() => { if (view?.params.user === name) { diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 73e765d19..03c5b13bb 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' import { @@ -10,7 +10,7 @@ import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' import {UserAvatar} from '../util/UserAvatar' -import {s, colors} from '../../lib/styles' +import {s} from '../../lib/styles' import {usePalette} from '../../lib/hooks/usePalette' export const ProfileFollows = observer(function ProfileFollows({ @@ -19,7 +19,7 @@ export const ProfileFollows = observer(function ProfileFollows({ name: string }) { const store = useStores() - const [view, setView] = useState<UserFollowsViewModel | undefined>() + const [view, setView] = React.useState<UserFollowsViewModel | undefined>() useEffect(() => { if (view?.params.user === name) { diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index a4d7c7a92..4fd766952 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -147,7 +147,7 @@ export const ProfileHeader = observer(function ProfileHeader({ // = if (view.hasError) { return ( - <View> + <View testID="profileHeaderHasError"> <Text>{view.error}</Text> </View> ) @@ -192,6 +192,7 @@ export const ProfileHeader = observer(function ProfileHeader({ <View style={[styles.buttonsLine]}> {isMe ? ( <TouchableOpacity + testID="profileHeaderEditProfileButton" onPress={onPressEditProfile} style={[styles.btn, styles.mainBtn, pal.btn]}> <Text type="button" style={pal.text}> @@ -214,7 +215,9 @@ export const ProfileHeader = observer(function ProfileHeader({ </Text> </TouchableOpacity> ) : ( - <TouchableOpacity onPress={onPressToggleFollow}> + <TouchableOpacity + testID="profileHeaderToggleFollowButton" + onPress={onPressToggleFollow}> <LinearGradient colors={[gradient[1], gradient[0]]} start={{x: 0, y: 0}} @@ -257,6 +260,7 @@ export const ProfileHeader = observer(function ProfileHeader({ </View> <View style={styles.metricsLine}> <TouchableOpacity + testID="profileHeaderFollowersButton" style={[s.flexRow, s.mr10]} onPress={onPressFollowers}> <Text type="body2" style={[s.bold, s.mr2, pal.text]}> @@ -268,6 +272,7 @@ export const ProfileHeader = observer(function ProfileHeader({ </TouchableOpacity> {view.isUser ? ( <TouchableOpacity + testID="profileHeaderFollowsButton" style={[s.flexRow, s.mr10]} onPress={onPressFollows}> <Text type="body2" style={[s.bold, s.mr2, pal.text]}> @@ -280,6 +285,7 @@ export const ProfileHeader = observer(function ProfileHeader({ ) : undefined} {view.isScene ? ( <TouchableOpacity + testID="profileHeaderMembersButton" style={[s.flexRow, s.mr10]} onPress={onPressMembers}> <Text type="body2" style={[s.bold, s.mr2, pal.text]}> @@ -350,7 +356,9 @@ export const ProfileHeader = observer(function ProfileHeader({ </View> {view.isScene && view.creator === store.me.did ? ( <View style={[styles.sceneAdminContainer, pal.border]}> - <TouchableOpacity onPress={onPressInviteMembers}> + <TouchableOpacity + testID="profileHeaderInviteMembersButton" + onPress={onPressInviteMembers}> <LinearGradient colors={[gradient[1], gradient[0]]} start={{x: 0, y: 0}} @@ -369,6 +377,7 @@ export const ProfileHeader = observer(function ProfileHeader({ </View> ) : undefined} <TouchableOpacity + testID="profileHeaderAviButton" style={[pal.view, {borderColor: pal.colors.background}, styles.avi]} onPress={onPressAvi}> <UserAvatar diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx index 7f566c198..a63de9e32 100644 --- a/src/view/com/profile/ProfileMembers.tsx +++ b/src/view/com/profile/ProfileMembers.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, View} from 'react-native' import {MembersViewModel, MemberItem} from '../../../state/models/members-view' @@ -12,7 +12,8 @@ export const ProfileMembers = observer(function ProfileMembers({ name: string }) { const store = useStores() - const [view, setView] = useState<MembersViewModel | undefined>() + // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment + const [view, setView] = React.useState<MembersViewModel | undefined>() useEffect(() => { if (view?.params.actor === name) { @@ -37,7 +38,7 @@ export const ProfileMembers = observer(function ProfileMembers({ view.params.actor !== name ) { return ( - <View> + <View testID="profileMembersActivityIndicatorView"> <ActivityIndicator /> </View> ) @@ -68,7 +69,7 @@ export const ProfileMembers = observer(function ProfileMembers({ /> ) return ( - <View> + <View testID="profileMembersFlatList"> <FlatList data={view.members} keyExtractor={item => item._reactKey} diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index c0ef412d8..25f171598 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -115,6 +115,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { <View style={[styles.ctrls, opts.style]}> <View style={s.flex1}> <TouchableOpacity + testID="postCtrlsReplyButton" style={styles.ctrl} hitSlop={HITSLOP} onPress={opts.onPressReply}> @@ -130,6 +131,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { </View> <View style={s.flex1}> <TouchableOpacity + testID="postCtrlsToggleRepostButton" hitSlop={HITSLOP} onPress={onPressToggleRepostWrapper} style={styles.ctrl}> @@ -156,6 +158,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { </View> <View style={s.flex1}> <TouchableOpacity + testID="postCtrlsToggleUpvoteButton" style={styles.ctrl} hitSlop={HITSLOP} onPress={onPressToggleUpvoteWrapper}> diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx index 905268d3e..ee31ad2cb 100644 --- a/src/view/com/util/error/ErrorMessage.tsx +++ b/src/view/com/util/error/ErrorMessage.tsx @@ -8,7 +8,6 @@ import { } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../text/Text' -import {colors} from '../../../lib/styles' import {useTheme} from '../../../lib/ThemeContext' import {usePalette} from '../../../lib/hooks/usePalette' @@ -26,7 +25,7 @@ export function ErrorMessage({ const theme = useTheme() const pal = usePalette('error') return ( - <View style={[styles.outer, pal.view, style]}> + <View testID="errorMessageView" style={[styles.outer, pal.view, style]}> <View style={[styles.errorIcon, {backgroundColor: theme.palette.error.icon}]}> <FontAwesomeIcon icon="exclamation" style={pal.text} size={16} /> @@ -38,7 +37,10 @@ export function ErrorMessage({ {message} </Text> {onPressTryAgain && ( - <TouchableOpacity style={styles.btn} onPress={onPressTryAgain}> + <TouchableOpacity + testID="errorMessageTryAgainButton" + style={styles.btn} + onPress={onPressTryAgain}> <FontAwesomeIcon icon="arrows-rotate" style={{color: theme.palette.error.icon}} diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index 6db54a9f2..0033195d9 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -11,16 +11,18 @@ export function ErrorScreen({ message, details, onPressTryAgain, + testID, }: { title: string message: string details?: string onPressTryAgain?: () => void + testID?: string }) { const theme = useTheme() const pal = usePalette('error') return ( - <View style={[styles.outer, pal.view]}> + <View testID={testID} style={[styles.outer, pal.view]}> <View style={styles.errorIconContainer}> <View style={[ @@ -40,6 +42,7 @@ export function ErrorScreen({ <Text style={[styles.message, pal.textLight]}>{message}</Text> {details && ( <Text + testID={`${testID}-details`} type="body2" style={[ styles.details, @@ -52,6 +55,7 @@ export function ErrorScreen({ {onPressTryAgain && ( <View style={styles.btnContainer}> <TouchableOpacity + testID="errorScreenTryAgainButton" style={[styles.btn, {backgroundColor: theme.palette.error.icon}]} onPress={onPressTryAgain}> <FontAwesomeIcon icon="arrows-rotate" style={pal.text} size={16} /> diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index c81ccf6c5..33387f894 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -75,7 +75,8 @@ export function DropdownButton({ style={style} onPress={onPress} hitSlop={HITSLOP} - ref={ref}> + // Fix an issue where specific references cause runtime error in jest environment + ref={process.env.JEST_WORKER_ID != null ? null : ref}> {children} </TouchableOpacity> ) diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index 8de56d79a..b22e52fe5 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -25,7 +25,9 @@ export const Contacts = ({navIdx, visible, params}: ScreenParams) => { return ( <View> <View style={styles.section}> - <Text style={styles.title}>Contacts</Text> + <Text testID="contactsTitle" style={styles.title}> + Contacts + </Text> </View> <View style={styles.section}> <View style={styles.searchContainer}> @@ -35,6 +37,7 @@ export const Contacts = ({navIdx, visible, params}: ScreenParams) => { style={styles.searchIcon} /> <TextInput + testID="contactsTextInput" ref={inputRef} value={searchText} style={styles.searchInput} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index d5fe7f1f9..9800c6846 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useEffect} from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' @@ -24,48 +24,48 @@ export const Home = observer(function Home({ const store = useStores() const onMainScroll = useOnMainScroll(store) const safeAreaInsets = useSafeAreaInsets() - const [hasSetup, setHasSetup] = useState<boolean>(false) + const [wasVisible, setWasVisible] = React.useState<boolean>(false) const {appState} = useAppState({ onForeground: () => doPoll(true), }) - const doPoll = (knownActive = false) => { - if ((!knownActive && appState !== 'active') || !visible) { - return - } - if (store.me.mainFeed.isLoading) { - return - } - store.log.debug('Polling home feed') - store.me.mainFeed.checkForLatest().catch(e => { - store.log.error('Failed to poll feed', e) - }) - } + const doPoll = React.useCallback( + (knownActive = false) => { + if ((!knownActive && appState !== 'active') || !visible) { + return + } + if (store.me.mainFeed.isLoading) { + return + } + store.log.debug('Polling home feed') + store.me.mainFeed.checkForLatest().catch(e => { + store.log.error('Failed to poll feed', e) + }) + }, + [appState, visible, store], + ) useEffect(() => { - let aborted = false const pollInterval = setInterval(() => doPoll(), 15e3) if (!visible) { + setWasVisible(false) + return + } else if (wasVisible) { return } + setWasVisible(true) - if (hasSetup) { - store.log.debug('Updating home feed') + store.nav.setTitle(navIdx, 'Home') + store.log.debug('Updating home feed') + if (store.me.mainFeed.hasContent) { store.me.mainFeed.update() - doPoll() } else { - store.nav.setTitle(navIdx, 'Home') - store.log.debug('Fetching home feed') - store.me.mainFeed.setup().then(() => { - if (aborted) return - setHasSetup(true) - }) + store.me.mainFeed.setup() } return () => { clearInterval(pollInterval) - aborted = true } - }, [visible, store]) + }, [visible, store, navIdx, doPoll, wasVisible]) const onPressCompose = () => { store.shell.openComposer({}) @@ -82,6 +82,7 @@ export const Home = observer(function Home({ <View style={s.flex1}> <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} /> <Feed + testID="homeFeed" key="default" feed={store.me.mainFeed} scrollElRef={scrollElRef} diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index 0315e287e..8363dbfe0 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -35,8 +35,11 @@ const SigninOrCreateAccount = ({ <Text style={styles.title}>Bluesky</Text> <Text style={styles.subtitle}>[ private beta ]</Text> </View> - <View style={s.flex1}> - <TouchableOpacity style={styles.btn} onPress={onPressCreateAccount}> + <View testID="signinOrCreateAccount" style={s.flex1}> + <TouchableOpacity + testID="createAccountButton" + style={styles.btn} + onPress={onPressCreateAccount}> <Text style={styles.btnLabel}>Create a new account</Text> </TouchableOpacity> <View style={styles.or}> @@ -60,7 +63,10 @@ const SigninOrCreateAccount = ({ </Svg> <Text style={styles.orLabel}>or</Text> </View> - <TouchableOpacity style={styles.btn} onPress={onPressSignin}> + <TouchableOpacity + testID="signInButton" + style={styles.btn} + onPress={onPressSignin}> <Text style={styles.btnLabel}>Sign in</Text> </TouchableOpacity> </View> diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx index 3591b696c..79477fa9b 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/NotFound.tsx @@ -7,7 +7,7 @@ import {useStores} from '../../state' export const NotFound = () => { const stores = useStores() return ( - <View> + <View testID="notFoundView"> <ViewHeader title="Page not found" /> <View style={{ @@ -16,7 +16,11 @@ export const NotFound = () => { paddingTop: 100, }}> <Text style={{fontSize: 40, fontWeight: 'bold'}}>Page not found</Text> - <Button title="Home" onPress={() => stores.nav.navigate('/')} /> + <Button + testID="navigateHomeButton" + title="Home" + onPress={() => stores.nav.navigate('/')} + /> </View> </View> ) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index c89c2ad13..64bb4f042 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState, useMemo} from 'react' +import React, {useEffect, useState} from 'react' import {ActivityIndicator, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -30,7 +30,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { const store = useStores() const onMainScroll = useOnMainScroll(store) const [hasSetup, setHasSetup] = useState<boolean>(false) - const uiState = useMemo( + const uiState = React.useMemo( () => new ProfileUiModel(store, {user: params.name}), [params.user], ) @@ -201,6 +201,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { ? () => ( <> <FontAwesomeIcon + testID="shouldAdminButton" icon="user-xmark" style={[s.mr5]} size={14} @@ -242,10 +243,11 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { const title = uiState.profile.displayName || uiState.profile.handle || params.name return ( - <View style={styles.container}> + <View testID="profileView" style={styles.container}> <ViewHeader title={title} /> {uiState.profile.hasError ? ( <ErrorScreen + testID="profileErrorScreen" title="Failed to load profile" message={`There was an issue when attempting to load ${params.name}`} details={uiState.profile.error} diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index d9d933b7e..5844aa11d 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -57,6 +57,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { <View style={[pal.view, pal.border, styles.inputContainer]}> <MagnifyingGlassIcon style={[pal.text, styles.inputIcon]} /> <TextInput + testID="searchTextInput" ref={textInput} placeholder="Type your query here..." placeholderTextColor={pal.colors.textLight} @@ -68,7 +69,7 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { </View> <View style={styles.outputContainer}> {query ? ( - <ScrollView onScroll={Keyboard.dismiss}> + <ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}> {autocompleteView.searchRes.map((item, i) => ( <TouchableOpacity key={i} diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx index 99f2bdab6..6a673d25f 100644 --- a/src/view/shell/mobile/Menu.tsx +++ b/src/view/shell/mobile/Menu.tsx @@ -75,6 +75,7 @@ export const Menu = observer( onPress?: () => void }) => ( <TouchableOpacity + testID="menuItemButton" style={styles.menuItem} onPress={onPress ? onPress : () => onNavigate(url || '/')}> <View style={[styles.menuItemIconWrapper]}> @@ -98,8 +99,9 @@ export const Menu = observer( ) return ( - <ScrollView style={[styles.view, pal.view]}> + <ScrollView testID="menuView" style={[styles.view, pal.view]}> <TouchableOpacity + testID="profileCardButton" onPress={() => onNavigate(`/profile/${store.me.handle}`)} style={styles.profileCard}> <UserAvatar @@ -123,6 +125,7 @@ export const Menu = observer( </View> </TouchableOpacity> <TouchableOpacity + testID="searchBtn" style={[styles.searchBtn, pal.btn]} onPress={() => onNavigate('/search')}> <MagnifyingGlassIcon diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx index 71aaa200d..433471602 100644 --- a/src/view/shell/mobile/TabsSelector.tsx +++ b/src/view/shell/mobile/TabsSelector.tsx @@ -116,11 +116,12 @@ export const TabsSelector = observer( } if (!active) { - return <View /> + return <View testID="emptyView" /> } return ( <Animated.View + testID="tabsSelectorView" style={[ styles.wrapper, {bottom: insets.bottom + 55}, @@ -129,7 +130,9 @@ export const TabsSelector = observer( <View onLayout={onLayout}> <View style={[s.p10, styles.section]}> <View style={styles.btns}> - <TouchableWithoutFeedback onPress={onPressShareTab}> + <TouchableWithoutFeedback + testID="shareButton" + onPress={onPressShareTab}> <View style={[styles.btn]}> <View style={styles.btnIcon}> <FontAwesomeIcon size={16} icon="share" /> @@ -137,7 +140,9 @@ export const TabsSelector = observer( <Text style={styles.btnText}>Share</Text> </View> </TouchableWithoutFeedback> - <TouchableWithoutFeedback onPress={onPressCloneTab}> + <TouchableWithoutFeedback + testID="cloneButton" + onPress={onPressCloneTab}> <View style={[styles.btn]}> <View style={styles.btnIcon}> <FontAwesomeIcon size={16} icon={['far', 'clone']} /> @@ -145,7 +150,9 @@ export const TabsSelector = observer( <Text style={styles.btnText}>Clone tab</Text> </View> </TouchableWithoutFeedback> - <TouchableWithoutFeedback onPress={onPressNewTab}> + <TouchableWithoutFeedback + testID="newTabButton" + onPress={onPressNewTab}> <View style={[styles.btn]}> <View style={styles.btnIcon}> <FontAwesomeIcon size={16} icon="plus" /> @@ -164,6 +171,7 @@ export const TabsSelector = observer( return ( <Swipeable key={tab.id} + testID="tabsSwipable" renderLeftActions={renderSwipeActions} renderRightActions={renderSwipeActions} leftThreshold={100} @@ -185,6 +193,7 @@ export const TabsSelector = observer( isActive && styles.active, ]}> <TouchableWithoutFeedback + testID="changeTabButton" onPress={() => onPressChangeTab(tabIndex)}> <View style={styles.tabInner}> <View style={styles.tabIcon}> @@ -203,6 +212,7 @@ export const TabsSelector = observer( </View> </TouchableWithoutFeedback> <TouchableWithoutFeedback + testID="closeTabButton" onPress={() => onCloseTab(tabIndex)}> <View style={styles.tabClose}> <FontAwesomeIcon diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 07fdfc843..ffb22bda9 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -327,7 +327,7 @@ export const MobileShell: React.FC = observer(() => { start={{x: 0, y: 0.8}} end={{x: 0, y: 1}} style={styles.outerContainer}> - <SafeAreaView style={styles.innerContainer}> + <SafeAreaView testID="noSessionView" style={styles.innerContainer}> <ErrorBoundary> <Login /> </ErrorBoundary> @@ -338,7 +338,7 @@ export const MobileShell: React.FC = observer(() => { } if (store.onboard.isOnboarding) { return ( - <View style={styles.outerContainer}> + <View testID="onboardOuterView" style={styles.outerContainer}> <View style={styles.innerContainer}> <ErrorBoundary> <Onboard /> @@ -355,7 +355,7 @@ export const MobileShell: React.FC = observer(() => { backgroundColor: theme.colorScheme === 'dark' ? colors.gray7 : colors.gray1, } return ( - <View style={[styles.outerContainer, pal.view]}> + <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> <StatusBar barStyle={ theme.colorScheme === 'dark' ? 'light-content' : 'dark-content' |