mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-10-08 02:52:57 -04:00
Merge branch 'main' into mob-1327-tabs
This commit is contained in:
commit
ea30e0bdfc
14 files changed
+330
-134
No files matched your search
+10
-14
@@ -471,13 +471,11 @@ lane :internal do
|
||||
end
|
||||
changelog = get_changelog
|
||||
|
||||
# TODO: disabled because we have just migrated this app to a new Google Play account and
|
||||
# have not yet created credentials to talk to the upload API
|
||||
# upload_to_play_store(
|
||||
# aab: aab_path,
|
||||
# track: "internal",
|
||||
# version_name: last_tag
|
||||
# )
|
||||
upload_to_play_store(
|
||||
aab: aab_path,
|
||||
track: "internal",
|
||||
version_name: last_tag
|
||||
)
|
||||
|
||||
upload_to_testflight(
|
||||
ipa: get_ipa_path,
|
||||
@@ -513,13 +511,11 @@ lane :beta do
|
||||
build_number = get_build_number( xcodeproj: XCODEPROJ )
|
||||
|
||||
# UI.message "Play Store: promoting build #{build_number} to beta track"
|
||||
# TODO: disabled because we have just migrated this app to a new Google Play account and
|
||||
# have not yet created credentials to talk to the upload API
|
||||
# upload_to_play_store(
|
||||
# version_code: build_number,
|
||||
# track: "internal",
|
||||
# track_promote_to: "beta"
|
||||
# )
|
||||
upload_to_play_store(
|
||||
version_code: build_number,
|
||||
track: "internal",
|
||||
track_promote_to: "beta"
|
||||
)
|
||||
|
||||
UI.message "TestFlight: adding build #{build_number} to Open Beta group"
|
||||
add_build_to_testflight_beta_group(
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Tailwind/NativeWind className for a standard 62x62 rounded thumbnail
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const THUMBNAIL_CLASS = "w-[62px] h-[62px] rounded-lg";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { THUMBNAIL_CLASS } from "appConstants/classNames";
|
||||
import {
|
||||
Body1,
|
||||
Body3,
|
||||
@@ -22,8 +23,6 @@ import React from "react";
|
||||
import { useCurrentUser, useTranslation } from "sharedHooks";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
const THUMBNAIL_CLASS = "w-[62px] h-[62px] rounded-lg";
|
||||
|
||||
function subjectLabel( subject: ExploreV2Subject | null, t: TFunction ): string {
|
||||
if ( !subject ) { return t( "All-organisms" ); }
|
||||
switch ( subject.type ) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ApiPost } from "api/types";
|
||||
import { THUMBNAIL_CLASS } from "appConstants/classNames";
|
||||
import {
|
||||
Body1,
|
||||
List2,
|
||||
@@ -29,7 +30,7 @@ const PostListItem = ( {
|
||||
{item.parent.icon_url && (
|
||||
<Image
|
||||
source={{ uri: item.parent.icon_url }}
|
||||
className="w-[62px] h-[62px] rounded-lg"
|
||||
className={THUMBNAIL_CLASS}
|
||||
accessibilityRole="image"
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ApiObservation } from "api/types";
|
||||
import { THUMBNAIL_CLASS } from "appConstants/classNames";
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
INatIcon,
|
||||
@@ -20,10 +21,8 @@ const ObservationIcon = ( {
|
||||
return (
|
||||
<View
|
||||
className={classnames(
|
||||
"w-[62px]",
|
||||
"h-[62px]",
|
||||
THUMBNAIL_CLASS,
|
||||
"bg-white",
|
||||
"rounded-lg",
|
||||
"border-[2px]",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
@@ -42,10 +41,8 @@ const ObservationIcon = ( {
|
||||
return (
|
||||
<View
|
||||
className={classnames(
|
||||
"w-[62px]",
|
||||
"h-[62px]",
|
||||
THUMBNAIL_CLASS,
|
||||
"bg-white",
|
||||
"rounded-lg",
|
||||
"border-[2px]",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
@@ -63,7 +60,7 @@ const ObservationIcon = ( {
|
||||
return (
|
||||
<Image
|
||||
testID="ObservationIcon.photo"
|
||||
className="w-[62px] h-[62px] rounded-lg"
|
||||
className={THUMBNAIL_CLASS}
|
||||
source={{ uri: photoUri }}
|
||||
accessibilityRole="image"
|
||||
accessibilityIgnoresInvertColors
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ProjectRulePreference } from "api/types";
|
||||
import { THUMBNAIL_CLASS } from "appConstants/classNames";
|
||||
import classnames from "classnames";
|
||||
import displayProjectType from "components/Projects/helpers/displayProjectType";
|
||||
import {
|
||||
@@ -31,7 +32,7 @@ interface Props {
|
||||
const ProjectListItem = ( { item, isHeader = false }: Props ) => {
|
||||
const { t, i18n } = useTranslation( );
|
||||
|
||||
const iconClassName = "w-[62px] h-[62px] rounded-lg bg-white mr-3";
|
||||
const iconClassName = classnames( THUMBNAIL_CLASS, "bg-white mr-3" );
|
||||
|
||||
const displayBriefcase = ( ) => (
|
||||
<INatIcon
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
useNetInfo,
|
||||
} from "@react-native-community/netinfo";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
signOut,
|
||||
} from "components/LoginSignUp/AuthenticationService";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body2,
|
||||
@@ -13,65 +5,32 @@ import {
|
||||
Heading4,
|
||||
} from "components/SharedComponents";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
View,
|
||||
} from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
import QueueItem from "realmModels/QueueItem";
|
||||
import {
|
||||
useLayoutPrefs,
|
||||
useTranslation,
|
||||
useUserMe,
|
||||
} from "sharedHooks";
|
||||
import useNavigateToAccountSettings from "sharedHooks/useNavigateToAccountSettings";
|
||||
|
||||
import LanguageSetting from "./LanguageSetting";
|
||||
import TaxonNamesSetting from "./TaxonNamesSetting";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const SETTINGS_URL = `${Config.OAUTH_API_URL}/users/edit?noh1=true`;
|
||||
const FINISHED_WEB_SETTINGS = "finished-web-settings";
|
||||
|
||||
const LoggedInDefaultSettings = ( ) => {
|
||||
const realm = useRealm( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
const {
|
||||
remoteUser, isLoading, refetchUserMe,
|
||||
} = useUserMe( { updateRealm: false } );
|
||||
const {
|
||||
setIsDefaultMode,
|
||||
} = useLayoutPrefs();
|
||||
const [settings, setSettings] = useState( {} );
|
||||
const [isSaving, setIsSaving] = useState( false );
|
||||
const [showingWebViewSettings, setShowingWebViewSettings] = useState( false );
|
||||
|
||||
useFocusEffect(
|
||||
useCallback( () => {
|
||||
if ( showingWebViewSettings ) {
|
||||
// When we get back from the webview of settings - in case the user updated their profile
|
||||
// photo or other details
|
||||
refetchUserMe();
|
||||
setShowingWebViewSettings( false );
|
||||
}
|
||||
}, [showingWebViewSettings, refetchUserMe] ),
|
||||
);
|
||||
|
||||
const confirmInternetConnection = useCallback( ( ) => {
|
||||
if ( !isConnected ) {
|
||||
Alert.alert(
|
||||
t( "Internet-Connection-Required" ),
|
||||
t( "Please-try-again-when-you-are-connected-to-the-internet" ),
|
||||
);
|
||||
}
|
||||
return isConnected;
|
||||
}, [t, isConnected] );
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigateToAccountSettings = useNavigateToAccountSettings( { onFinish: refetchUserMe } );
|
||||
|
||||
useEffect( () => {
|
||||
if ( remoteUser ) {
|
||||
@@ -81,18 +40,6 @@ const LoggedInDefaultSettings = ( ) => {
|
||||
}
|
||||
}, [remoteUser, realm] );
|
||||
|
||||
// Listen for the webview to finish so we can fetch the updates users/me
|
||||
// response
|
||||
useEffect( ( ) => {
|
||||
const listener = EventRegister.addEventListener(
|
||||
FINISHED_WEB_SETTINGS,
|
||||
refetchUserMe,
|
||||
);
|
||||
return ( ) => {
|
||||
EventRegister?.removeEventListener( listener );
|
||||
};
|
||||
}, [refetchUserMe] );
|
||||
|
||||
return (
|
||||
<View className="mt-[30px]">
|
||||
{( isSaving || isLoading ) && (
|
||||
@@ -143,41 +90,7 @@ const LoggedInDefaultSettings = ( ) => {
|
||||
<Button
|
||||
className="mt-4"
|
||||
text={t( "ACCOUNT-SETTINGS" )}
|
||||
onPress={() => {
|
||||
confirmInternetConnection( );
|
||||
if ( !isConnected ) { return; }
|
||||
setShowingWebViewSettings( true );
|
||||
|
||||
navigation.navigate( "FullPageWebView", {
|
||||
title: t( "ACCOUNT-SETTINGS" ),
|
||||
loggedIn: true,
|
||||
initialUrl: SETTINGS_URL,
|
||||
blurEvent: FINISHED_WEB_SETTINGS,
|
||||
clickablePathnames: ["/users/delete"],
|
||||
skipSetSourceInShouldStartLoadWithRequest: true,
|
||||
shouldLoadUrl: url => {
|
||||
async function signOutGoHome() {
|
||||
Alert.alert(
|
||||
t( "Account-Deleted" ),
|
||||
t( "It-may-take-up-to-an-hour-to-remove-content" ),
|
||||
);
|
||||
// sign out
|
||||
await signOut( { realm, clearRealm: true, queryClient } );
|
||||
// revert back to default mode
|
||||
setIsDefaultMode( true );
|
||||
// navigate to My Obs
|
||||
navigation.navigate( "ObsList" );
|
||||
}
|
||||
// If the webview navigates to a URL that indicates the account
|
||||
// was deleted, sign the current user out of the app
|
||||
if ( url === `${Config.OAUTH_API_URL}/?account_deleted=true` ) {
|
||||
signOutGoHome( );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
} );
|
||||
}}
|
||||
onPress={navigateToAccountSettings}
|
||||
accessibilityLabel={t( "INATURALIST-SETTINGS" )}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { THUMBNAIL_CLASS } from "appConstants/classNames";
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
DisplayTaxonName,
|
||||
@@ -32,7 +33,7 @@ const DisplayTaxon = ( {
|
||||
const { t } = useTranslation( );
|
||||
const currentUser = useCurrentUser( );
|
||||
|
||||
const imageClassName = "w-[62px] h-[62px] rounded-lg";
|
||||
const imageClassName = THUMBNAIL_CLASS;
|
||||
|
||||
const iconicTaxonName = taxon?.isIconic
|
||||
? taxon.name
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Button,
|
||||
Heading1,
|
||||
Heading4,
|
||||
INatIconButton,
|
||||
List2,
|
||||
OverviewCounts,
|
||||
ScrollViewWrapper,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { TabStackScreenProps } from "navigation/types";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import User from "realmModels/User";
|
||||
import { formatLongDate } from "sharedHelpers/dateAndTime";
|
||||
import {
|
||||
@@ -27,8 +28,10 @@ import {
|
||||
useFeatureFlag,
|
||||
useTranslation,
|
||||
} from "sharedHooks";
|
||||
import useNavigateToAccountSettings from "sharedHooks/useNavigateToAccountSettings";
|
||||
import { FeatureFlag } from "stores/createFeatureFlagSlice";
|
||||
import useStore from "stores/useStore";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
import FollowButtonContainer from "./FollowButtonContainer";
|
||||
import UnfollowSheet from "./UnfollowSheet";
|
||||
@@ -46,10 +49,13 @@ const UserProfile = ( ) => {
|
||||
const { t, i18n } = useTranslation( );
|
||||
|
||||
const fetchId = userId || login;
|
||||
const { data: remoteUser, isError, error }: {
|
||||
const {
|
||||
data: remoteUser, isError, error, refetch: refetchUser,
|
||||
}: {
|
||||
data: ApiUser | null;
|
||||
isError: boolean;
|
||||
error: INatApiError | ErrorWithResponse;
|
||||
refetch: ( ) => void;
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteUser", fetchId],
|
||||
optsWithAuth => fetchRemoteUser( fetchId, {}, optsWithAuth ),
|
||||
@@ -78,18 +84,29 @@ const UserProfile = ( ) => {
|
||||
},
|
||||
);
|
||||
|
||||
// useEffect( ( ) => {
|
||||
// const headerRight = ( ) => currentUser?.login === user?.login && (
|
||||
// <INatIconButton
|
||||
// icon="pencil"
|
||||
// color={colors.darkGray}
|
||||
// size={22}
|
||||
// accessibilityLabel={t( "Edit" )}
|
||||
// />
|
||||
// );
|
||||
const isCurrentUser = !!currentUser && currentUser?.login === user?.login;
|
||||
const navigateToAccountSettings = useNavigateToAccountSettings( { onFinish: refetchUser } );
|
||||
|
||||
// navigation.setOptions( { headerRight } );
|
||||
// }, [navigation, user, currentUser] );
|
||||
const headerRight = useCallback(
|
||||
( ) => ( isCurrentUser
|
||||
? (
|
||||
<INatIconButton
|
||||
testID="UserProfile.editButton"
|
||||
onPress={navigateToAccountSettings}
|
||||
icon="pencil"
|
||||
color={String( colors?.darkGray )}
|
||||
size={22}
|
||||
accessibilityLabel={t( "Edit" )}
|
||||
/>
|
||||
)
|
||||
: null ),
|
||||
[isCurrentUser, navigateToAccountSettings, t],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
( ) => navigation.setOptions( { headerRight } ),
|
||||
[headerRight, navigation],
|
||||
);
|
||||
|
||||
if ( isError && error?.status === 404 ) {
|
||||
return (
|
||||
|
||||
@@ -767,7 +767,7 @@ Search-suggestions-with-location = Zoek suggesties met locatie
|
||||
Search-suggestions-without-location = Zoek suggesties zonder locatie te gebruiken
|
||||
SEARCH-TAXA = ZOEK TAXA
|
||||
SEARCH-USERS = ZOEK GEBRUIKERS
|
||||
See-journal-posts = Zie dagboek
|
||||
See-journal-posts = Zie dagboek
|
||||
See-observations-by-this-user-in-Explore = Toon waarnemingen van deze gebruiker in Verkennen
|
||||
See-observations-of-this-taxon-in-explore = Toon waarnemingen van dit taxon in verkennen
|
||||
See-project-members = Toon projectdeelnemers
|
||||
|
||||
@@ -711,7 +711,7 @@
|
||||
"Search-suggestions-without-location": "Zoek suggesties zonder locatie te gebruiken",
|
||||
"SEARCH-TAXA": "ZOEK TAXA",
|
||||
"SEARCH-USERS": "ZOEK GEBRUIKERS",
|
||||
"See-journal-posts": "Zie dagboek",
|
||||
"See-journal-posts": "Zie dagboek",
|
||||
"See-observations-by-this-user-in-Explore": "Toon waarnemingen van deze gebruiker in Verkennen",
|
||||
"See-observations-of-this-taxon-in-explore": "Toon waarnemingen van dit taxon in verkennen",
|
||||
"See-project-members": "Toon projectdeelnemers",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { signOut } from "components/LoginSignUp/AuthenticationService";
|
||||
import type { TabStackScreenProps } from "navigation/types";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
|
||||
import useLayoutPrefs from "./useLayoutPrefs";
|
||||
import useTranslation from "./useTranslation";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const SETTINGS_URL = `${Config.OAUTH_API_URL}/users/edit?noh1=true`;
|
||||
const FINISHED_WEB_SETTINGS = "finished-web-settings";
|
||||
|
||||
interface Options {
|
||||
// Called when the user returns from the web settings view (e.g. to refetch
|
||||
// any profile data that may have changed)
|
||||
onFinish?: ( ) => void;
|
||||
}
|
||||
|
||||
const useNavigateToAccountSettings = ( { onFinish }: Options = {} ) => {
|
||||
const realm = useRealm( );
|
||||
const { isConnected } = useNetInfo( );
|
||||
const navigation = useNavigation<
|
||||
TabStackScreenProps<"UserProfile" | "Settings">["navigation"]
|
||||
>( );
|
||||
const { t } = useTranslation( );
|
||||
const { setIsDefaultMode } = useLayoutPrefs( );
|
||||
const queryClient = useQueryClient( );
|
||||
const [showingWebViewSettings, setShowingWebViewSettings] = useState( false );
|
||||
|
||||
useFocusEffect(
|
||||
useCallback( ( ) => {
|
||||
if ( showingWebViewSettings ) {
|
||||
// When we get back from the webview of settings - in case the user updated their profile
|
||||
// photo or other details
|
||||
onFinish?.( );
|
||||
setShowingWebViewSettings( false );
|
||||
}
|
||||
}, [showingWebViewSettings, onFinish] ),
|
||||
);
|
||||
|
||||
// Listen for the webview to finish so callers can refresh their data
|
||||
useEffect( ( ) => {
|
||||
const listener = EventRegister.addEventListener(
|
||||
FINISHED_WEB_SETTINGS,
|
||||
( ) => onFinish?.( ),
|
||||
);
|
||||
return ( ) => {
|
||||
EventRegister?.removeEventListener( listener as string );
|
||||
};
|
||||
}, [onFinish] );
|
||||
|
||||
const navigateToAccountSettings = useCallback( ( ) => {
|
||||
if ( !isConnected ) {
|
||||
Alert.alert(
|
||||
t( "Internet-Connection-Required" ),
|
||||
t( "Please-try-again-when-you-are-connected-to-the-internet" ),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setShowingWebViewSettings( true );
|
||||
|
||||
navigation.navigate( "FullPageWebView", {
|
||||
title: t( "ACCOUNT-SETTINGS" ),
|
||||
loggedIn: true,
|
||||
initialUrl: SETTINGS_URL,
|
||||
blurEvent: FINISHED_WEB_SETTINGS,
|
||||
clickablePathnames: ["/users/delete"],
|
||||
skipSetSourceInShouldStartLoadWithRequest: true,
|
||||
shouldLoadUrl: url => {
|
||||
async function signOutGoHome() {
|
||||
Alert.alert(
|
||||
t( "Account-Deleted" ),
|
||||
t( "It-may-take-up-to-an-hour-to-remove-content" ),
|
||||
);
|
||||
// sign out
|
||||
await signOut( { realm, clearRealm: true, queryClient } );
|
||||
// revert back to default mode
|
||||
setIsDefaultMode( true );
|
||||
// navigate to My Obs
|
||||
navigation.navigate( "ObsList" );
|
||||
}
|
||||
// If the webview navigates to a URL that indicates the account
|
||||
// was deleted, sign the current user out of the app
|
||||
if ( url === `${Config.OAUTH_API_URL}/?account_deleted=true` ) {
|
||||
signOutGoHome( );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
} );
|
||||
}, [isConnected, navigation, queryClient, realm, setIsDefaultMode, t] );
|
||||
|
||||
return navigateToAccountSettings;
|
||||
};
|
||||
|
||||
export default useNavigateToAccountSettings;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import UserProfile from "components/UserProfile/UserProfile";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import * as useCurrentUser from "sharedHooks/useCurrentUser";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
@@ -11,6 +12,7 @@ jest.mock( "sharedHooks/useAuthenticatedQuery", () => ( {
|
||||
__esModule: true,
|
||||
default: () => ( {
|
||||
data: mockUser,
|
||||
refetch: jest.fn(),
|
||||
} ),
|
||||
} ) );
|
||||
|
||||
@@ -22,6 +24,8 @@ jest.mock( "sharedHooks/useAuthenticatedMutation", () => ( {
|
||||
} ),
|
||||
} ) );
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockSetOptions = jest.fn();
|
||||
jest.mock( "@react-navigation/native", () => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
@@ -32,7 +36,8 @@ jest.mock( "@react-navigation/native", () => {
|
||||
},
|
||||
} ),
|
||||
useNavigation: () => ( {
|
||||
setOptions: () => ( { } ),
|
||||
navigate: mockNavigate,
|
||||
setOptions: mockSetOptions,
|
||||
} ),
|
||||
};
|
||||
} );
|
||||
@@ -47,7 +52,21 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
const renderHeaderRight = ( ) => {
|
||||
const lastCall = mockSetOptions.mock.calls.at( -1 );
|
||||
const HeaderRight = lastCall?.[0]?.headerRight;
|
||||
if ( !HeaderRight ) return null;
|
||||
renderComponent( <HeaderRight /> );
|
||||
return HeaderRight;
|
||||
};
|
||||
|
||||
describe( "UserProfile", () => {
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
// Default: viewing someone else's profile (no current user)
|
||||
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => null );
|
||||
} );
|
||||
|
||||
it( "should render inside mocked container for testing", () => {
|
||||
renderComponent( <UserProfile /> );
|
||||
expect( screen.getByTestId( "UserProfile" ) ).toBeTruthy();
|
||||
@@ -88,4 +107,41 @@ describe( "UserProfile", () => {
|
||||
const projectsButton = await screen.findByText( /VIEW PROJECTS/ );
|
||||
expect( projectsButton ).toBeVisible( );
|
||||
} );
|
||||
|
||||
describe( "edit button", () => {
|
||||
test( "is shown when viewing your own profile", async () => {
|
||||
jest.spyOn( useCurrentUser, "default" )
|
||||
.mockImplementation( () => ( { login: mockUser.login } ) );
|
||||
|
||||
renderComponent( <UserProfile /> );
|
||||
renderHeaderRight( );
|
||||
|
||||
expect( screen.getByTestId( "UserProfile.editButton" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "is not shown when viewing another user's profile", async () => {
|
||||
jest.spyOn( useCurrentUser, "default" )
|
||||
.mockImplementation( () => ( { login: "someone-else" } ) );
|
||||
|
||||
renderComponent( <UserProfile /> );
|
||||
renderHeaderRight( );
|
||||
|
||||
expect( screen.queryByTestId( "UserProfile.editButton" ) ).toBeNull( );
|
||||
} );
|
||||
|
||||
test( "navigates to account settings when pressed", async () => {
|
||||
jest.spyOn( useCurrentUser, "default" )
|
||||
.mockImplementation( () => ( { login: mockUser.login } ) );
|
||||
|
||||
renderComponent( <UserProfile /> );
|
||||
renderHeaderRight( );
|
||||
|
||||
fireEvent.press( screen.getByTestId( "UserProfile.editButton" ) );
|
||||
|
||||
expect( mockNavigate ).toHaveBeenCalledWith(
|
||||
"FullPageWebView",
|
||||
expect.objectContaining( { loggedIn: true } ),
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useNetInfo } from "@react-native-community/netinfo";
|
||||
import { act, renderHook } from "@testing-library/react-native";
|
||||
import { signOut } from "components/LoginSignUp/AuthenticationService";
|
||||
import { Alert } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
import useNavigateToAccountSettings from "sharedHooks/useNavigateToAccountSettings";
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock( "@react-navigation/native", () => ( {
|
||||
...jest.requireActual( "@react-navigation/native" ),
|
||||
useNavigation: () => ( { navigate: mockNavigate } ),
|
||||
// Run the focus callback synchronously so we don't need a NavigationContainer
|
||||
useFocusEffect: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockSetIsDefaultMode = jest.fn();
|
||||
jest.mock( "sharedHooks/useLayoutPrefs", () => ( {
|
||||
__esModule: true,
|
||||
default: () => ( { setIsDefaultMode: mockSetIsDefaultMode } ),
|
||||
} ) );
|
||||
|
||||
const mockRealm = {};
|
||||
jest.mock( "providers/contexts", () => ( {
|
||||
RealmContext: {
|
||||
useRealm: () => mockRealm,
|
||||
},
|
||||
} ) );
|
||||
|
||||
const mockQueryClient = {};
|
||||
jest.mock( "@tanstack/react-query", () => ( {
|
||||
...jest.requireActual( "@tanstack/react-query" ),
|
||||
useQueryClient: () => mockQueryClient,
|
||||
} ) );
|
||||
|
||||
jest.mock( "components/LoginSignUp/AuthenticationService", () => ( {
|
||||
__esModule: true,
|
||||
signOut: jest.fn( () => Promise.resolve() ),
|
||||
} ) );
|
||||
|
||||
const ACCOUNT_DELETED_URL = `${Config.OAUTH_API_URL}/?account_deleted=true`;
|
||||
|
||||
describe( "useNavigateToAccountSettings", () => {
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
useNetInfo.mockReturnValue( { isConnected: true } );
|
||||
} );
|
||||
|
||||
it( "alerts and does not navigate when there is no internet", () => {
|
||||
const alertSpy = jest.spyOn( Alert, "alert" ).mockImplementation( () => {} );
|
||||
useNetInfo.mockReturnValue( { isConnected: false } );
|
||||
|
||||
const { result } = renderHook( () => useNavigateToAccountSettings() );
|
||||
act( () => result.current() );
|
||||
|
||||
expect( alertSpy ).toHaveBeenCalled();
|
||||
expect( mockNavigate ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( "navigates to the account settings web view when connected", () => {
|
||||
const { result } = renderHook( () => useNavigateToAccountSettings() );
|
||||
act( () => result.current() );
|
||||
|
||||
expect( mockNavigate ).toHaveBeenCalledWith(
|
||||
"FullPageWebView",
|
||||
expect.objectContaining( {
|
||||
loggedIn: true,
|
||||
clickablePathnames: ["/users/delete"],
|
||||
} ),
|
||||
);
|
||||
} );
|
||||
|
||||
it( "signs the user out when the web view reports the account was deleted", async () => {
|
||||
const alertSpy = jest.spyOn( Alert, "alert" ).mockImplementation( () => {} );
|
||||
|
||||
const { result } = renderHook( () => useNavigateToAccountSettings() );
|
||||
act( () => result.current() );
|
||||
|
||||
const { shouldLoadUrl } = mockNavigate.mock.calls[0][1];
|
||||
const shouldLoad = shouldLoadUrl( ACCOUNT_DELETED_URL );
|
||||
|
||||
// Block the web view from loading the deletion-confirmation URL
|
||||
expect( shouldLoad ).toBe( false );
|
||||
// Allow the async signOut flow inside shouldLoadUrl to resolve
|
||||
await Promise.resolve();
|
||||
|
||||
expect( signOut ).toHaveBeenCalledWith(
|
||||
expect.objectContaining( { realm: mockRealm, clearRealm: true } ),
|
||||
);
|
||||
expect( mockSetIsDefaultMode ).toHaveBeenCalledWith( true );
|
||||
expect( mockNavigate ).toHaveBeenCalledWith( "ObsList" );
|
||||
expect( alertSpy ).toHaveBeenCalled();
|
||||
} );
|
||||
it( "calls onFinish when the web settings finished event fires", () => {
|
||||
const onFinish = jest.fn();
|
||||
let registeredCallback;
|
||||
jest.spyOn( EventRegister, "addEventListener" )
|
||||
.mockImplementation( ( eventName, cb ) => {
|
||||
registeredCallback = cb;
|
||||
return "listener-id";
|
||||
} );
|
||||
|
||||
renderHook( () => useNavigateToAccountSettings( { onFinish } ) );
|
||||
|
||||
expect( registeredCallback ).toBeDefined();
|
||||
registeredCallback();
|
||||
expect( onFinish ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
Reference in new issue
Block a user