From 3ed7e9165f0a9667325e3c68241df5f12f8bf8fd Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Fri, 22 Nov 2024 13:37:48 -0800 Subject: [PATCH] fix(SILVA-511): fixing opening map popup (#498) --- .../oracle/dto/OpeningSearchFiltersDto.java | 24 --- .../oracle/repository/OpeningRepository.java | 64 +++++++- .../oracle/service/OpeningService.java | 5 + .../endpoint/OpeningFavoriteEndpoint.java | 5 + .../service/UserRecentOpeningService.java | 145 +++++++++--------- ...peningFavoriteEndpointIntegrationTest.java | 34 +++- .../service/UserRecentOpeningServiceTest.java | 5 +- .../components/OpeningsMapEntryPopup.test.tsx | 101 ++++++++++++ .../src/components/FavoriteButton/index.tsx | 7 +- .../src/components/OpeningsMapEntry/index.tsx | 6 +- .../OpeningsMapEntryPopup/index.tsx | 66 ++++++++ .../src/services/OpeningFavouriteService.ts | 38 ++--- frontend/src/services/apiConfig.ts | 26 ++++ frontend/src/services/search/openings.ts | 26 +--- frontend/src/types/OpeningPolygon.ts | 2 +- 15 files changed, 392 insertions(+), 162 deletions(-) create mode 100644 frontend/src/__test__/components/OpeningsMapEntryPopup.test.tsx create mode 100644 frontend/src/components/OpeningsMapEntryPopup/index.tsx create mode 100644 frontend/src/services/apiConfig.ts diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java index d710a60b..e3d20618 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java @@ -35,7 +35,6 @@ public class OpeningSearchFiltersDto { @Setter private String requestUserId; - private List openingIds; /** Creates an instance of the search opening filter dto. */ public OpeningSearchFiltersDto( @@ -59,7 +58,6 @@ public OpeningSearchFiltersDto( this.orgUnit = !Objects.isNull(orgUnit) ? orgUnit : null; this.category = !Objects.isNull(category) ? category : null; this.statusList = !Objects.isNull(statusList) ? statusList : null; - this.openingIds = null; this.myOpenings = myOpenings; this.submittedToFrpa = BooleanUtils @@ -88,27 +86,6 @@ public OpeningSearchFiltersDto( Objects.isNull(mainSearchTerm) ? null : mainSearchTerm.toUpperCase().trim(); } - // Create a constructor with only the List openingIds - public OpeningSearchFiltersDto(List openingIds) { - this.orgUnit = null; - this.category = null; - this.statusList = null; - this.openingIds = openingIds; - this.myOpenings = null; - this.submittedToFrpa = "NO"; - this.disturbanceDateStart = null; - this.disturbanceDateEnd = null; - this.regenDelayDateStart = null; - this.regenDelayDateEnd = null; - this.freeGrowingDateStart = null; - this.freeGrowingDateEnd = null; - this.updateDateStart = null; - this.updateDateEnd = null; - this.cuttingPermitId = null; - this.cutBlockId = null; - this.timberMark = null; - this.mainSearchTerm = null; - } /** * Define if a property has value. * @@ -120,7 +97,6 @@ public boolean hasValue(String prop) { case SilvaOracleConstants.ORG_UNIT -> !Objects.isNull(this.orgUnit) && !this.orgUnit.isEmpty(); case SilvaOracleConstants.CATEGORY -> !Objects.isNull(this.category) && !this.category.isEmpty(); case SilvaOracleConstants.STATUS_LIST -> !Objects.isNull(this.statusList) && !this.statusList.isEmpty(); - case SilvaOracleConstants.OPENING_IDS -> !Objects.isNull(this.openingIds) && !this.openingIds.isEmpty(); case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings); case SilvaOracleConstants.SUBMITTED_TO_FRPA -> !Objects.isNull(this.submittedToFrpa); case SilvaOracleConstants.DISTURBANCE_DATE_START -> diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRepository.java index 369519ee..0b2be95b 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRepository.java @@ -206,9 +206,6 @@ LEFT JOIN THE.STOCKING_MILESTONE smfg ON (smfg.STOCKING_STANDARD_UNIT_ID = ssu.S ) AND ( NVL(:#{#filter.timberMark},'NOVALUE') = 'NOVALUE' OR TIMBER_MARK = :#{#filter.timberMark} - ) - AND ( - NVL(:#{#filter.openingIds},'NOVALUE') = 'NOVALUE' OR OPENING_ID IN (:#{#filter.openingIds}) )""", countQuery = """ SELECT count(o.OPENING_ID) as total @@ -279,9 +276,6 @@ LEFT JOIN THE.STOCKING_MILESTONE smfg ON (smfg.STOCKING_STANDARD_UNIT_ID = ssu.S ) AND ( NVL(:#{#filter.timberMark},'NOVALUE') = 'NOVALUE' OR cboa.TIMBER_MARK = :#{#filter.timberMark} - ) - AND ( - NVL(:#{#filter.openingIds},'NOVALUE') = 'NOVALUE' OR o.OPENING_ID IN (:#{#filter.openingIds}) )""", nativeQuery = true ) @@ -289,4 +283,62 @@ Page searchBy( OpeningSearchFiltersDto filter, Pageable pageable ); + + @Query( + value = """ + SELECT opening_id, opening_number, category, status, cutting_permit_id, timber_mark,\s + cut_block_id, opening_gross_area, disturbance_start_date, forest_file_id,\s + org_unit_code, org_unit_name, client_number, client_location, regen_delay_date,\s + early_free_growing_date, late_free_growing_date, update_timestamp, entry_user_id,\s + submitted_to_frpa108 + FROM ( + SELECT + o.OPENING_ID AS opening_id, + o.OPENING_NUMBER AS opening_number, + o.OPEN_CATEGORY_CODE AS category, + o.OPENING_STATUS_CODE AS status, + cboa.CUTTING_PERMIT_ID AS cutting_permit_id, + cboa.TIMBER_MARK AS timber_mark, + cboa.CUT_BLOCK_ID AS cut_block_id, + cboa.OPENING_GROSS_AREA AS opening_gross_area, + cboa.DISTURBANCE_START_DATE AS disturbance_start_date, + cboa.FOREST_FILE_ID AS forest_file_id, + ou.ORG_UNIT_CODE AS org_unit_code, + ou.ORG_UNIT_NAME AS org_unit_name, + res.CLIENT_NUMBER AS client_number, + res.CLIENT_LOCN_CODE AS client_location, + ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMRG.LATE_OFFSET_YEARS, 0) * 12)) AS regen_delay_date, + ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.EARLY_OFFSET_YEARS, 0) * 12)) AS early_free_growing_date, + ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.LATE_OFFSET_YEARS, 0) * 12)) AS late_free_growing_date, + o.UPDATE_TIMESTAMP AS update_timestamp, + o.ENTRY_USERID AS entry_user_id, + COALESCE(sra.SILV_RELIEF_APPLICATION_ID, 0) AS submitted_to_frpa108, + ROW_NUMBER() OVER (PARTITION BY o.OPENING_ID ORDER BY o.UPDATE_TIMESTAMP DESC) AS rn + FROM THE.OPENING o + LEFT JOIN THE.CUT_BLOCK_OPEN_ADMIN cboa ON (cboa.OPENING_ID = o.OPENING_ID) + LEFT JOIN THE.ORG_UNIT ou ON (ou.ORG_UNIT_NO = o.ADMIN_DISTRICT_NO) + LEFT JOIN THE.RESULTS_ELECTRONIC_SUBMISSION res ON (res.RESULTS_SUBMISSION_ID = o.RESULTS_SUBMISSION_ID) + LEFT JOIN THE.SILV_RELIEF_APPLICATION sra ON (sra.ACTIVITY_TREATMENT_UNIT_ID = o.OPENING_ID AND sra.SILV_RELIEF_APPL_STATUS_CODE = 'APP') + LEFT JOIN THE.STOCKING_STANDARD_UNIT ssu ON (ssu.OPENING_ID = o.OPENING_ID) + LEFT JOIN THE.STOCKING_MILESTONE smrg ON (smrg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID AND SMRG.SILV_MILESTONE_TYPE_CODE = 'RG') + LEFT JOIN THE.STOCKING_MILESTONE smfg ON (smfg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID AND smfg.SILV_MILESTONE_TYPE_CODE = 'FG') + ) + WHERE rn = 1 AND OPENING_ID IN :openingIds""", + countQuery = """ + SELECT count(o.OPENING_ID) as total + FROM THE.OPENING o + LEFT JOIN THE.CUT_BLOCK_OPEN_ADMIN cboa ON (cboa.OPENING_ID = o.OPENING_ID) + LEFT JOIN THE.ORG_UNIT ou ON (ou.ORG_UNIT_NO = o.ADMIN_DISTRICT_NO) + LEFT JOIN THE.RESULTS_ELECTRONIC_SUBMISSION res ON (res.RESULTS_SUBMISSION_ID = o.RESULTS_SUBMISSION_ID) + LEFT JOIN THE.SILV_RELIEF_APPLICATION sra ON (sra.ACTIVITY_TREATMENT_UNIT_ID = o.OPENING_ID AND sra.SILV_RELIEF_APPL_STATUS_CODE = 'APP') + LEFT JOIN THE.STOCKING_STANDARD_UNIT ssu ON (ssu.OPENING_ID = o.OPENING_ID) + LEFT JOIN THE.STOCKING_MILESTONE smrg ON (smrg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID AND SMRG.SILV_MILESTONE_TYPE_CODE = 'RG') + LEFT JOIN THE.STOCKING_MILESTONE smfg ON (smfg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID AND smfg.SILV_MILESTONE_TYPE_CODE = 'FG') + WHERE o.OPENING_ID IN :openingIds""", + nativeQuery = true + ) + Page searchByOpeningIds( + List openingIds, + Pageable pageable + ); } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java index 2c630eac..b8c3d71f 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java @@ -133,6 +133,11 @@ public PaginatedResult openingSearch( pagination.toPageable(Sort.by("opening_id").descending()) ); + return parsePageResult(pagination, searchResultPage); + } + + public PaginatedResult parsePageResult( + PaginationParameters pagination, Page searchResultPage) { PaginatedResult result = new PaginatedResult<>(); result.setTotalItems(searchResultPage.getTotalElements()); result.setPageIndex(pagination.page()); diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java index 1cd022db..efd5d0dd 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java @@ -25,6 +25,11 @@ public List getFavorites() { return userOpeningService.listUserFavoriteOpenings(); } + @GetMapping("/{id}") + public boolean checkFavorite(@PathVariable Long id) { + return !userOpeningService.checkForFavorites(List.of(id)).isEmpty(); + } + @PutMapping("/{id}") @ResponseStatus(HttpStatus.ACCEPTED) public void addToFavorites(@PathVariable Long id) { diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java index f286f497..ba7d26dc 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java @@ -4,7 +4,6 @@ import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; import ca.bc.gov.restapi.results.common.security.LoggedUserService; -import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchFiltersDto; import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; import ca.bc.gov.restapi.results.oracle.repository.OpeningRepository; import ca.bc.gov.restapi.results.oracle.service.OpeningService; @@ -30,87 +29,91 @@ @RequiredArgsConstructor public class UserRecentOpeningService { - private final LoggedUserService loggedUserService; - private final UserRecentOpeningRepository userRecentOpeningRepository; - private final OpeningService openingService; - private final OpeningRepository openingRepository; + private final LoggedUserService loggedUserService; + private final UserRecentOpeningRepository userRecentOpeningRepository; + private final OpeningService openingService; + private final OpeningRepository openingRepository; - @Transactional - public UserRecentOpeningDto storeViewedOpening(Long openingId) { - log.info("Adding opening ID {} as recently viewed for user {}", openingId, - loggedUserService.getLoggedUserId()); + @Transactional + public UserRecentOpeningDto storeViewedOpening(Long openingId) { + log.info("Adding opening ID {} as recently viewed for user {}", openingId, + loggedUserService.getLoggedUserId()); - if(openingId == null) { - log.info("Opening ID is null"); - throw new IllegalArgumentException("Opening ID must contain numbers only!"); - } + if (openingId == null) { + log.info("Opening ID is null"); + throw new IllegalArgumentException("Opening ID must contain numbers only!"); + } - if (!openingRepository.existsById(openingId)) { - log.info("Opening ID not found: {}", openingId); - throw new OpeningNotFoundException(); - } + if (!openingRepository.existsById(openingId)) { + log.info("Opening ID not found: {}", openingId); + throw new OpeningNotFoundException(); + } - LocalDateTime lastViewed = LocalDateTime.now(); + LocalDateTime lastViewed = LocalDateTime.now(); - userRecentOpeningRepository.saveAndFlush( + userRecentOpeningRepository.saveAndFlush( userRecentOpeningRepository .findByUserIdAndOpeningId(loggedUserService.getLoggedUserId(), openingId) .map(entity -> entity.withLastViewed(lastViewed)) .orElse( - new UserRecentOpeningEntity(null,loggedUserService.getLoggedUserId(),openingId,lastViewed) + new UserRecentOpeningEntity(null, loggedUserService.getLoggedUserId(), openingId, + lastViewed) ) + ); + + // Return the DTO + return new UserRecentOpeningDto( + loggedUserService.getLoggedUserId(), + openingId, + lastViewed + ); + } + + /** + * Retrieves the recent openings viewed by the logged-in user, limited by the provided limit. + * + * @param limit The maximum number of recent openings to retrieve. + * @return A list of opening IDs the user has viewed, sorted by last viewed in descending order. + */ + public PaginatedResult getAllRecentOpeningsForUser(int limit) { + String userId = loggedUserService.getLoggedUserId(); + Pageable pageable = PageRequest.of(0, limit); // PageRequest object to apply limit + + // Fetch recent openings for the user + Page recentOpenings = userRecentOpeningRepository + .findByUserIdOrderByLastViewedDesc(userId, pageable); + + // Extract opening IDs as String + Map openingIds = recentOpenings.getContent().stream() + .collect(Collectors.toMap(UserRecentOpeningEntity::getOpeningId, + UserRecentOpeningEntity::getLastViewed)); + log.info("User with the userId {} has the following openingIds {}", userId, openingIds); + + if (openingIds.isEmpty()) { + // Ensure an empty data list instead of null + return new PaginatedResult().withData(Collections.emptyList()); + } + + PaginatedResult pageResult = + openingService.parsePageResult( + new PaginationParameters(0, 10), + openingRepository + .searchByOpeningIds(new ArrayList<>(openingIds.keySet()), + PageRequest.of(0, 10) + ) ); - - // Return the DTO - return new UserRecentOpeningDto( - loggedUserService.getLoggedUserId(), - openingId, - lastViewed + + return pageResult + .withData( + pageResult + .getData() + .stream() + .map(result -> result.withLastViewDate( + openingIds.get(result.getOpeningId().longValue()))) + .sorted(Comparator.comparing(OpeningSearchResponseDto::getLastViewDate).reversed()) + .toList() ); - } + } + - /** - * Retrieves the recent openings viewed by the logged-in user, limited by the provided limit. - * - * @param limit The maximum number of recent openings to retrieve. - * @return A list of opening IDs the user has viewed, sorted by last viewed in descending order. - */ - public PaginatedResult getAllRecentOpeningsForUser(int limit) { - String userId = loggedUserService.getLoggedUserId(); - Pageable pageable = PageRequest.of(0, limit); // PageRequest object to apply limit - - // Fetch recent openings for the user - Page recentOpenings = userRecentOpeningRepository - .findByUserIdOrderByLastViewedDesc(userId, pageable); - - // Extract opening IDs as String - Map openingIds = recentOpenings.getContent().stream() - .collect(Collectors.toMap(UserRecentOpeningEntity::getOpeningId, UserRecentOpeningEntity::getLastViewed)); - log.info("User with the userId {} has the following openingIds {}", userId, openingIds); - - if (openingIds.isEmpty()) { - // Ensure an empty data list instead of null - return new PaginatedResult() - .withData(Collections.emptyList()); - } - - PaginatedResult pageResult = - openingService - .openingSearch( - new OpeningSearchFiltersDto(new ArrayList<>(openingIds.keySet())), - new PaginationParameters(0, 10) - ); - - return pageResult - .withData( - pageResult - .getData() - .stream() - .map(result -> result.withLastViewDate(openingIds.get(result.getOpeningId().longValue()))) - .sorted(Comparator.comparing(OpeningSearchResponseDto::getLastViewDate).reversed()) - .toList() - ); - } - - } diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java index ad50a528..df55ff4a 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java @@ -48,6 +48,13 @@ void shouldBeEmpty() throws Exception { @Test @Order(2) + @DisplayName("Should check if entry is favourite and get false") + void shouldCheckIfEntryIsFavouriteGetFails() throws Exception { + checkFavourite(101,false); + } + + @Test + @Order(3) @DisplayName("Should add to favorite") void shouldAddToFavorite() throws Exception { mockMvc @@ -65,7 +72,14 @@ void shouldAddToFavorite() throws Exception { } @Test - @Order(3) + @Order(4) + @DisplayName("Should check if entry is favourite") + void shouldCheckIfEntryIsFavourite() throws Exception { + checkFavourite(101,true); + } + + @Test + @Order(5) @DisplayName("Should not add to favorite if doesn't exist") void shouldNotAddIfDoesNotExist() throws Exception { mockMvc @@ -75,18 +89,17 @@ void shouldNotAddIfDoesNotExist() throws Exception { .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) .andExpect(content().string(StringUtils.EMPTY)); - //.andExpect(content().string("UserOpening record(s) not found!")); } @Test - @Order(4) + @Order(6) @DisplayName("Multiple requests to add to favorite should not fail, nor duplicate") void shouldAddToFavoriteAgain() throws Exception { shouldAddToFavorite(); } @Test - @Order(5) + @Order(7) @DisplayName("Should see list of favourites") void shouldBeAbleToSeeOpening() throws Exception { mockMvc @@ -99,7 +112,7 @@ void shouldBeAbleToSeeOpening() throws Exception { } @Test - @Order(6) + @Order(8) @DisplayName("Should remove from favorite") void shouldRemoveFromFavorites() throws Exception { mockMvc @@ -116,7 +129,7 @@ void shouldRemoveFromFavorites() throws Exception { } @Test - @Order(7) + @Order(9) @DisplayName("Should thrown an error if trying to remove entry that doesn't exist") void shouldThrownErrorIfNoFavoriteFound() throws Exception { mockMvc @@ -129,5 +142,14 @@ void shouldThrownErrorIfNoFavoriteFound() throws Exception { } + private void checkFavourite(long id, boolean expected) throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get("/api/openings/favourites/{id}", id) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(expected+"")); + } + } \ No newline at end of file diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java index a427faec..1d34830f 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java @@ -14,7 +14,6 @@ import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; import ca.bc.gov.restapi.results.common.security.LoggedUserService; -import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchFiltersDto; import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; import ca.bc.gov.restapi.results.oracle.repository.OpeningRepository; import ca.bc.gov.restapi.results.oracle.service.OpeningService; @@ -167,7 +166,9 @@ void getAllRecentOpeningsForUser_withRecentOpenings_returnsSortedResult() { PaginatedResult pageResult = new PaginatedResult<>(); pageResult.setData(List.of(dto1, dto2)); - when(openingService.openingSearch(any(OpeningSearchFiltersDto.class), any(PaginationParameters.class))) + when(openingRepository.searchByOpeningIds(any(List.class), any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of(dto2, dto1))); + when(openingService.parsePageResult(any(PaginationParameters.class), any(Page.class))) .thenReturn(pageResult); PaginatedResult result = userRecentOpeningService.getAllRecentOpeningsForUser(limit); diff --git a/frontend/src/__test__/components/OpeningsMapEntryPopup.test.tsx b/frontend/src/__test__/components/OpeningsMapEntryPopup.test.tsx new file mode 100644 index 00000000..4a179dbe --- /dev/null +++ b/frontend/src/__test__/components/OpeningsMapEntryPopup.test.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { vi } from 'vitest'; +import OpeningsMapEntryPopup from '../../components/OpeningsMapEntryPopup'; +import { isOpeningFavourite, setOpeningFavorite, deleteOpeningFavorite } from '../../services/OpeningFavouriteService'; +import { useNotification } from '../../contexts/NotificationProvider'; + +// Mock services and context +vi.mock('../../services/OpeningFavouriteService', () => ({ + isOpeningFavourite: vi.fn(), + setOpeningFavorite: vi.fn(), + deleteOpeningFavorite: vi.fn(), +})); + +vi.mock('../../contexts/NotificationProvider', () => ({ + useNotification: vi.fn(), +})); + +describe('OpeningsMapEntryPopup', () => { + const mockDisplayNotification = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + (useNotification as vi.Mock).mockReturnValue({ displayNotification: mockDisplayNotification }); + }); + + it('renders correctly with given openingId', () => { + render(); + expect(screen.getByText('Opening ID')).toBeInTheDocument(); + expect(screen.getByText('123')).toBeInTheDocument(); + }); + + it('fetches the favorite status on mount', async () => { + (isOpeningFavourite as vi.Mock).mockResolvedValue(true); + await act(async () => render()); + await waitFor(() => { expect(isOpeningFavourite).toHaveBeenCalledWith(123); }); + expect(screen.getByRole('button', { name: /favorite/i })).toHaveAttribute('aria-pressed', 'true'); + }); + + it('handles setting the opening as favorite', async () => { + (isOpeningFavourite as vi.Mock).mockResolvedValue(false); + (setOpeningFavorite as vi.Mock).mockResolvedValue(); + + await act(async () => render()); + + const favoriteButton = screen.getByRole('button', { name: /favorite/i }); + await act(async () => fireEvent.click(favoriteButton)); + + await waitFor(() => { + expect(setOpeningFavorite).toHaveBeenCalledWith(123); + }); + + expect(mockDisplayNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Opening Id 123 favourited', + type: 'success', + }), + ); + }); + + it('handles removing the opening from favorites', async () => { + (isOpeningFavourite as vi.Mock).mockResolvedValue(true); + (deleteOpeningFavorite as vi.Mock).mockResolvedValue(); + + await act(async () => render()); + + const favoriteButton = screen.getByRole('button', { name: /favorite/i }); + await act(async () => fireEvent.click(favoriteButton)); + + await waitFor(() => { + expect(deleteOpeningFavorite).toHaveBeenCalledWith(123); + }); + + expect(mockDisplayNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Opening Id 123 unfavourited', + type: 'success', + }), + ); + }); + + it('displays an error notification on failure', async () => { + (isOpeningFavourite as vi.Mock).mockResolvedValue(false); + (setOpeningFavorite as vi.Mock).mockRejectedValue(new Error('Failed to favorite')); + + await act(async () => render()); + + const favoriteButton = screen.getByRole('button', { name: /favorite/i }); + await act(async () => fireEvent.click(favoriteButton)); + + await waitFor(() => { + expect(mockDisplayNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Error', + subTitle: 'Failed to update favorite status for 123', + type: 'error', + }), + ); + }); + }); +}); diff --git a/frontend/src/components/FavoriteButton/index.tsx b/frontend/src/components/FavoriteButton/index.tsx index 8d8cb11a..d2870d6f 100644 --- a/frontend/src/components/FavoriteButton/index.tsx +++ b/frontend/src/components/FavoriteButton/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Button } from '@carbon/react'; import * as Icons from '@carbon/icons-react'; import './style.scss'; // Import the styles @@ -47,6 +47,10 @@ function FavoriteButton({ } const CustomIcon = () => ; + useEffect(() => { + setIsFavorite(favorited); + }, [favorited]); + return (