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:
Amanda Bullington
2023-02-14 13:14:38 -08:00
committed by GitHub
parent 092ebb189d
commit 7a98b6faf1
36 changed files with 527 additions and 366 deletions

View File

@@ -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/).

View File

@@ -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,

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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">

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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";

View File

@@ -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

View File

@@ -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 } );
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -14,3 +14,4 @@ X-Observations =
[one] 1 Observación
*[other] { $count } Observaciones
}
Welcome-back = Bienvenido de nuevo,

View File

@@ -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,"
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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( );
}
);
} );

View File

@@ -1,4 +1,3 @@
import "i18n";
import "react-native-gesture-handler/jestSetup";
import mockBottomSheet from "@gorhom/bottom-sheet/mock";

View File

@@ -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( );
} );
} );

View File

@@ -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>

View File

@@ -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();
} );
} );

View File

@@ -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( );
} );
} );
} );

View File

@@ -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( );

View File

@@ -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,

View File

@@ -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( );
} );

View File

@@ -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();
} );
} );

View File

@@ -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( );
} );
} );
} );

View File

@@ -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" );

View File

@@ -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" );

View File

@@ -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();
} );
} );
} );

View File

@@ -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( );
} );
} );

View File

@@ -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(

View File

@@ -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();

View 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" );
} );
} );
} );