mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Timeless dates (#457)
* Add DateDisplay to ObsCard and make first pass at translation strings * Add failing tests (due to lack of localization) for timeless dates * WIP: trying to ensure i18next gets initialized before tests run The remaining test failures might be legit. This probably breaks the actual app, though. * Got the rest of the tests working * Updated tests to assume UTC * Updated README to advise against using `npx jest` so test runs always have the env vars we specify in our `npm test` script * Moved i18next initialization to an explicitly-named file * Use i18next init function in app * Fixed up remaining tests * Added test for non-English localization of date format * Cleanup * Made DateDisplay explicitly handle strings not Dates * Restore skipped localization tests for MyObservations * Remove duplicative tests from DateDisplay unit test * Added note to the README about initializing i18next * Updated change to DateDisplay in main --------- Co-authored-by: Ken-ichi Ueda <kenichi.ueda@gmail.com>
This commit is contained in:
committed by
GitHub
parent
092ebb189d
commit
7a98b6faf1
10
README.md
10
README.md
@@ -52,12 +52,16 @@ We're using [Jest](https://jestjs.io/) and [React Native Testing Library](https:
|
||||
npm test
|
||||
|
||||
# Run test paths matching a pattern
|
||||
npx jest MyObs
|
||||
npm test MyObs
|
||||
|
||||
# Run individual tests matching a pattern
|
||||
npx jest -t accessibility
|
||||
# Run individual tests matching a pattern. Note the `--` to pass arguments to jest
|
||||
npm test -- -t accessibility
|
||||
```
|
||||
|
||||
Note that you can run `npx jest` as well, but that will omit some environment variables we need to set for the test environment, so for consistent test runs please use `npm test`.
|
||||
|
||||
Also note that `i18next` needs to be initialized in individual test files (haven't figured out a way to await initialization before *all* tests, plus allowing tests to control initialization helps when testing different locales). Add `beforeAll( async ( ) => { await initI18next( ); } );` to a test file if it depends on localized text.
|
||||
|
||||
### E2E tests
|
||||
We're using [Detox](https://wix.github.io/Detox/docs/19.x/) for E2E tests. If you want to run the e2e tests on your local machine, make sure you fulfill the RN development requirements, see above, and also follow the iOS specific [environment setup](https://wix.github.io/Detox/docs/19.x/introduction/ios-dev-env/).
|
||||
|
||||
|
||||
5
index.js
5
index.js
@@ -1,7 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import "i18n";
|
||||
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
import handleError from "api/error";
|
||||
import App from "components/App";
|
||||
import { getJWT } from "components/LoginSignUp/AuthenticationService";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
@@ -51,6 +50,8 @@ setNativeExceptionHandler( exceptionString => {
|
||||
|
||||
startNetworkLogging();
|
||||
|
||||
initI18next();
|
||||
|
||||
// Configure inatjs to use the chosen URLs
|
||||
inatjs.setConfig( {
|
||||
apiURL: Config.API_URL,
|
||||
|
||||
@@ -158,7 +158,7 @@ const ActivityHeader = ( { item, refetchRemoteObservation, toggleRefetch }:Props
|
||||
<View className="flex-row ml-4 justify-between">
|
||||
<InlineUser user={user} />
|
||||
{( item._created_at )
|
||||
? <DateDisplay dateTime={item._created_at} />
|
||||
? <DateDisplay dateString={item.created_at} />
|
||||
: ifCommentOrID()}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -50,12 +50,7 @@ const SearchTaxonIcon = (
|
||||
size={25}
|
||||
/>
|
||||
)}
|
||||
accessible
|
||||
// TODO: this uses a Pressable under the hood, but we want this actually not to be pressable,
|
||||
// but overriding this with a role of "none" errors out the a11y test matcher
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t( "None" )}
|
||||
accessibilityState={{ disabled: true }}
|
||||
accessibilityElementsHidden
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -142,10 +137,8 @@ const AddID = ( { route }: Props ): Node => {
|
||||
icon="pencil"
|
||||
size={25}
|
||||
onPress={() => navigation.navigate( "TaxonDetails", { id: taxon.id } )}
|
||||
accessible
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={t( "Navigate-to-taxon-details" )}
|
||||
accessibilityValue={{ text: taxon.name }}
|
||||
accessibilityState={{ disabled: false }}
|
||||
/>
|
||||
<IconButton
|
||||
@@ -156,10 +149,8 @@ const AddID = ( { route }: Props ): Node => {
|
||||
onIDAdded( createIdentification( taxon ) );
|
||||
if ( goBackOnSave ) { navigation.goBack( ); }
|
||||
}}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t( "Add-this-ID" )}
|
||||
accessibilityValue={{ text: taxon.name }}
|
||||
accessibilityState={{ disabled: false }}
|
||||
/>
|
||||
</View>
|
||||
@@ -234,6 +225,7 @@ const AddID = ( { route }: Props ): Node => {
|
||||
"Search-for-a-taxon-to-add-an-identification"
|
||||
)}
|
||||
accessibilityRole="search"
|
||||
accessibilityState={{ disabled: false }}
|
||||
/>
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
|
||||
@@ -49,7 +49,7 @@ const ObsCard = ( { item, handlePress }: Props ): Node => {
|
||||
<View className="flex-row shrink">
|
||||
{obsListPhoto}
|
||||
<View className="shrink">
|
||||
<ObsCardDetails item={item} />
|
||||
<ObsCardDetails observation={item} />
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row items-center justify-items-center ml-2">
|
||||
|
||||
@@ -2,42 +2,39 @@
|
||||
|
||||
import DisplayTaxonName from "components/DisplayTaxonName";
|
||||
import checkCamelAndSnakeCase from "components/ObsDetails/helpers/checkCamelAndSnakeCase";
|
||||
import { Body4, DateDisplay } from "components/SharedComponents";
|
||||
import { Text, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import { formatObsListTime } from "sharedHelpers/dateAndTime";
|
||||
|
||||
type Props = {
|
||||
item: Object,
|
||||
observation: Object,
|
||||
view?: string,
|
||||
};
|
||||
|
||||
const ObsCardDetails = ( { item = "list", view }: Props ): Node => {
|
||||
const placeGuess = checkCamelAndSnakeCase( item, "placeGuess" );
|
||||
|
||||
const displayTime = ( ) => {
|
||||
if ( item._created_at ) {
|
||||
return formatObsListTime( item._created_at );
|
||||
}
|
||||
return "no time given";
|
||||
};
|
||||
|
||||
const ObsCardDetails = ( { observation, view = "list" }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
let displayLocation = checkCamelAndSnakeCase( observation, "placeGuess" );
|
||||
if ( !displayLocation && observation.latitude ) {
|
||||
displayLocation = `${observation.latitude}, ${observation.longitude}`;
|
||||
}
|
||||
if ( !displayLocation ) {
|
||||
displayLocation = t( "Missing-Location" );
|
||||
}
|
||||
return (
|
||||
<View className={view === "grid" && "border border-border p-2"}>
|
||||
<DisplayTaxonName
|
||||
taxon={item.taxon}
|
||||
scientificNameFirst={item?.user?.prefers_scientific_name_first}
|
||||
taxon={observation.taxon}
|
||||
scientificNameFirst={observation?.user?.prefers_scientific_name_first}
|
||||
layout={view === "list" ? "horizontal" : "vertical"}
|
||||
/>
|
||||
<Text numberOfLines={1}>
|
||||
<Text numberOfLines={1} className="mb-2">
|
||||
<IconMaterial name="location-pin" size={15} />
|
||||
{placeGuess || "no place guess"}
|
||||
</Text>
|
||||
<Text numberOfLines={1}>
|
||||
<IconMaterial name="watch-later" size={15} />
|
||||
{displayTime( )}
|
||||
<Body4 className="text-darkGray ml-[5px]">{displayLocation}</Body4>
|
||||
</Text>
|
||||
<DateDisplay dateString={observation.time_observed_at || observation.observed_on_string} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,28 +5,21 @@ import { View } from "components/styledComponents";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import { formatObsListTime } from "sharedHelpers/dateAndTime";
|
||||
import { formatApiDatetime } from "sharedHelpers/dateAndTime";
|
||||
|
||||
type Props = {
|
||||
dateTime: string | typeof undefined
|
||||
dateString: String
|
||||
};
|
||||
|
||||
const DateDisplay = ( {
|
||||
dateTime
|
||||
dateString
|
||||
}: Props ): React.Node => {
|
||||
const { t } = useTranslation( );
|
||||
const displayTime = ( ) => {
|
||||
if ( dateTime ) {
|
||||
return formatObsListTime( dateTime );
|
||||
}
|
||||
return t( "no time given" );
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex flex-row items-center">
|
||||
<IconMaterial name="watch-later" size={15} />
|
||||
<Body4 className="text-darkGray ml-[5px]">
|
||||
{displayTime( )}
|
||||
{formatApiDatetime( dateString, t )}
|
||||
</Body4>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,10 @@ const NavBar = ( ): React.Node => {
|
||||
screen: MESSAGES_SCREEN_ID
|
||||
} );
|
||||
|
||||
// TODO this renders A LOT and should not constantly be fetching the curret
|
||||
// user. Also adds an async effect that messes with tests. We should have
|
||||
// everything we need to know about the current user cached locally
|
||||
// ~~~kueda 2023-02-14
|
||||
const { remoteUser: user } = useUserMe( );
|
||||
|
||||
const footerHeight = Platform.OS === "ios" ? "h-20" : "h-15";
|
||||
|
||||
@@ -262,7 +262,7 @@ const UiLibrary = () => {
|
||||
/>
|
||||
|
||||
<Heading2 className="my-2">Date Display Component</Heading2>
|
||||
<DateDisplay dateTime="2023-12-14T21:07:41-09:30" />
|
||||
<DateDisplay dateString="2023-12-14T21:07:41-09:30" />
|
||||
|
||||
<Heading2 className="my-2">ObservationLocation Component</Heading2>
|
||||
<ObservationLocation
|
||||
|
||||
@@ -23,25 +23,26 @@ import { initReactI18next } from "react-i18next";
|
||||
// generate before building the app
|
||||
import loadTranslations from "./loadTranslations";
|
||||
|
||||
// Initialize and configure i18next
|
||||
i18next
|
||||
.use( initReactI18next )
|
||||
.use( Fluent )
|
||||
.use( resourcesToBackend( ( locale, namespace, callback ) => {
|
||||
// Note that we're not using i18next namespaces at present
|
||||
callback( null, loadTranslations( locale ) );
|
||||
} ) )
|
||||
.init( {
|
||||
export const I18NEXT_CONFIG = {
|
||||
// Added since otherwise Android would crash - see here: https://stackoverflow.com/a/70521614 and https://www.i18next.com/misc/migration-guide
|
||||
lng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss
|
||||
},
|
||||
react: {
|
||||
// Added since otherwise Android would crash - see here: https://stackoverflow.com/a/70521614 and https://www.i18next.com/misc/migration-guide
|
||||
lng: "en",
|
||||
// debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss
|
||||
},
|
||||
react: {
|
||||
// Added since otherwise Android would crash - see here: https://stackoverflow.com/a/70521614 and https://www.i18next.com/misc/migration-guide
|
||||
useSuspense: false
|
||||
}
|
||||
} );
|
||||
useSuspense: false
|
||||
}
|
||||
};
|
||||
|
||||
export default i18next;
|
||||
export default async function initI18next( config = {} ) {
|
||||
// Initialize and configure i18next
|
||||
return i18next
|
||||
.use( initReactI18next )
|
||||
.use( Fluent )
|
||||
.use( resourcesToBackend( ( locale, namespace, callback ) => {
|
||||
// Note that we're not using i18next namespaces at present
|
||||
callback( null, loadTranslations( locale ) );
|
||||
} ) )
|
||||
.init( { ...I18NEXT_CONFIG, ...config } );
|
||||
}
|
||||
@@ -262,6 +262,8 @@ Mammals = Mammals
|
||||
|
||||
Media = Media
|
||||
|
||||
Missing-Date = Missing Date
|
||||
|
||||
Mollusks = Mollusks
|
||||
|
||||
# The following Month- strings are the months of the year (in month order, not alphabetical order)
|
||||
@@ -809,3 +811,8 @@ x-identifications = {$count ->
|
||||
[one] {$count} identification
|
||||
*[other] {$count} identifications
|
||||
}
|
||||
|
||||
# Date formatting using date-fns
|
||||
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
|
||||
date-format-short = M/d/yy
|
||||
datetime-format-short = M/d/yy h:mm a
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
"Low": "Low",
|
||||
"Mammals": "Mammals",
|
||||
"Media": "Media",
|
||||
"Missing-Date": "Missing Date",
|
||||
"Mollusks": "Mollusks",
|
||||
"Month-January": {
|
||||
"comment": "The following Month- strings are the months of the year (in month order, not alphabetical order)",
|
||||
@@ -590,5 +591,10 @@
|
||||
"Open-side-menu": "Open side menu",
|
||||
"Intl-number": "{ $val }",
|
||||
"x-comments": "{ $count ->\n [one] { $count } comment\n *[other] { $count } comments\n}",
|
||||
"x-identifications": "{ $count ->\n [one] { $count } identification\n *[other] { $count } identifications\n}"
|
||||
"x-identifications": "{ $count ->\n [one] { $count } identification\n *[other] { $count } identifications\n}",
|
||||
"date-format-short": {
|
||||
"comment": "Date formatting using date-fns\nSee complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format",
|
||||
"val": "M/d/yy"
|
||||
},
|
||||
"datetime-format-short": "M/d/yy h:mm a"
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ List-View = List View
|
||||
OBSERVATIONS = OBSERVATIONS
|
||||
Observations = Observations
|
||||
Your-Observations = Tus observaciones
|
||||
date-format-short = d/M/yy
|
||||
datetime-format-short = d/M/yy h:mm a
|
||||
@@ -12,5 +12,7 @@
|
||||
},
|
||||
"OBSERVATIONS": "OBSERVATIONS",
|
||||
"Observations": "Observations",
|
||||
"Your-Observations": "Tus observaciones"
|
||||
"Your-Observations": "Tus observaciones",
|
||||
"date-format-short": "d/M/yy",
|
||||
"datetime-format-short": "d/M/yy h:mm a"
|
||||
}
|
||||
|
||||
@@ -14,3 +14,4 @@ X-Observations =
|
||||
[one] 1 Observación
|
||||
*[other] { $count } Observaciones
|
||||
}
|
||||
Welcome-back = Bienvenido de nuevo,
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"Observations": "Observaciones",
|
||||
"Your-Observations": "Sus observaciones",
|
||||
"TAXONOMY-header": "TAXONOMIE es bueno",
|
||||
"X-Observations": "{ $count ->\n [one] 1 Observación\n *[other] { $count } Observaciones\n}"
|
||||
"X-Observations": "{ $count ->\n [one] 1 Observación\n *[other] { $count } Observaciones\n}",
|
||||
"Welcome-back": "Bienvenido de nuevo,"
|
||||
}
|
||||
|
||||
@@ -262,6 +262,8 @@ Mammals = Mammals
|
||||
|
||||
Media = Media
|
||||
|
||||
Missing-Date = Missing Date
|
||||
|
||||
Mollusks = Mollusks
|
||||
|
||||
# The following Month- strings are the months of the year (in month order, not alphabetical order)
|
||||
@@ -809,3 +811,8 @@ x-identifications = {$count ->
|
||||
[one] {$count} identification
|
||||
*[other] {$count} identifications
|
||||
}
|
||||
|
||||
# Date formatting using date-fns
|
||||
# See complete list of formatting styles: https://date-fns.org/v2.29.3/docs/format
|
||||
date-format-short = M/d/yy
|
||||
datetime-format-short = M/d/yy h:mm a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
differenceInDays, differenceInHours, differenceInMinutes,
|
||||
format, formatDistanceToNow, formatISO, fromUnixTime, getUnixTime, getYear, parseISO
|
||||
format, formatDistanceToNow, formatISO, fromUnixTime, getUnixTime, getYear,
|
||||
parseISO
|
||||
} from "date-fns";
|
||||
|
||||
const formatISONoTimezone = date => {
|
||||
@@ -27,6 +28,19 @@ const displayDateTimeObsEdit = date => date && format( new Date( date ), "PPpp"
|
||||
|
||||
const timeAgo = pastTime => formatDistanceToNow( new Date( pastTime ) );
|
||||
|
||||
const formatApiDatetime = ( dateString, t ) => {
|
||||
if ( !dateString || dateString === "" ) {
|
||||
return t( "Missing-Date" );
|
||||
}
|
||||
const hasTime = dateString.includes( "T" );
|
||||
const date = parseISO( dateString );
|
||||
|
||||
if ( hasTime ) {
|
||||
return format( date, t( "datetime-format-short" ) );
|
||||
}
|
||||
return format( date, t( "date-format-short" ) );
|
||||
};
|
||||
|
||||
const formatObsListTime = date => {
|
||||
const dateTime = "M/d/yy h:mm a";
|
||||
if ( typeof date === "string" ) {
|
||||
@@ -70,6 +84,7 @@ const formatIdDate = ( date, t ) => {
|
||||
export {
|
||||
createObservedOnStringForUpload,
|
||||
displayDateTimeObsEdit,
|
||||
formatApiDatetime,
|
||||
formatDateStringFromTimestamp,
|
||||
formatIdDate,
|
||||
formatISONoTimezone,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import { screen, waitFor } from "@testing-library/react-native";
|
||||
import ObsList from "components/Observations/ObsList";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import i18next from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import React from "react";
|
||||
|
||||
@@ -11,9 +13,11 @@ import factory, { makeResponse } from "../factory";
|
||||
import { renderAppWithComponent } from "../helpers/render";
|
||||
import { signIn, signOut } from "../helpers/user";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
|
||||
describe( "MyObservations", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
beforeEach( signOut );
|
||||
|
||||
afterEach( ( ) => {
|
||||
@@ -35,17 +39,15 @@ describe( "MyObservations", ( ) => {
|
||||
// } );
|
||||
|
||||
describe( "when signed out", ( ) => {
|
||||
async function testApiMethodNotCalled( apiMethod, language ) {
|
||||
async function testApiMethodNotCalled( apiMethod ) {
|
||||
// Let's make sure the mock hasn't already been used
|
||||
expect( apiMethod ).not.toHaveBeenCalled( );
|
||||
const signedInUsers = global.realm.objects( "User" ).filtered( "signedIn == true" );
|
||||
expect( signedInUsers.length ).toEqual( 0 );
|
||||
renderAppWithComponent( <ObsList /> );
|
||||
// TODO: We should really address this globally in the test suite. On first render,
|
||||
// text doesn't have a language set, but on second render, text will default to English.
|
||||
const textByLanguage = language === "en" ? "Log in to iNaturalist" : "Log-in-to-iNaturalist";
|
||||
const loginText = i18next.t( "Log-in-to-iNaturalist" );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( textByLanguage ) ).toBeTruthy( );
|
||||
expect( screen.getByText( loginText ) ).toBeTruthy( );
|
||||
} );
|
||||
// Unpleasant, but without adjusting the timeout it doesn't seem like
|
||||
// all of these requests get caught
|
||||
@@ -54,10 +56,10 @@ describe( "MyObservations", ( ) => {
|
||||
}, { timeout: 3000, interval: 500 } );
|
||||
}
|
||||
it( "should not make a request to users/me", async ( ) => {
|
||||
await testApiMethodNotCalled( inatjs.users.me, undefined );
|
||||
await testApiMethodNotCalled( inatjs.users.me );
|
||||
} );
|
||||
it( "should not make a request to observations/updates", async ( ) => {
|
||||
await testApiMethodNotCalled( inatjs.observations.updates, "en" );
|
||||
await testApiMethodNotCalled( inatjs.observations.updates );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -73,7 +75,7 @@ describe( "MyObservations", ( ) => {
|
||||
expect( screen.queryByText( /Welcome-back/ ) ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it.skip( "should be Spanish if signed in user's locale is Spanish", async ( ) => {
|
||||
it( "should be Spanish if signed in user's locale is Spanish", async ( ) => {
|
||||
const mockSpanishUser = factory( "LocalUser", {
|
||||
locale: "es"
|
||||
} );
|
||||
@@ -81,12 +83,12 @@ describe( "MyObservations", ( ) => {
|
||||
await signIn( mockSpanishUser );
|
||||
renderAppWithComponent( <ObsList /> );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( / Observaciones/ ) ).toBeTruthy();
|
||||
expect( screen.getByText( /Bienvenido/ ) ).toBeTruthy();
|
||||
} );
|
||||
expect( screen.queryByText( /X-Observations/ ) ).toBeFalsy( );
|
||||
expect( screen.queryByText( /Welcome/ ) ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it.skip(
|
||||
it(
|
||||
"should change to es when local user locale is en but remote user locale is es",
|
||||
async ( ) => {
|
||||
const mockUser = factory( "LocalUser" );
|
||||
@@ -105,8 +107,8 @@ describe( "MyObservations", ( ) => {
|
||||
await waitFor( ( ) => {
|
||||
expect( inatjs.users.me ).toHaveBeenCalled( );
|
||||
} );
|
||||
expect( await screen.findByText( / Observaciones/ ) ).toBeTruthy( );
|
||||
expect( screen.queryByText( /X-Observations/ ) ).toBeFalsy( );
|
||||
expect( await screen.findByText( /Bienvenido/ ) ).toBeTruthy( );
|
||||
expect( screen.queryByText( /Welcome/ ) ).toBeFalsy( );
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "i18n";
|
||||
import "react-native-gesture-handler/jestSetup";
|
||||
|
||||
import mockBottomSheet from "@gorhom/bottom-sheet/mock";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import About from "components/About";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
import Mailer from "react-native-mail";
|
||||
|
||||
@@ -9,10 +10,16 @@ jest.mock( "react-native-mail", ( ) => ( {
|
||||
mail: jest.fn( )
|
||||
} ) );
|
||||
|
||||
test( "native email client is opened on button press", ( ) => {
|
||||
renderComponent( <About /> );
|
||||
const debugLogButton = screen.getByText( /EMAIL-DEBUG-LOGS/ );
|
||||
expect( debugLogButton ).toBeTruthy( );
|
||||
fireEvent.press( debugLogButton );
|
||||
expect( Mailer.mail ).toHaveBeenCalled( );
|
||||
describe( "email logs button", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should open the native email client", ( ) => {
|
||||
renderComponent( <About /> );
|
||||
const debugLogButton = screen.getByText( /EMAIL DEBUG LOGS/ );
|
||||
expect( debugLogButton ).toBeTruthy( );
|
||||
fireEvent.press( debugLogButton );
|
||||
expect( Mailer.mail ).toHaveBeenCalled( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import AddID from "components/ObsEdit/AddID";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { t } from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
@@ -10,10 +11,6 @@ import { renderComponent } from "../../../helpers/render";
|
||||
// Mock inaturalistjs so we can make some fake responses
|
||||
jest.mock( "inaturalistjs" );
|
||||
|
||||
// this resolves a test failure with the Animated library:
|
||||
// Animated: `useNativeDriver` is not supported because the native animated module is missing.
|
||||
jest.useFakeTimers( );
|
||||
|
||||
jest.mock(
|
||||
"components/SharedComponents/ViewNoFooter",
|
||||
() => function MockViewNoFooter( props ) {
|
||||
@@ -68,9 +65,32 @@ jest.mock( "@gorhom/bottom-sheet", () => {
|
||||
};
|
||||
} );
|
||||
|
||||
// react-native-paper's TextInput does a bunch of async stuff that's hard to
|
||||
// control in a test, so we're just mocking it here.
|
||||
jest.mock( "react-native-paper", () => {
|
||||
const RealModule = jest.requireActual( "react-native-paper" );
|
||||
const MockTextInput = props => {
|
||||
const MockName = "mock-text-input";
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <MockName {...props}>{props.children}</MockName>;
|
||||
};
|
||||
MockTextInput.Icon = RealModule.TextInput.Icon;
|
||||
const MockedModule = {
|
||||
...RealModule,
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
// TextInput: props => <View {...props}>{props.children}</View>
|
||||
TextInput: MockTextInput
|
||||
};
|
||||
return MockedModule;
|
||||
} );
|
||||
|
||||
const mockRoute = { params: {} };
|
||||
|
||||
describe( "AddID", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", ( ) => {
|
||||
const addID = (
|
||||
<INatPaperProvider>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import ActivityItem from "components/ObsDetails/ActivityItem";
|
||||
import FlagItemModal from "components/ObsDetails/FlagItemModal";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
import { Provider as PaperProvider } from "react-native-paper";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
const mockCallback = jest.fn();
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
created_at: "2022-11-27T19:07:41-08:00",
|
||||
@@ -50,72 +50,78 @@ jest.mock( "react-native-keyboard-aware-scroll-view", () => {
|
||||
return { KeyboardAwareScrollView };
|
||||
} );
|
||||
|
||||
test( "renders activity item with Flag Button", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
describe( "Flags", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
it( "renders activity item with Flag Button", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( screen.getByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Flag" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders Flag Modal when Flag button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
fireEvent.press( await screen.findByTestId( "MenuItem.Flag" ) );
|
||||
expect( screen.queryByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", true );
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders Flag Modal content", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Other" ) ).toBeTruthy( );
|
||||
expect( screen.getAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
} );
|
||||
|
||||
test( "calls flag api when save button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
fireEvent.press( screen.queryByText( "Spam" ) );
|
||||
expect( screen.getByText( "Save" ) ).toBeTruthy( );
|
||||
fireEvent.press( screen.queryByText( "Save" ) );
|
||||
expect( await mockMutate ).toHaveBeenCalled();
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( screen.getByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Flag" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "renders Flag Modal when Flag button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<PaperProvider>
|
||||
<ActivityItem item={mockIdentification} />
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
|
||||
expect( await screen.findByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", false );
|
||||
|
||||
fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
|
||||
expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
|
||||
fireEvent.press( await screen.findByTestId( "MenuItem.Flag" ) );
|
||||
expect( screen.queryByTestId( "FlagItemModal" ) ).toHaveProperty( "props.visible", true );
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "renders Flag Modal content", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Other" ) ).toBeTruthy( );
|
||||
expect( screen.getAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
} );
|
||||
|
||||
it( "calls flag api when save button pressed", async ( ) => {
|
||||
renderComponent(
|
||||
<FlagItemModal
|
||||
id="000"
|
||||
itemType="foo"
|
||||
showFlagItemModal
|
||||
closeFlagItemModal={mockCallback}
|
||||
onItemFlagged={mockCallback}
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
|
||||
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
|
||||
expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
|
||||
fireEvent.press( screen.queryByText( "Spam" ) );
|
||||
expect( screen.getByText( "Save" ) ).toBeTruthy( );
|
||||
fireEvent.press( screen.queryByText( "Save" ) );
|
||||
expect( await mockMutate ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import ObsDetails from "components/ObsDetails/ObsDetails";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
@@ -9,8 +10,6 @@ import useIsConnected from "sharedHooks/useIsConnected";
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
|
||||
const mockNavigate = jest.fn( );
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
created_at: "2022-11-27T19:07:41-08:00",
|
||||
@@ -92,70 +91,76 @@ jest.mock( "sharedHooks/useUserLocation", ( ) => ( {
|
||||
} ) );
|
||||
|
||||
describe( "ObsDetails", () => {
|
||||
test( "should not have accessibility errors", async () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should not have accessibility errors", async () => {
|
||||
renderComponent( <ObsDetails /> );
|
||||
const obsDetails = await screen.findByTestId(
|
||||
`ObsDetails.${mockObservation.uuid}`
|
||||
);
|
||||
expect( obsDetails ).toBeAccessible();
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "renders obs details from remote call", async ( ) => {
|
||||
useIsConnected.mockImplementation( ( ) => true );
|
||||
renderComponent( <ObsDetails /> );
|
||||
|
||||
expect( await screen.findByTestId( `ObsDetails.${mockObservation.uuid}` ) ).toBeTruthy( );
|
||||
expect( screen.getByText( mockObservation.taxon.name ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "renders data tab on button press", async ( ) => {
|
||||
renderComponent( <ObsDetails /> );
|
||||
const button = await screen.findByTestId( "ObsDetails.DataTab" );
|
||||
expect( screen.queryByTestId( "mock-data-tab" ) ).not.toBeTruthy( );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( await screen.findByTestId( "mock-data-tab" ) ).toBeTruthy();
|
||||
} );
|
||||
|
||||
describe( "Observation with no evidence", () => {
|
||||
beforeEach( () => {
|
||||
useAuthenticatedQuery.mockReturnValue( {
|
||||
data: mockNoEvidenceObservation
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "should render fallback image icon instead of photos", async () => {
|
||||
it( "renders obs details from remote call", async ( ) => {
|
||||
useIsConnected.mockImplementation( ( ) => true );
|
||||
renderComponent( <ObsDetails /> );
|
||||
|
||||
const labelText = t( "Observation-has-no-photos-and-no-sounds" );
|
||||
const fallbackImage = await screen.findByLabelText( labelText );
|
||||
expect( fallbackImage ).toBeTruthy( );
|
||||
expect( await screen.findByTestId( `ObsDetails.${mockObservation.uuid}` ) ).toBeTruthy( );
|
||||
expect( screen.getByText( mockObservation.taxon.name ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
useAuthenticatedQuery.mockReturnValue( {
|
||||
data: mockObservation
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "activity tab", ( ) => {
|
||||
test( "navigates to taxon details on button press", async ( ) => {
|
||||
it( "renders data tab on button press", async ( ) => {
|
||||
renderComponent( <ObsDetails /> );
|
||||
fireEvent.press( await screen.findByTestId( `ObsDetails.taxon.${mockObservation.taxon.id}` ) );
|
||||
expect( mockNavigate ).toHaveBeenCalledWith( "TaxonDetails", {
|
||||
id: mockObservation.taxon.id
|
||||
const button = await screen.findByTestId( "ObsDetails.DataTab" );
|
||||
expect( screen.queryByTestId( "mock-data-tab" ) ).not.toBeTruthy( );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( await screen.findByTestId( "mock-data-tab" ) ).toBeTruthy();
|
||||
} );
|
||||
|
||||
describe( "Observation with no evidence", () => {
|
||||
beforeEach( () => {
|
||||
useAuthenticatedQuery.mockReturnValue( {
|
||||
data: mockNoEvidenceObservation
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "should render fallback image icon instead of photos", async () => {
|
||||
useIsConnected.mockImplementation( ( ) => true );
|
||||
renderComponent( <ObsDetails /> );
|
||||
|
||||
const labelText = t( "Observation-has-no-photos-and-no-sounds" );
|
||||
const fallbackImage = await screen.findByLabelText( labelText );
|
||||
expect( fallbackImage ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
useAuthenticatedQuery.mockReturnValue( {
|
||||
data: mockObservation
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "shows network error image instead of observation photos if user is offline", async ( ) => {
|
||||
useIsConnected.mockImplementation( ( ) => false );
|
||||
renderComponent( <ObsDetails /> );
|
||||
const labelText = t( "Observation-photos-unavailable-without-internet" );
|
||||
const noInternet = await screen.findByLabelText( labelText );
|
||||
expect( noInternet ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "PhotoScroll.photo" ) ).toBeNull( );
|
||||
describe( "activity tab", ( ) => {
|
||||
it( "navigates to taxon details on button press", async ( ) => {
|
||||
renderComponent( <ObsDetails /> );
|
||||
fireEvent.press(
|
||||
await screen.findByTestId( `ObsDetails.taxon.${mockObservation.taxon.id}` )
|
||||
);
|
||||
expect( mockNavigate ).toHaveBeenCalledWith( "TaxonDetails", {
|
||||
id: mockObservation.taxon.id
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "shows network error image instead of observation photos if user is offline", async ( ) => {
|
||||
useIsConnected.mockImplementation( ( ) => false );
|
||||
renderComponent( <ObsDetails /> );
|
||||
const labelText = t( "Observation-photos-unavailable-without-internet" );
|
||||
const noInternet = await screen.findByLabelText( labelText );
|
||||
expect( noInternet ).toBeTruthy( );
|
||||
expect( screen.queryByTestId( "PhotoScroll.photo" ) ).toBeNull( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
|
||||
import DeleteObservationDialog from "components/ObsEdit/DeleteObservationDialog";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import i18next from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
@@ -8,8 +10,6 @@ import React from "react";
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
|
||||
beforeEach( async ( ) => {
|
||||
global.realm.write( ( ) => {
|
||||
global.realm.deleteAll( );
|
||||
@@ -63,6 +63,10 @@ const getLocalObservation = uuid => global.realm
|
||||
.objectForPrimaryKey( "Observation", uuid );
|
||||
|
||||
describe( "delete observation", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
describe( "delete an unsynced observation", ( ) => {
|
||||
it( "should delete an observation from realm", async ( ) => {
|
||||
const observations = [factory( "LocalObservation", {
|
||||
@@ -75,7 +79,8 @@ describe( "delete observation", ( ) => {
|
||||
expect( localObservation ).toBeTruthy( );
|
||||
mockObsEditProviderWithObs( observations );
|
||||
renderDeleteDialog( );
|
||||
const deleteButton = screen.queryByText( /Yes-delete-observation/ );
|
||||
const deleteButtonText = i18next.t( "Yes-delete-observation" );
|
||||
const deleteButton = screen.queryByText( deleteButtonText );
|
||||
expect( deleteButton ).toBeTruthy( );
|
||||
fireEvent.press( deleteButton );
|
||||
expect( getLocalObservation( observations[0].uuid ) ).toBeFalsy( );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import ObsEdit from "components/ObsEdit/ObsEdit";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
@@ -62,7 +63,11 @@ const renderObsEdit = ( ) => renderComponent(
|
||||
);
|
||||
|
||||
describe( "ObsEdit", () => {
|
||||
test( "should not have accessibility errors", async () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should not have accessibility errors", async () => {
|
||||
const observations = [
|
||||
factory( "RemoteObservation", {
|
||||
latitude: 37.99,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
fireEvent, render, screen, waitFor
|
||||
} from "@testing-library/react-native";
|
||||
import ObsCard from "components/Observations/ObsCard";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -10,55 +11,63 @@ const testObservation = factory( "LocalObservation", {
|
||||
taxon: { preferred_common_name: "Foo", name: "bar" }
|
||||
} );
|
||||
|
||||
test.only( "renders text passed into observation card", async ( ) => {
|
||||
render(
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( "ObsList.photo" ).props.source )
|
||||
.toStrictEqual( { uri: testObservation.observationPhotos[0].photo.url } );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
`${testObservation.taxon.preferred_common_name}${testObservation.taxon.name}`
|
||||
);
|
||||
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy( );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( testObservation.comments.length.toString( ) ) ).toBeTruthy( );
|
||||
describe( "ObsCard", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( testObservation.identifications.length.toString( ) ) ).toBeTruthy( );
|
||||
|
||||
it( "renders text passed into observation card", async ( ) => {
|
||||
render(
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( "ObsList.photo" ).props.source )
|
||||
.toStrictEqual( { uri: testObservation.observationPhotos[0].photo.url } );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
`${testObservation.taxon.preferred_common_name} ${testObservation.taxon.name}`
|
||||
);
|
||||
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy( );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( testObservation.comments.length.toString( ) ) ).toBeTruthy( );
|
||||
} );
|
||||
await waitFor( ( ) => {
|
||||
expect(
|
||||
screen.getByText( testObservation.identifications.length.toString( ) )
|
||||
).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "navigates to ObsDetails on button press", ( ) => {
|
||||
const fakeNavigation = {
|
||||
navigate: jest.fn( )
|
||||
};
|
||||
|
||||
render(
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => fakeNavigation.navigate( "ObsDetails" )}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( fakeNavigation.navigate ).toBeCalledWith( "ObsDetails" );
|
||||
} );
|
||||
|
||||
it( "should not have accessibility errors", ( ) => {
|
||||
const obsCard = (
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect( obsCard ).toBeAccessible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "navigates to ObsDetails on button press", ( ) => {
|
||||
const fakeNavigation = {
|
||||
navigate: jest.fn( )
|
||||
};
|
||||
|
||||
render(
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => fakeNavigation.navigate( "ObsDetails" )}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId( `ObsList.obsCard.${testObservation.uuid}` );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( fakeNavigation.navigate ).toBeCalledWith( "ObsDetails" );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", ( ) => {
|
||||
const obsCard = (
|
||||
<ObsCard
|
||||
item={testObservation}
|
||||
handlePress={( ) => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect( obsCard ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { render, screen } from "@testing-library/react-native";
|
||||
import ObsCardDetails from "components/Observations/ObsCardDetails";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -8,13 +9,19 @@ const testObservation = factory( "LocalObservation", {
|
||||
taxon: { preferred_common_name: "Foo", name: "bar" }
|
||||
} );
|
||||
|
||||
test( "renders correct taxon and observation details", () => {
|
||||
render(
|
||||
<ObsCardDetails view="list" item={testObservation} />
|
||||
);
|
||||
describe( "ObsCardDetails", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
`${testObservation.taxon.preferred_common_name} ${testObservation.taxon.name}`
|
||||
);
|
||||
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy();
|
||||
it( "renders correct taxon and observation details", () => {
|
||||
render(
|
||||
<ObsCardDetails view="list" observation={testObservation} />
|
||||
);
|
||||
|
||||
expect( screen.getByTestId( "display-taxon-name" ) ).toHaveTextContent(
|
||||
`${testObservation.taxon.preferred_common_name} ${testObservation.taxon.name}`
|
||||
);
|
||||
expect( screen.getByText( testObservation.placeGuess ) ).toBeTruthy();
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -2,13 +2,12 @@ import {
|
||||
fireEvent, screen, within
|
||||
} from "@testing-library/react-native";
|
||||
import ObsList from "components/Observations/ObsList";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.useFakeTimers( );
|
||||
|
||||
const mockObservations = [
|
||||
factory( "LocalObservation", {
|
||||
comments: [
|
||||
@@ -54,54 +53,58 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
it( "renders an observation", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
const obs = mockObservations[0];
|
||||
|
||||
const list = await screen.findByTestId( "ObservationViews.myObservations" );
|
||||
// Test that there isn't other data lingering
|
||||
expect( list.props.data.length ).toEqual( mockObservations.length );
|
||||
// Test that a card got rendered for the our test obs
|
||||
const card = await screen.findByTestId( `ObsList.obsCard.${obs.uuid}` );
|
||||
expect( card ).toBeTruthy( );
|
||||
// Test that the card has the correct comment count
|
||||
const commentCount = within( card ).getByTestId( "ActivityCount.commentCount" );
|
||||
// TODO: I disabled node eslint rule here because we will soon have to refactor this
|
||||
// test into it's own unit test, because the comment count will be a component
|
||||
// after the refactor we should change this line to be in compliance with the eslint rule
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
|
||||
} );
|
||||
|
||||
it( "renders multiple observations", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
// Awaiting the first observation because using await in the forEach errors out
|
||||
const firstObs = mockObservations[0];
|
||||
await screen.findByTestId( `ObsList.obsCard.${firstObs.uuid}` );
|
||||
mockObservations.forEach( obs => {
|
||||
expect( screen.getByTestId( `ObsList.obsCard.${obs.uuid}` ) ).toBeTruthy();
|
||||
} );
|
||||
// TODO: some things are still happening in the background so I unmount here,
|
||||
// better probably to mock away those things happening in the background for this test
|
||||
screen.unmount();
|
||||
} );
|
||||
|
||||
it( "renders grid view on button press", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
const button = await screen.findByTestId( "ObsList.toggleGridView" );
|
||||
fireEvent.press( button );
|
||||
// Awaiting the first observation because using await in the forEach errors out
|
||||
const firstObs = mockObservations[0];
|
||||
await screen.findByTestId( `ObsList.gridItem.${firstObs.uuid}` );
|
||||
mockObservations.forEach( obs => {
|
||||
expect( screen.getByTestId( `ObsList.gridItem.${obs.uuid}` ) ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "ObsList", () => {
|
||||
test( "should not have accessibility errors", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should not have accessibility errors", () => {
|
||||
renderComponent( <ObsList /> );
|
||||
const obsList = screen.getByTestId( "ObservationViews.myObservations" );
|
||||
expect( obsList ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
it( "renders an observation", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
const obs = mockObservations[0];
|
||||
|
||||
const list = await screen.findByTestId( "ObservationViews.myObservations" );
|
||||
// Test that there isn't other data lingering
|
||||
expect( list.props.data.length ).toEqual( mockObservations.length );
|
||||
// Test that a card got rendered for the our test obs
|
||||
const card = await screen.findByTestId( `ObsList.obsCard.${obs.uuid}` );
|
||||
expect( card ).toBeTruthy( );
|
||||
// Test that the card has the correct comment count
|
||||
const commentCount = within( card ).getByTestId( "ActivityCount.commentCount" );
|
||||
// TODO: I disabled node eslint rule here because we will soon have to refactor this
|
||||
// test into it's own unit test, because the comment count will be a component
|
||||
// after the refactor we should change this line to be in compliance with the eslint rule
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
|
||||
} );
|
||||
|
||||
it( "renders multiple observations", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
// Awaiting the first observation because using await in the forEach errors out
|
||||
const firstObs = mockObservations[0];
|
||||
await screen.findByTestId( `ObsList.obsCard.${firstObs.uuid}` );
|
||||
mockObservations.forEach( obs => {
|
||||
expect( screen.getByTestId( `ObsList.obsCard.${obs.uuid}` ) ).toBeTruthy();
|
||||
} );
|
||||
// TODO: some things are still happening in the background so I unmount here,
|
||||
// better probably to mock away those things happening in the background for this test
|
||||
screen.unmount();
|
||||
} );
|
||||
|
||||
it( "renders grid view on button press", async ( ) => {
|
||||
renderComponent( <ObsList /> );
|
||||
const button = await screen.findByTestId( "ObsList.toggleGridView" );
|
||||
fireEvent.press( button );
|
||||
// Awaiting the first observation because using await in the forEach errors out
|
||||
const firstObs = mockObservations[0];
|
||||
await screen.findByTestId( `ObsList.gridItem.${firstObs.uuid}` );
|
||||
mockObservations.forEach( obs => {
|
||||
expect( screen.getByTestId( `ObsList.gridItem.${obs.uuid}` ) ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import PhotoGallery from "components/PhotoImporter/PhotoGallery";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
// this resolves a test failure with the Animated library:
|
||||
// Animated: `useNativeDriver` is not supported because the native animated module is missing.
|
||||
jest.useFakeTimers( );
|
||||
|
||||
const mockPhoto = factory( "DevicePhoto" );
|
||||
|
||||
jest.mock( "components/PhotoImporter/hooks/useCameraRollPhotos", ( ) => ( {
|
||||
@@ -74,6 +71,10 @@ test( "renders photos from photo gallery", ( ) => {
|
||||
} );
|
||||
|
||||
describe( "PhotoGallery", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", async () => {
|
||||
renderPhotoGallery( );
|
||||
const photoGallery = await screen.findByTestId( "photo-gallery" );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import ProjectObservations from "components/Projects/ProjectObservations";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -30,6 +31,10 @@ jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
} ) );
|
||||
|
||||
describe( "ProjectObservations", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", async ( ) => {
|
||||
renderComponent( <ProjectObservations /> );
|
||||
const projectObservations = await screen.findByTestId( "ProjectObservations.grid" );
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import Projects from "components/Projects/Projects";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -26,25 +27,31 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
test( "displays project search results", ( ) => {
|
||||
renderComponent( <Projects /> );
|
||||
|
||||
const input = screen.getByTestId( "ProjectSearch.input" );
|
||||
fireEvent.changeText( input, "butterflies" );
|
||||
|
||||
expect( screen.getByText( mockProject.title ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( `Project.${mockProject.id}.photo` ).props.source )
|
||||
.toStrictEqual( { uri: mockProject.icon } );
|
||||
fireEvent.press( screen.getByTestId( `Project.${mockProject.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "ProjectDetails", {
|
||||
id: mockProject.id
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "Projects", () => {
|
||||
test( "should not have accessibility errors", async ( ) => {
|
||||
it( "should display project search results", ( ) => {
|
||||
renderComponent( <Projects /> );
|
||||
const projectObservations = await screen.findByTestId( "Projects" );
|
||||
expect( projectObservations ).toBeAccessible();
|
||||
|
||||
const input = screen.getByTestId( "ProjectSearch.input" );
|
||||
fireEvent.changeText( input, "butterflies" );
|
||||
|
||||
expect( screen.getByText( mockProject.title ) ).toBeTruthy( );
|
||||
expect( screen.getByTestId( `Project.${mockProject.id}.photo` ).props.source )
|
||||
.toStrictEqual( { uri: mockProject.icon } );
|
||||
fireEvent.press( screen.getByTestId( `Project.${mockProject.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "ProjectDetails", {
|
||||
id: mockProject.id
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( "accessibility", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should not have errors", async ( ) => {
|
||||
renderComponent( <Projects /> );
|
||||
const projectObservations = await screen.findByTestId( "Projects" );
|
||||
expect( projectObservations ).toBeAccessible();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import { DateDisplay } from "components/SharedComponents";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
describe( "DateDisplay", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should be accessible", () => {
|
||||
expect( <DateDisplay dateTime="2023-12-14T21:07:41-09:30" /> ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
it( "should format datetime correctly from date string", () => {
|
||||
renderComponent( <DateDisplay dateTime="2023-12-14T21:07:41-09:30" /> );
|
||||
expect( screen.getByText( "12/15/23 6:37 AM" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "should format datetime correctly from date string", () => {
|
||||
renderComponent( <DateDisplay dateTime="2023-01-02T21:09:41-23:30" /> );
|
||||
expect( screen.getByText( "1/3/23 8:39 PM" ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
it( "should display placeholder if no dateTime", () => {
|
||||
renderComponent( <DateDisplay dateTime={undefined} /> );
|
||||
expect( screen.getByText( "no time given" ) ).toBeTruthy( );
|
||||
expect( <DateDisplay dateString="2023-12-14T21:07:41+00:00" /> ).toBeAccessible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import { ObservationLocation } from "components/SharedComponents";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderAppWithComponent } from "../../../helpers/render";
|
||||
|
||||
describe( "ObservationLocation", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should be accessible", () => {
|
||||
const mockObservation = factory( "RemoteObservation" );
|
||||
expect(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import UserProfile from "components/UserProfile/UserProfile";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -42,6 +43,10 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe( "UserProfile", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should render inside mocked container for testing", () => {
|
||||
renderComponent( <UserProfile /> );
|
||||
expect( screen.getByTestId( "UserProfile" ) ).toBeTruthy();
|
||||
|
||||
50
tests/unit/helpers/dateAndTime.test.js
Normal file
50
tests/unit/helpers/dateAndTime.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import initI18next from "i18n/initI18next";
|
||||
import i18next from "i18next";
|
||||
import { formatApiDatetime } from "sharedHelpers/dateAndTime";
|
||||
|
||||
describe( "formatApiDatetime", ( ) => {
|
||||
describe( "in default locale", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should return missing date string if no date is present", async ( ) => {
|
||||
expect( formatApiDatetime( null, i18next.t ) ).toEqual( "Missing Date" );
|
||||
} );
|
||||
|
||||
it( "should return missing date string if date is empty", ( ) => {
|
||||
const date = "";
|
||||
expect( formatApiDatetime( date, i18next.t ) ).toEqual( "Missing Date" );
|
||||
} );
|
||||
|
||||
it( "should return a localized date when a date string is passed in", ( ) => {
|
||||
const date = "2022-11-02";
|
||||
expect( formatApiDatetime( date, i18next.t ) ).toEqual( "11/2/22" );
|
||||
} );
|
||||
|
||||
it( "should return a localized datetime when a datetime string is passed in", ( ) => {
|
||||
const date = "2022-11-02T18:43:00+00:00";
|
||||
expect( formatApiDatetime( date, i18next.t ) ).toEqual( "11/2/22 6:43 PM" );
|
||||
} );
|
||||
|
||||
it( "should return the date in the local time zone by default", () => {
|
||||
expect( process.env.TZ ).toEqual( "UTC" );
|
||||
expect(
|
||||
formatApiDatetime( "2023-01-02T08:00:00+01:00", i18next.t )
|
||||
).toEqual( "1/2/23 7:00 AM" );
|
||||
} );
|
||||
|
||||
it.todo( "should optionally show the date in the original time zone" );
|
||||
} );
|
||||
|
||||
describe( "in es-MX", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( { lng: "es-MX" } );
|
||||
} );
|
||||
|
||||
it( "should return a localized date when a date string is passed in", ( ) => {
|
||||
const date = "2022-11-02";
|
||||
expect( formatApiDatetime( date, i18next.t ) ).toEqual( "2/11/22" );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user