diff --git a/tests/unit/components/Match/PhotosSection.test.js b/tests/unit/components/Match/PhotosSection.test.js
new file mode 100644
index 000000000..c099e0fd3
--- /dev/null
+++ b/tests/unit/components/Match/PhotosSection.test.js
@@ -0,0 +1,105 @@
+import { fireEvent, screen, waitFor } from "@testing-library/react-native";
+import PhotosSection from "components/Match/PhotosSection";
+import React from "react";
+import factory from "tests/factory";
+import { renderComponent } from "tests/helpers/render";
+
+describe( "PhotosSection", () => {
+ const mockNavToTaxonDetails = jest.fn();
+
+ const defaultProps = {
+ representativePhoto: { ...factory( "RemotePhoto" ), ...{ id: 4 } },
+ obsPhotos: [factory( "LocalObservationPhoto" )],
+ navToTaxonDetails: mockNavToTaxonDetails,
+ taxon: factory( "LocalTaxon", {
+ taxonPhotos: [
+ { photo: factory( "RemotePhoto", { id: 1 } ) },
+ { photo: factory( "RemotePhoto", { id: 2 } ) },
+ { photo: factory( "RemotePhoto", { id: 3 } ) }
+ ]
+ } )
+ };
+
+ it( "displays photo count when multiple observation photos exist", async () => {
+ const multipleObsPhotos = [
+ factory( "LocalObservationPhoto" ),
+ factory( "LocalObservationPhoto" ),
+ factory( "LocalObservationPhoto" )
+ ];
+
+ renderComponent(
+
+ );
+
+ await waitFor( () => {
+ expect( screen.getByTestId( "MatchScreen.ObsPhoto" ) ).toBeVisible();
+ } );
+ expect( screen.getByText( "3" ) ).toBeVisible();
+ } );
+
+ it( "hides taxon photos when hideTaxonPhotos is true", async () => {
+ renderComponent(
+
+ );
+
+ await waitFor( () => {
+ expect( screen.getByTestId( "MatchScreen.ObsPhoto" ) ).toBeVisible();
+ } );
+ expect( screen.queryByTestId( "TaxonDetails.photo.1" ) ).toBeFalsy();
+ expect( screen.queryByTestId( "TaxonDetails.photo.4" ) ).toBeFalsy();
+ } );
+
+ // Not working due to known bug: https://linear.app/inaturalist/issue/MOB-1069/taxon-photos-hidden-for-iconic-taxa-match-screens
+ /* it( "does not render iconic taxon photos", async () => {
+ renderComponent(
+
+ );
+
+ await waitFor( () => {
+ expect( screen.getByTestId( "TaxonDetails.photo.2" ) ).toBeVisible();
+ } );
+ expect( screen.queryByTestId( "TaxonDetails.photo.1" ) ).toBeFalsy();
+ } ); */
+
+ it( "calls navToTaxonDetails prop when taxon photo is pressed", async () => {
+ renderComponent(
+
+ );
+
+ await waitFor( () => {
+ expect( screen.getByTestId( "TaxonDetails.photo.4" ) ).toBeVisible();
+ } );
+
+ const photoButton = screen.getByTestId( "TaxonDetails.photo.4" );
+ fireEvent.press( photoButton );
+
+ expect( mockNavToTaxonDetails ).toHaveBeenCalled();
+ } );
+
+ it( "limits displayed taxon photos to maximum of 3", async () => {
+ renderComponent(
+
+ );
+
+ // Representative photo + 2 more = 3 total
+ await waitFor( () => {
+ expect( screen.getByTestId( "TaxonDetails.photo.4" ) ).toBeVisible();
+ } );
+ expect( screen.getByTestId( "TaxonDetails.photo.1" ) ).toBeVisible();
+ expect( screen.getByTestId( "TaxonDetails.photo.2" ) ).toBeVisible();
+ expect( screen.queryByTestId( "TaxonDetails.photo.3" ) ).toBeFalsy();
+ } );
+} );
diff --git a/tests/unit/components/Match/PreMatchLoadingScreen.test.js b/tests/unit/components/Match/PreMatchLoadingScreen.test.js
new file mode 100644
index 000000000..4505f1667
--- /dev/null
+++ b/tests/unit/components/Match/PreMatchLoadingScreen.test.js
@@ -0,0 +1,20 @@
+import { screen } from "@testing-library/react-native";
+import PreMatchLoadingScreen from "components/Match/PreMatchLoadingScreen";
+import React from "react";
+import { renderComponent } from "tests/helpers/render";
+
+describe( "PreMatchLoadingScreen", () => {
+ it( "renders nothing when isLoading is false", () => {
+ renderComponent( );
+
+ expect( screen.queryByText( "Analyzing for the best identification..." ) ).toBeFalsy();
+ } );
+
+ it( "shows loading screen when isLoading is true", () => {
+ renderComponent( );
+
+ expect( screen.getByText( "Analyzing for the best identification..." ) ).toBeVisible();
+ // Find activity indicator
+ expect( screen.getByRole( "progressbar" ) ).toBeVisible();
+ } );
+} );
diff --git a/tests/unit/components/Match/SaveDiscardButtons.test.js b/tests/unit/components/Match/SaveDiscardButtons.test.js
new file mode 100644
index 000000000..510253c90
--- /dev/null
+++ b/tests/unit/components/Match/SaveDiscardButtons.test.js
@@ -0,0 +1,30 @@
+import { fireEvent, screen } from "@testing-library/react-native";
+import SaveDiscardButtons from "components/Match/SaveDiscardButtons";
+import React from "react";
+import { renderComponent } from "tests/helpers/render";
+
+describe( "SaveDiscardButtons", () => {
+ it( "calls handlePress with 'save'", () => {
+ const mockHandlePress = jest.fn();
+
+ renderComponent( );
+
+ const saveButton = screen.getByText( "SAVE" );
+ expect( saveButton ).toBeVisible();
+ fireEvent.press( saveButton );
+
+ expect( mockHandlePress ).toHaveBeenCalledWith( "save" );
+ } );
+
+ it( "calls handlePress with 'discard'", () => {
+ const mockHandlePress = jest.fn();
+
+ renderComponent( );
+
+ const discardButton = screen.getByText( "DISCARD" );
+ expect( discardButton ).toBeVisible();
+ fireEvent.press( discardButton );
+
+ expect( mockHandlePress ).toHaveBeenCalledWith( "discard" );
+ } );
+} );
diff --git a/tests/unit/components/Match/helpers/tryToReplaceWithLocalTaxon.test.js b/tests/unit/components/Match/helpers/tryToReplaceWithLocalTaxon.test.js
new file mode 100644
index 000000000..bb173c32a
--- /dev/null
+++ b/tests/unit/components/Match/helpers/tryToReplaceWithLocalTaxon.test.js
@@ -0,0 +1,58 @@
+import tryToReplaceWithLocalTaxon from "components/Match/helpers/tryToReplaceWithLocalTaxon";
+import factory from "tests/factory";
+
+describe( "tryToReplaceWithLocalTaxon", () => {
+ it( "should return original suggestion when local taxa array is empty", () => {
+ const localTaxa = [];
+ const suggestion = {
+ combined_score: 92,
+ taxon: factory( "RemoteTaxon", { id: 745 } )
+ };
+
+ const result = tryToReplaceWithLocalTaxon( localTaxa, suggestion );
+
+ expect( result ).toEqual( suggestion );
+ } );
+
+ it( "should return original suggestion when no matching local taxon is found", () => {
+ const localTaxa = [
+ factory( "LocalTaxon", { id: 746, name: "Silphium laciniatum" } ),
+ factory( "LocalTaxon", { id: 747, name: "Silphium integrifolium" } )
+ ];
+ const suggestion = {
+ combined_score: 88,
+ taxon: factory( "RemoteTaxon", { id: 745, name: "Silphium perfoliatum" } )
+ };
+
+ const result = tryToReplaceWithLocalTaxon( localTaxa, suggestion );
+
+ expect( result ).toEqual( suggestion );
+ } );
+
+ it( "should merge local taxon data when matching taxon is found", () => {
+ const localTaxon = factory( "LocalTaxon", {
+ id: 745,
+ name: "Silphium perfoliatum",
+ preferred_common_name: "Cup Plant",
+ rank: "species",
+ rank_level: 10,
+ _synced_at: new Date( "2024-01-15" ),
+ representative_photo: [{ photo: { id: 678 } }]
+ } );
+ const localTaxa = [localTaxon];
+ const suggestion = {
+ combined_score: 95,
+ taxon: {
+ id: 745,
+ name: "Silphium perfoliatum",
+ taxon_photos: [{ photo: { id: 123 } }],
+ iconic_taxon_name: "Plantae",
+ representative_photo: [{ photo: { id: 456 } }]
+ }
+ };
+
+ const result = tryToReplaceWithLocalTaxon( localTaxa, suggestion );
+
+ expect( result ).toEqual( { ...suggestion, taxon: { ...suggestion.taxon, ...localTaxon } } );
+ } );
+} );