From 7872b619b26485cc2af84cd49de968653ea40db7 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Tue, 11 Feb 2025 01:53:40 +0200 Subject: [PATCH] [MM-62898] Reset filter when changing team (#8564) (#8577) * refactor: Improve search team change handling and filter removal * fix: revert package.resolved * add test file * extract regex and fix formatting of useCallback * improve tests * fix style (cherry picked from commit d0073617ed110ef96f0d06d1b58e7aedbe6b78a5) Co-authored-by: Julien Tant <785518+JulienTant@users.noreply.github.com> --- app/screens/home/search/search.test.tsx | 168 ++++++++++++++++++++++++ app/screens/home/search/search.tsx | 17 ++- 2 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 app/screens/home/search/search.test.tsx diff --git a/app/screens/home/search/search.test.tsx b/app/screens/home/search/search.test.tsx new file mode 100644 index 000000000..2a2c38039 --- /dev/null +++ b/app/screens/home/search/search.test.tsx @@ -0,0 +1,168 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {fireEvent, waitFor} from '@testing-library/react-native'; +import React from 'react'; + +import {addSearchToTeamSearchHistory} from '@actions/local/team'; +import {searchPosts, searchFiles} from '@actions/remote/search'; +import {bottomSheet} from '@screens/navigation'; +import {renderWithEverything} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import SearchScreen from './search'; + +import type {TeamModel} from '@database/models/server'; +import type {Database} from '@nozbe/watermelondb'; + +// Some subcomponents require react-native-camera-roll, which is not available in the test environment +jest.mock('@react-native-camera-roll/camera-roll', () => ({})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + getState: () => ({ + index: 0, + routes: [{params: {searchTerm: ''}}], + }), + }), + useIsFocused: () => true, +})); + +jest.mock('@actions/local/post', () => ({ + getPosts: jest.fn().mockResolvedValue([]), +})); + +jest.mock('@actions/local/team', () => ({ + addSearchToTeamSearchHistory: jest.fn(), +})); + +jest.mock('@actions/remote/search', () => ({ + searchPosts: jest.fn().mockResolvedValue({order: [], matches: {}}), + searchFiles: jest.fn().mockResolvedValue({files: [], channels: []}), +})); + +jest.mock('@mattermost/hardware-keyboard', () => ({ + useHardwareKeyboardEvents: jest.fn(), +})); + +jest.mock('@screens/navigation', () => ({ + bottomSheet: jest.fn(), +})); + +describe('SearchScreen', () => { + const baseProps = { + teamId: 'team1', + teams: [ + {id: 'team1', displayName: 'Team 1'}, + {id: 'team2', displayName: 'Team 2'}, + ] as TeamModel[], + crossTeamSearchEnabled: true, + }; + + let database: Database; + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + database = server.database; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders search screen correctly', () => { + const {getByTestId, getByText, getByPlaceholderText} = renderWithEverything( + , + {database}, + ); + expect(getByTestId('search_messages.screen')).toBeTruthy(); + + // The page title + expect(getByText('Search')).toBeTruthy(); + + // The search input with the expected placeholder + expect(getByPlaceholderText('Search messages & files')).toBeTruthy(); + }); + + it('handles search input changes', () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + + const searchInput = getByTestId('navigation.header.search_bar.search.input'); + fireEvent.changeText(searchInput, 'test search'); + expect(searchInput.props.value).toBe('test search'); + }); + + it('performs search when submitting', async () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + + const searchInput = getByTestId('navigation.header.search_bar.search.input'); + fireEvent.changeText(searchInput, 'test search'); + fireEvent(searchInput, 'submitEditing'); + + await waitFor(() => { + expect(searchPosts).toHaveBeenCalledWith( + expect.any(String), + 'team1', + expect.objectContaining({terms: 'test search'}), + ); + expect(searchFiles).toHaveBeenCalledWith( + expect.any(String), + 'team1', + expect.objectContaining({terms: 'test search'}), + ); + }); + }); + + it('handles team changes', async () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + + const teamPicker = getByTestId('team_picker.button'); + fireEvent.press(teamPicker); + + expect(teamPicker).toBeTruthy(); + expect(bottomSheet).toHaveBeenCalled(); + }); + + it('clears search when clear button is pressed', async () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + + const searchInput = getByTestId('navigation.header.search_bar.search.input'); + fireEvent.changeText(searchInput, 'test search'); + + const clearButton = getByTestId('navigation.header.search_bar.search.clear.button'); + fireEvent.press(clearButton); + + expect(searchInput.props.value).toBe(''); + }); + + it('adds search to team history when searching in a specific team', async () => { + const {getByTestId} = renderWithEverything( + , + {database}, + ); + + const searchInput = getByTestId('navigation.header.search_bar.search.input'); + fireEvent.changeText(searchInput, 'test search'); + fireEvent(searchInput, 'submitEditing'); + + await waitFor(() => { + expect(addSearchToTeamSearchHistory).toHaveBeenCalledWith( + expect.any(String), + 'team1', + 'test search', + ); + }); + }); +}); diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index 6b9096255..d58bfc5d9 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -79,6 +79,8 @@ const getSearchParams = (terms: string, filterValue?: FileFilter) => { const searchScreenIndex = 1; +const CHANNEL_AND_USER_FILTERS_REGEX = /(?:from|channel|in):\s?[^\s\n]+/gi; + const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { const nav = useNavigation(); const isFocused = useIsFocused(); @@ -238,9 +240,20 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { setResultsLoading(false); }, [lastSearchedValue, searchTeamId, serverUrl]); + const removeChannelAndUserFiltersFromString = (str: string) => { + return str.replace(CHANNEL_AND_USER_FILTERS_REGEX, '').trim(); + }; + + const updateSearchTeamId = useCallback((newTeamId: string) => { + setSearchTeamId(newTeamId); + setSearchValue(removeChannelAndUserFiltersFromString(searchValue)); + }, [searchValue]); + const handleResultsTeamChange = useCallback((newTeamId: string) => { setSearchTeamId(newTeamId); - handleSearch(newTeamId, lastSearchedValue); + const cleanedSearchValue = removeChannelAndUserFiltersFromString(lastSearchedValue); + setSearchValue(cleanedSearchValue); + handleSearch(newTeamId, cleanedSearchValue); }, [lastSearchedValue, handleSearch]); const initialContainerStyle: AnimatedStyle = useMemo(() => { @@ -265,7 +278,7 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { setRecentValue={handleRecentSearch} searchRef={searchRef} setSearchValue={handleModifierTextChange} - setTeamId={setSearchTeamId} + setTeamId={updateSearchTeamId} teamId={searchTeamId} teams={teams} />