From ca6f91e3524971eb37139ba37737b44b61b4398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:45:00 +0100 Subject: [PATCH 01/12] fix: re align arrow --- src/ui/components/tiles/HorizontalTile.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/components/tiles/HorizontalTile.tsx b/src/ui/components/tiles/HorizontalTile.tsx index 0994efcf27b..d10d786e4ae 100644 --- a/src/ui/components/tiles/HorizontalTile.tsx +++ b/src/ui/components/tiles/HorizontalTile.tsx @@ -69,6 +69,7 @@ const Column = Flex const Row = styled(Flex).attrs({ flexDirection: 'row', + alignItems: 'center', })`` const Container = styled(Row).attrs({ From 8b6460726f2f90194520080c4d718d90dbd46e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:34:28 +0100 Subject: [PATCH 02/12] refactor: extract MovieScreenig in its own file and rename --- .../components/VenueOffers/VenueMovies.tsx | 50 ++++++++++++++++ .../components/VenueOffers/VenueOffers.tsx | 57 ++----------------- 2 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 src/features/venue/components/VenueOffers/VenueMovies.tsx diff --git a/src/features/venue/components/VenueOffers/VenueMovies.tsx b/src/features/venue/components/VenueOffers/VenueMovies.tsx new file mode 100644 index 00000000000..424a0ba3989 --- /dev/null +++ b/src/features/venue/components/VenueOffers/VenueMovies.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react' +import { InView } from 'react-native-intersection-observer' +import { useTheme } from 'styled-components' +import styled from 'styled-components/native' + +import { MoviesScreeningCalendar } from 'features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar' +import { useOfferCTA } from 'features/offer/components/OfferContent/OfferCTAProvider' +import type { VenueOffers } from 'features/venue/types' +import { Anchor } from 'ui/components/anchor/Anchor' +import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext' +import { Spacer, TypoDS, getSpacing } from 'ui/theme' +import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' + +const cinemaCTAButtonName = 'Accéder aux séances' + +export const VenueMovies: React.FC<{ venueOffers: VenueOffers }> = ({ venueOffers }) => { + const { isDesktopViewport } = useTheme() + const { setButton, showButton } = useOfferCTA() + const scrollToAnchor = useScrollToAnchor() + + useEffect(() => { + setButton(cinemaCTAButtonName, () => { + scrollToAnchor('venue-cine-availabilities') + }) + + return () => { + setButton('', () => null) + } + }, [scrollToAnchor, setButton]) + + return ( + + + + { + showButton(!inView) + }}> + {'Les films à l’affiche'} + + + + + + ) +} + +const MoviesTitle = styled(TypoDS.Title3).attrs(getHeadingAttrs(2))({ + marginLeft: getSpacing(6), +}) diff --git a/src/features/venue/components/VenueOffers/VenueOffers.tsx b/src/features/venue/components/VenueOffers/VenueOffers.tsx index cfb948cb953..24a7a638e61 100644 --- a/src/features/venue/components/VenueOffers/VenueOffers.tsx +++ b/src/features/venue/components/VenueOffers/VenueOffers.tsx @@ -1,34 +1,25 @@ -import React, { useEffect } from 'react' -import { InView } from 'react-native-intersection-observer' -import { useTheme } from 'styled-components' -import styled from 'styled-components/native' +import React from 'react' import { SubcategoryIdEnum, VenueResponse } from 'api/gen' import { useGTLPlaylists } from 'features/gtlPlaylist/hooks/useGTLPlaylists' import { GtlPlaylistData } from 'features/gtlPlaylist/types' -import { MoviesScreeningCalendar } from 'features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar' -import { useOfferCTA } from 'features/offer/components/OfferContent/OfferCTAProvider' import { useVenueOffers } from 'features/venue/api/useVenueOffers' import { NoOfferPlaceholder } from 'features/venue/components/Placeholders/NoOfferPlaceholder' +import { VenueMovies } from 'features/venue/components/VenueOffers/VenueMovies' import { VenueOffersList } from 'features/venue/components/VenueOffers/VenueOffersList' -import type { VenueOffersArtists, VenueOffers as VenueOffersType } from 'features/venue/types' +import type { VenueOffersArtists, VenueOffers } from 'features/venue/types' import { useFeatureFlag } from 'libs/firebase/firestore/featureFlags/useFeatureFlag' import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types' -import { Anchor } from 'ui/components/anchor/Anchor' -import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext' import { OfferPlaylistSkeleton, TileSize } from 'ui/components/placeholders/OfferPlaylistSkeleton' -import { Spacer, TypoDS, getSpacing } from 'ui/theme' -import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' +import { Spacer } from 'ui/theme' export interface VenueOffersProps { venue: VenueResponse venueArtists?: VenueOffersArtists - venueOffers?: VenueOffersType + venueOffers?: VenueOffers playlists?: GtlPlaylistData[] } -const cinemaCTAButtonName = 'Accéder aux séances' - const LoadingState: React.FC = () => ( @@ -36,38 +27,6 @@ const LoadingState: React.FC = () => ( ) -const MovieScreening: React.FC<{ venueOffers: VenueOffersType }> = ({ venueOffers }) => { - const { isDesktopViewport } = useTheme() - const { setButton, showButton } = useOfferCTA() - const scrollToAnchor = useScrollToAnchor() - - useEffect(() => { - setButton(cinemaCTAButtonName, () => { - scrollToAnchor('venue-cine-availabilities') - }) - - return () => { - setButton('', () => null) - } - }, [scrollToAnchor, setButton]) - - return ( - - - - { - showButton(!inView) - }}> - {'Les films à l’affiche'} - - - - - - ) -} - export function VenueOffers({ venue, venueArtists, @@ -93,7 +52,7 @@ export function VenueOffers({ } if (isOfferAMovieScreening && enableCine) { - return + return } return ( @@ -105,7 +64,3 @@ export function VenueOffers({ /> ) } - -const MoviesTitle = styled(TypoDS.Title3).attrs(getHeadingAttrs(2))({ - marginLeft: getSpacing(6), -}) From ee25e7c16351a00d44d069c0379e207ea9aa4830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:02:46 +0100 Subject: [PATCH 03/12] refactor: remove circular dependency --- .../MovieOfferTile.tsx | 11 ++++- .../moviesOffer.builder.ts | 42 +++++++++---------- .../MoviesScreeningCalendar/types.ts | 6 +++ 3 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 src/features/offer/components/MoviesScreeningCalendar/types.ts diff --git a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx index a4ed21add9e..85d1aca3bde 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx +++ b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx @@ -8,9 +8,10 @@ import { getMovieScreenings, } from 'features/offer/components/MovieScreeningCalendar/useMovieScreeningCalendar' import { useSelectedDateScreening } from 'features/offer/components/MovieScreeningCalendar/useSelectedDateScreenings' -import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { isDateNotWithinNextNbDays } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' import { NextScreeningButton } from 'features/offer/components/MoviesScreeningCalendar/NextScreeningButton' +import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/types' import { useOfferCTAButton } from 'features/offer/components/OfferCTAButton/useOfferCTAButton' import { formatDuration } from 'features/offer/helpers/formatDuration/formatDuration' import { VenueOffers } from 'features/venue/types' @@ -57,10 +58,12 @@ export const MovieOfferTile: FC = ({ () => selectedDateScreenings(offer.venue.id, onPressOfferCTA, movieScreeningUserData), [movieScreeningUserData, offer.venue.id, onPressOfferCTA, selectedDateScreenings] ) + const offerScreeningOnSelectedDates = useMemo( () => venueOffers.hits.find((item) => Number(item.objectID) === offer.id), [offer.id, venueOffers.hits] ) + return ( @@ -81,7 +84,11 @@ export const MovieOfferTile: FC = ({ goToDate(nextScreeningDate)} + onPress={ + isDateNotWithinNextNbDays(new Date(), nextScreeningDate, 15) + ? () => onPressOfferCTA() + : () => goToDate(nextScreeningDate) + } /> ) : ( diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts index 007399c5d8b..7d871e2a7cc 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts @@ -8,7 +8,7 @@ import { } from 'date-fns' import { OfferResponseV2, OfferStockResponse } from 'api/gen' -import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' +import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/types' import { GeoCoordinates } from 'libs/location' import { computeDistanceInMeters } from 'libs/parsers/formatDistance' @@ -22,7 +22,7 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => withoutScreeningsOnDay: (selectedDate: Date) => { movieOffers = movieOffers.filter( ({ offer }) => - !offer.stocks.some((stock) => { + !offer?.stocks?.some((stock) => { if (!stock.beginningDatetime) { return true } @@ -41,7 +41,7 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => ...rest, offer: { ...offer, - stocks: offer.stocks.filter((stock) => { + stocks: offer?.stocks?.filter((stock) => { if (!stock.beginningDatetime) { return false } @@ -57,22 +57,22 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => }), }, })) - .filter(({ offer }) => offer.stocks.length > 0) + .filter(({ offer }) => offer?.stocks?.length > 0) return builderObject }, sortedByLast30DaysBooking: () => { movieOffers = movieOffers.sort((a, b) => { - const aValue = a.offer.last30DaysBookings - const bValue = b.offer.last30DaysBookings + const aValue = a?.offer?.last30DaysBookings + const bValue = b?.offer?.last30DaysBookings if (aValue === null || aValue === undefined) return 1 if (bValue === null || bValue === undefined) return -1 if (bValue === aValue) { - const aEarliestDate = getEarliestDate(a.offer.stocks) - const bEarliestDate = getEarliestDate(b.offer.stocks) + const aEarliestDate = getEarliestDate(a?.offer?.stocks) + const bEarliestDate = getEarliestDate(b?.offer?.stocks) return aEarliestDate - bEarliestDate } @@ -86,34 +86,33 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => sortedByDistance: (location: GeoCoordinates) => { movieOffers = movieOffers.sort((a, b) => { const aDistance = computeDistanceInMeters( - a.offer.venue.coordinates.latitude ?? 0, - a.offer.venue.coordinates.longitude ?? 0, + a.offer?.venue?.coordinates?.latitude ?? 0, + a.offer?.venue?.coordinates?.longitude ?? 0, location?.latitude, location?.longitude ) const bDistance = computeDistanceInMeters( - b.offer.venue.coordinates.latitude ?? 0, - b.offer.venue.coordinates.longitude ?? 0, + b.offer?.venue?.coordinates?.latitude ?? 0, + b.offer?.venue?.coordinates?.longitude ?? 0, location?.latitude, location?.longitude ) if (aDistance === bDistance) { - const aEarliestDate = getEarliestDate(a.offer.stocks) - const bEarliestDate = getEarliestDate(b.offer.stocks) + const aEarliestDate = getEarliestDate(a?.offer?.stocks) + const bEarliestDate = getEarliestDate(b?.offer?.stocks) return aEarliestDate - bEarliestDate } return aDistance - bDistance }) - return builderObject }, withScreeningsAfterNbDays: (nbDays: number) => { movieOffers = movieOffers .map(({ offer }) => { - const filteredStocks = offer.stocks.filter((stock) => { + const filteredStocks = offer?.stocks?.filter((stock) => { if (!stock.beginningDatetime) { return false } @@ -121,21 +120,20 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => return isDateNotWithinNextNbDays(new Date(), new Date(stock.beginningDatetime), nbDays) }) return { - nextDate: new Date(filteredStocks[0]?.beginningDatetime as string), + nextDate: new Date(filteredStocks?.[0]?.beginningDatetime as string), offer: { ...offer, stocks: filteredStocks, }, } }) - .filter(({ offer }) => offer.stocks.length > 0) - + .filter(({ offer }) => offer?.stocks?.length > 0) return builderObject }, withoutScreeningsAfterNbDays: (nbDays: number) => { movieOffers = movieOffers.filter(({ offer }) => - offer.stocks.some((stock) => { + offer?.stocks?.some((stock) => { if (!stock.beginningDatetime) { return false } @@ -198,7 +196,7 @@ const isDateBeforeToday = (referenceDate: Date, targetDate: Date) => { } const getNextDate = (offer: OfferResponseV2, date: Date) => { - const dates = offer.stocks + const dates = offer?.stocks .filter((stock) => stock.beginningDatetime) .map((stock) => new Date(stock.beginningDatetime as string)) @@ -234,7 +232,7 @@ const getUpcomingDate = (offer: OfferResponseV2) => { } const getEarliestDate = (stocks: OfferStockResponse[]) => { - return stocks.reduce((earliest, stock) => { + return stocks?.reduce((earliest, stock) => { if (!stock.beginningDatetime) return earliest const stockDate = new Date(stock.beginningDatetime).getTime() return stockDate < earliest ? stockDate : earliest diff --git a/src/features/offer/components/MoviesScreeningCalendar/types.ts b/src/features/offer/components/MoviesScreeningCalendar/types.ts new file mode 100644 index 00000000000..df7c9bfb790 --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/types.ts @@ -0,0 +1,6 @@ +import { OfferResponseV2 } from 'api/gen' + +export type MovieOffer = { + nextDate?: Date + offer: OfferResponseV2 +} From f3b1467ec5c8a2d86dad6cdae42ea790c1790166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:06:03 +0100 Subject: [PATCH 04/12] refactor: add missing property to type MovieOffer --- .../components/MoviesScreeningCalendar/moviesOffer.builder.ts | 1 + src/features/offer/components/MoviesScreeningCalendar/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts index 7d871e2a7cc..c827ba53b87 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts @@ -125,6 +125,7 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => ...offer, stocks: filteredStocks, }, + isUpcoming: false, } }) .filter(({ offer }) => offer?.stocks?.length > 0) diff --git a/src/features/offer/components/MoviesScreeningCalendar/types.ts b/src/features/offer/components/MoviesScreeningCalendar/types.ts index df7c9bfb790..1acb3afb50a 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/types.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/types.ts @@ -3,4 +3,5 @@ import { OfferResponseV2 } from 'api/gen' export type MovieOffer = { nextDate?: Date offer: OfferResponseV2 + isUpcoming: boolean } From 2b9330d2668d696c8198db2a218939a9a97d1a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 02:16:00 +0100 Subject: [PATCH 05/12] feat: handle logic --- .../hook/getVenueMovieOffers.native.test.ts | 175 ++++++++++++++++++ .../hook/getVenueMovieOffers.ts | 41 ++++ .../moviesOffer.builder.test.ts | 54 ++++++ .../moviesOffer.builder.ts | 16 +- 4 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts create mode 100644 src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.ts diff --git a/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts new file mode 100644 index 00000000000..1aa20e25605 --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts @@ -0,0 +1,175 @@ +import mockdate from 'mockdate' + +import { OffersStocksResponseV2 } from 'api/gen' +import { mockBuilder } from 'tests/mockBuilder' +import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' +import { renderHook } from 'tests/utils' + +import { getVenueMovieOffers } from './getVenueMovieOffers' + +const TODAY = new Date('2023-05-10') +const BEFORE_SELECTED_DATE = '2023-05-07T00:00:00.000Z' +const SELECTED_DATE = '2023-05-15T00:00:00.000Z' +const AFTER_SELECTED_DATE = '2023-05-20T00:00:00.000Z' +const AT_LEAST_15_DAYS_AFTER_SELECTED_DATE = '2023-06-20T00:00:00.000Z' + +describe('useVenueMovieOffers', () => { + beforeEach(() => { + mockdate.set(TODAY) + }) + + it('should sort offers by descending last30DaysBooking order', async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + last30DaysBookings: 400, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + }), + mockBuilder.offerResponseV2({ + id: 2, + last30DaysBookings: 200, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + }), + mockBuilder.offerResponseV2({ + id: 3, + last30DaysBookings: 300, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + }), + ], + } + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toEqual([ + expect.objectContaining({ + offer: expect.objectContaining({ last30DaysBookings: 400 }), + }), + expect.objectContaining({ + offer: expect.objectContaining({ last30DaysBookings: 300 }), + }), + expect.objectContaining({ + offer: expect.objectContaining({ last30DaysBookings: 200 }), + }), + ]) + }) + + it('should return movie offers of the selected date', async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + }), + mockBuilder.offerResponseV2({ + id: 2, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: BEFORE_SELECTED_DATE })], + }), + ], + } + + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toHaveLength(1) + expect(result.current.venueMovieOffers[0]?.offer.id).toBe(1) + }) + + it('should return next movie offers of a given date', async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: AFTER_SELECTED_DATE, + }), + ], + }), + mockBuilder.offerResponseV2({ + id: 2, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: BEFORE_SELECTED_DATE })], + }), + ], + } + + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toHaveLength(1) + expect(result.current.venueMovieOffers[0]?.offer.id).toBe(1) + }) + + it('should return empty array when there are no movies on a given day or after', async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: BEFORE_SELECTED_DATE })], + }), + ], + } + + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toHaveLength(0) + }) + + it('should not return movies with screenings after 15 days when other movies have screenings within 15 days', async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: SELECTED_DATE, + }), + ], + }), + mockBuilder.offerResponseV2({ + id: 2, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: AT_LEAST_15_DAYS_AFTER_SELECTED_DATE, + }), + ], + }), + ], + } + + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toHaveLength(1) + expect(result.current.venueMovieOffers[0]?.offer.id).toBe(1) + }) + + it("should only return movie with screenings after 15 days when other movies don't have screenings within 15 days", async () => { + const offers = { + offers: [ + mockBuilder.offerResponseV2({ + id: 1, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: BEFORE_SELECTED_DATE, + }), + ], + }), + mockBuilder.offerResponseV2({ + id: 2, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: AT_LEAST_15_DAYS_AFTER_SELECTED_DATE, + }), + ], + }), + ], + } + + const { result } = renderUseVenueMovieOffers(new Date(SELECTED_DATE), offers) + + expect(result.current.venueMovieOffers).toHaveLength(1) + expect(result.current.venueMovieOffers[0]?.offer.id).toBe(2) + }) +}) + +const renderUseVenueMovieOffers = (selectedDate: Date, offers: OffersStocksResponseV2) => + renderHook(() => getVenueMovieOffers(selectedDate, offers), { + wrapper: ({ children }) => reactQueryProviderHOC(children), + }) diff --git a/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.ts b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.ts new file mode 100644 index 00000000000..c1a3ebf53f3 --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.ts @@ -0,0 +1,41 @@ +import { OffersStocksResponseV2, SubcategoryIdEnum } from 'api/gen' +import { moviesOfferBuilder } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' + +export const getVenueMovieOffers = ( + selectedDate: Date, + offersWithStocks: OffersStocksResponseV2 | undefined +) => { + const movieOffers = offersWithStocks?.offers.filter( + (offer) => offer.subcategoryId === SubcategoryIdEnum.SEANCE_CINE + ) + + const dayOffers = moviesOfferBuilder(movieOffers) + .withScreeningsOnDay(selectedDate) + .sortedByLast30DaysBooking() + .build() + + const nextOffers = moviesOfferBuilder(movieOffers) + .withoutScreeningsOnDay(selectedDate) + .withoutScreeningsAfterNbDays(15) + .withNextScreeningFromDate(selectedDate) + .sortedByLast30DaysBooking() + .build() + + const after15DaysOffers = moviesOfferBuilder(movieOffers) + .withScreeningsAfterNbDays(15) + .sortedByLast30DaysBooking() + .sortedByDate() + .build() + + const hasStocksOnlyAfter15Days = + !dayOffers.length && !nextOffers.length && after15DaysOffers.length > 0 + + const venueMovieOffers = hasStocksOnlyAfter15Days + ? after15DaysOffers + : [...dayOffers, ...nextOffers] + + return { + venueMovieOffers, + hasStocksOnlyAfter15Days, + } +} diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts index 1e41956bed7..a9c20a2f68c 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts @@ -34,6 +34,28 @@ describe('moviesOfferBuilder', () => { }) }) + describe('withoutMoviesOnDay', () => { + it('should not return movies of the selected day', () => { + const offer1 = mockBuilder.offerResponseV2({ + stocks: [mockBuilder.offerStockResponse({ beginningDatetime: selectedDate.toString() })], + }) + const offer2 = mockBuilder.offerResponseV2({ + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: addDays(selectedDate, 1).toString(), + }), + ], + }) + + const result = moviesOfferBuilder([offer1, offer2]) + .withoutScreeningsOnDay(selectedDate) + .buildOfferResponse() + + expect(result).toHaveLength(1) + expect(result[0]?.id).toBe(offer2.id) + }) + }) + describe('sortedByLast30DaysBooking', () => { it('should sort offers by last 30 days bookings', () => { const offer1 = mockBuilder.offerResponseV2({ last30DaysBookings: 10 }) @@ -128,6 +150,38 @@ describe('moviesOfferBuilder', () => { }) }) + describe('sortedByDate', () => { + it('should sort offers by earliest next date', () => { + const offer1 = mockBuilder.offerResponseV2({ + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: addDays(selectedDate, 3).toString(), + }), + ], + }) + const offer2 = mockBuilder.offerResponseV2({ + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: addDays(selectedDate, 4).toString(), + }), + ], + }) + const offer3 = mockBuilder.offerResponseV2({ + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: addDays(selectedDate, 2).toString(), + }), + ], + }) + + const result = moviesOfferBuilder([offer1, offer2, offer3]) + .sortedByDate() + .buildOfferResponse() + + expect(result.map((o) => o.id)).toEqual([offer3.id, offer1.id, offer2.id]) + }) + }) + describe('withoutMoviesAfterNbDays', () => { it('should filter out movies within the specified range of days', () => { const offer1 = mockBuilder.offerResponseV2({ diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts index c827ba53b87..6c5903c95e9 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts @@ -26,8 +26,12 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => if (!stock.beginningDatetime) { return true } - if (isSameDay(new Date(stock.beginningDatetime), selectedDate)) { - return isAfter(new Date(stock.beginningDatetime), selectedDate) + const isSameDate = isSameDay(new Date(stock.beginningDatetime), selectedDate) + if ( + isSameDate || + (isSameDate && isAfter(new Date(stock.beginningDatetime), selectedDate)) + ) { + return true } return false }) @@ -109,6 +113,14 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => return builderObject }, + sortedByDate: () => { + movieOffers = movieOffers.sort( + (a, b) => new Date(a.nextDate ?? 0).getTime() - new Date(b.nextDate ?? 0).getTime() + ) + + return builderObject + }, + withScreeningsAfterNbDays: (nbDays: number) => { movieOffers = movieOffers .map(({ offer }) => { From 06c324f17b5e7a8f313f2a7368e8e6294d5eb7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 02:17:08 +0100 Subject: [PATCH 06/12] feat: handle calendar display --- .../MoviesScreeningCalendar/VenueCalendar.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx b/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx index 17bf47a84b0..411c5af269b 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx +++ b/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx @@ -2,7 +2,12 @@ import React, { FunctionComponent, useCallback } from 'react' import { View } from 'react-native' import styled from 'styled-components/native' -import { useMoviesScreeningsList } from 'features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList' +import { useOffersStocks } from 'features/offer/api/useOffersStocks' +import { getVenueMovieOffers } from 'features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers' +import { + useDisplayCalendar, + useMovieCalendar, +} from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' import { MovieOfferTile } from 'features/offer/components/MoviesScreeningCalendar/MovieOfferTile' import { VenueOffers } from 'features/venue/types' import { Spacer } from 'ui/theme' @@ -13,20 +18,29 @@ type Props = { } export const VenueCalendar: FunctionComponent = ({ venueOffers, offerIds }) => { - const { moviesOffers } = useMoviesScreeningsList(offerIds) + const { data: offersWithStocks } = useOffersStocks({ offerIds }) + + const { selectedDate } = useMovieCalendar() + + const { venueMovieOffers, hasStocksOnlyAfter15Days } = getVenueMovieOffers( + selectedDate, + offersWithStocks + ) + + useDisplayCalendar(!hasStocksOnlyAfter15Days) const getIsLast = useCallback( (index: number) => { - const length = moviesOffers.length ?? 0 + const length = venueMovieOffers?.length ?? 0 return index === length - 1 }, - [moviesOffers.length] + [venueMovieOffers?.length] ) return ( - {moviesOffers.map((movie, index) => ( + {venueMovieOffers?.map((movie, index) => ( Date: Wed, 22 Jan 2025 02:17:52 +0100 Subject: [PATCH 07/12] refactor: remove some files --- .../filterOffersStocksByDate.test.ts | 112 ----------- .../filterOffersStocksByDate.ts | 7 - .../getNextMoviesByDate.test.ts | 187 ------------------ .../getNextMoviesByDate.ts | 19 -- .../useMoviesScreeningsList.native.test.ts | 82 -------- .../hook/useMoviesScreeningsList.ts | 32 --- 6 files changed, 439 deletions(-) delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.test.ts delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.ts delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.test.ts delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.ts delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.native.test.ts delete mode 100644 src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.ts diff --git a/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.test.ts b/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.test.ts deleted file mode 100644 index dba79831c59..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import mockdate from 'mockdate' - -import { OfferResponseV2, OfferStockResponse } from 'api/gen' -import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' -import { dateBuilder, mockBuilder } from 'tests/mockBuilder' - -import { filterOffersStocksByDate } from './filterOffersStocksByDate' - -const DATE_WITH_STOCK = dateBuilder().withDay(8).toString() -const DATE_WITH_NO_STOCK = dateBuilder().withDay(5).toString() -const AVAILABLE_STOCK_ID = 11 - -const STOCKS = [ - mockBuilder.offerStockResponse({ - id: AVAILABLE_STOCK_ID, - beginningDatetime: DATE_WITH_STOCK, - }), -] - -const offers: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - id: 1, - stocks: STOCKS, - }), -] - -describe('filterOffersStocksByDate', () => { - it('should return all movies with a stock available at a specific date', () => { - const filteredOffersStocks = filterOffersStocksByDate(offers, new Date(DATE_WITH_STOCK)) - const expectedStock = getStockById(filteredOffersStocks, AVAILABLE_STOCK_ID) - - expect(expectedStock).toBeDefined() - }) - - it('should not return movies if a stock is unavailable at a specific date', () => { - const filteredOffersStocksByDate = filterOffersStocksByDate( - offers, - new Date(DATE_WITH_NO_STOCK) - ) - - const expectedStock = getStockById(filteredOffersStocksByDate, AVAILABLE_STOCK_ID) - - expect(expectedStock).not.toBeDefined() - }) - - it('should sort the stocks by descending last30DaysBookings order', () => { - const offers: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - id: 1, - last30DaysBookings: 100, - stocks: STOCKS, - }), - mockBuilder.offerResponseV2({ - id: 3, - last30DaysBookings: 300, - stocks: STOCKS, - }), - mockBuilder.offerResponseV2({ - id: 2, - last30DaysBookings: 200, - stocks: STOCKS, - }), - mockBuilder.offerResponseV2({ - id: 4, - last30DaysBookings: 400, - stocks: STOCKS, - }), - ] - - const filteredOffersStocksByDate = filterOffersStocksByDate(offers, new Date(DATE_WITH_STOCK)) - - expect(filteredOffersStocksByDate[0]?.offer?.last30DaysBookings).toBe(400) - expect(filteredOffersStocksByDate[1]?.offer?.last30DaysBookings).toBe(300) - expect(filteredOffersStocksByDate[2]?.offer.last30DaysBookings).toBe(200) - expect(filteredOffersStocksByDate[3]?.offer.last30DaysBookings).toBe(100) - }) -}) - -it('should not return movies when there is no more screening until the end of the day', () => { - const NOW = dateBuilder().withHours(16).toDate() - mockdate.set(NOW) - - const BEFORE_NOW = dateBuilder().withHours(14).toString() - const AFTER_NOW = dateBuilder().withHours(18).toString() - - const STOCK_BEFORE_NOW = mockBuilder.offerStockResponse({ - beginningDatetime: BEFORE_NOW, - }) - const STOCK_AFTER_NOW = mockBuilder.offerStockResponse({ - beginningDatetime: AFTER_NOW, - }) - - const offersContainingAnOfferAfterNow = filterOffersStocksByDate( - [mockBuilder.offerResponseV2({ stocks: [STOCK_BEFORE_NOW, STOCK_AFTER_NOW] })], - NOW - ) - - expect(offersContainingAnOfferAfterNow).toHaveLength(1) - - const offersContainingNoOfferAfterNow = filterOffersStocksByDate( - [mockBuilder.offerResponseV2({ stocks: [STOCK_BEFORE_NOW] })], - NOW - ) - - expect(offersContainingNoOfferAfterNow).toHaveLength(0) -}) - -const getStockById = (movieOffer: MovieOffer[], id: number) => { - return movieOffer.find(({ offer }) => - offer?.stocks.find((stock: OfferStockResponse) => stock.id === id) - ) -} diff --git a/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.ts b/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.ts deleted file mode 100644 index c746ac9718f..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { OfferResponseV2 } from 'api/gen' -import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' -import { moviesOfferBuilder } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' - -export const filterOffersStocksByDate = (offers: OfferResponseV2[], date: Date): MovieOffer[] => { - return moviesOfferBuilder(offers).withScreeningsOnDay(date).sortedByLast30DaysBooking().build() -} diff --git a/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.test.ts b/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.test.ts deleted file mode 100644 index 0fc1ec54b2c..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import mockdate from 'mockdate' - -import { OfferResponseV2 } from 'api/gen' -import { getNextMoviesByDate } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' -import { dateBuilder, mockBuilder } from 'tests/mockBuilder' - -const SELECTED_DATE = dateBuilder().withDay(5).toString() -const SELECTED_DATE_PLUS_1 = dateBuilder().withDay(6).toString() - -const TODAY = dateBuilder().withDay(1).toString() -const TOMORROW = dateBuilder().withDay(2).toString() -const AFTER_TOMORROW = dateBuilder().withDay(3).toString() -const AFTER_15_DAYS = dateBuilder().withDay(17).toString() - -const TOMORROW_STOCKS = [ - mockBuilder.offerStockResponse({ beginningDatetime: TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_TOMORROW }), -] -const AFTER_15_DAYS_STOCKS = [mockBuilder.offerStockResponse({ beginningDatetime: AFTER_15_DAYS })] - -mockdate.set(TODAY) - -describe('getNextMoviesByDate', () => { - it('should return next movies', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ id: 1, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ id: 2, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ id: 3, stocks: TOMORROW_STOCKS }), - ] - - const nextMovies = getNextMoviesByDate(offerStocks, new Date(TODAY)) - - expect(nextMovies).toHaveLength(offerStocks.length) - }) - - it('should not return movies with only stocks after 15 days', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ id: 1, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ id: 2, stocks: AFTER_15_DAYS_STOCKS }), - ] - - const nextMovies = getNextMoviesByDate(offerStocks, new Date(TODAY)) - - expect(nextMovies).toHaveLength(1) - expect(nextMovies[0]?.offer.id).toBe(1) - }) - - it('should return movies with stocks after 15 days only if they have also stocks in the next 15 days', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - stocks: [ - mockBuilder.offerStockResponse({ beginningDatetime: TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_15_DAYS }), - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_15_DAYS }), - ], - }), - ] - const nextMovies = getNextMoviesByDate(offerStocks, new Date(TODAY)) - - expect(nextMovies).toHaveLength(1) - }) - - it('should return the movies with their first upcoming date', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - stocks: [ - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: TOMORROW }), - ], - }), - ] - - const nextMovies = getNextMoviesByDate(offerStocks, new Date(TODAY)) - - expect(nextMovies[0]?.nextDate).toStrictEqual(new Date(TOMORROW)) - }) - - it('should return the movies with their first upcoming date when the selected day is different than today', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - stocks: [ - mockBuilder.offerStockResponse({ beginningDatetime: TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE_PLUS_1 }), - ], - }), - ] - - const nextMovies = getNextMoviesByDate(offerStocks, new Date(SELECTED_DATE)) - - expect(nextMovies[0]?.nextDate).toStrictEqual(new Date(SELECTED_DATE_PLUS_1)) - }) - - it('should return the movies with the first upcoming stock if there is none after the selected date within the 15 next days from today', () => { - const offerStocks: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ - stocks: [ - mockBuilder.offerStockResponse({ beginningDatetime: TOMORROW }), - mockBuilder.offerStockResponse({ beginningDatetime: AFTER_TOMORROW }), - ], - }), - ] - - const nextMovies = getNextMoviesByDate(offerStocks, new Date(SELECTED_DATE)) - - expect(nextMovies[0]?.nextDate).toStrictEqual(new Date(TOMORROW)) - }) - - it('should return the movies with the no stock available from now', () => { - const NOW = dateBuilder().withHours(16).toDate() - mockdate.set(NOW) - - const BEFORE_NOW = dateBuilder().withHours(14).toString() - const AFTER_NOW = dateBuilder().withHours(18).toString() - - const STOCK_BEFORE_NOW = mockBuilder.offerStockResponse({ beginningDatetime: BEFORE_NOW }) - const STOCK_AFTER_NOW = mockBuilder.offerStockResponse({ beginningDatetime: AFTER_NOW }) - - const offersContainingAnOfferAfterNow = getNextMoviesByDate( - [mockBuilder.offerResponseV2({ stocks: [STOCK_BEFORE_NOW, STOCK_AFTER_NOW] })], - NOW - ) - - expect(offersContainingAnOfferAfterNow).toHaveLength(0) - - const offersContainingNoOfferAfterNow = getNextMoviesByDate( - [mockBuilder.offerResponseV2({ stocks: [STOCK_BEFORE_NOW, ...TOMORROW_STOCKS] })], - NOW - ) - - expect(offersContainingNoOfferAfterNow).toHaveLength(1) - }) - - it('should not return a movie if there is no stock after the selected date', () => { - const NOW = dateBuilder().withHours(16).toDate() - mockdate.set(NOW) - - const BEFORE_NOW = dateBuilder().withHours(14).toString() - - const STOCK_BEFORE_NOW = mockBuilder.offerStockResponse({ beginningDatetime: BEFORE_NOW }) - - const offersContainingNoOfferAfterNow = getNextMoviesByDate( - [mockBuilder.offerResponseV2({ stocks: [STOCK_BEFORE_NOW] })], - NOW - ) - - expect(offersContainingNoOfferAfterNow).toHaveLength(0) - }) - - it('should sort the offers by descending last30DaysBookings order', () => { - const offers: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ last30DaysBookings: 100, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ last30DaysBookings: 300, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ last30DaysBookings: 200, stocks: TOMORROW_STOCKS }), - mockBuilder.offerResponseV2({ last30DaysBookings: 400, stocks: TOMORROW_STOCKS }), - ] - - const filteredOffersStocks = getNextMoviesByDate(offers, new Date(TODAY)) - - expect(filteredOffersStocks[0]?.offer?.last30DaysBookings).toBe(400) - expect(filteredOffersStocks[1]?.offer?.last30DaysBookings).toBe(300) - expect(filteredOffersStocks[2]?.offer.last30DaysBookings).toBe(200) - expect(filteredOffersStocks[3]?.offer.last30DaysBookings).toBe(100) - }) - - it('should sort the offers by ascending beginingDateTime order if 2 offers have the same last30DaysBooking value', () => { - const EARLIER_BEGINNING_DATE_TIME = dateBuilder().withDay(2).withHours(1).toString() - const LATER_BEGINNING_DATE_TIME = dateBuilder().withDay(2).withHours(2).toString() - - const EARLIER_STOCKS = [ - mockBuilder.offerStockResponse({ beginningDatetime: EARLIER_BEGINNING_DATE_TIME }), - ] - const LATER_STOCKS = [ - mockBuilder.offerStockResponse({ beginningDatetime: LATER_BEGINNING_DATE_TIME }), - ] - - const offers: OfferResponseV2[] = [ - mockBuilder.offerResponseV2({ id: 1, last30DaysBookings: 300, stocks: LATER_STOCKS }), - mockBuilder.offerResponseV2({ id: 2, last30DaysBookings: 300, stocks: EARLIER_STOCKS }), - ] - - const filteredOffersStocks = getNextMoviesByDate(offers, new Date(TODAY)) - - expect(filteredOffersStocks[0]?.offer?.id).toBe(2) - expect(filteredOffersStocks[1]?.offer?.id).toBe(1) - }) -}) diff --git a/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.ts b/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.ts deleted file mode 100644 index a57cce2a630..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { OfferResponseV2 } from 'api/gen' -import { moviesOfferBuilder } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' - -export type MovieOffer = { - nextDate?: Date - offer: OfferResponseV2 -} - -export const getNextMoviesByDate = ( - offersWithStocks: OfferResponseV2[], - date: Date -): MovieOffer[] => { - return moviesOfferBuilder(offersWithStocks) - .withoutScreeningsAfterNbDays(15) - .withoutScreeningsOnDay(date) - .withNextScreeningFromDate(date) - .sortedByLast30DaysBooking() - .build() -} diff --git a/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.native.test.ts b/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.native.test.ts deleted file mode 100644 index ec4ca81c596..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.native.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { SubcategoryIdEnum } from 'api/gen' -import * as getStocksByOfferIdsModule from 'features/offer/api/getStocksByOfferIds' -import * as useMovieCalendarModule from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' -import { mockBuilder } from 'tests/mockBuilder' -import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' -import { act, renderHook } from 'tests/utils' - -import { useMoviesScreeningsList } from './useMoviesScreeningsList' - -jest.mock('libs/network/NetInfoWrapper') - -const getStocksByOfferIdsModuleSpy = jest.spyOn(getStocksByOfferIdsModule, 'getStocksByOfferIds') - -describe('useMoviesScreeningsList', () => { - const mockSelectedDate = new Date('2023-05-15') - const mockOfferIds = [1, 2, 3] - - beforeEach(() => { - jest.spyOn(useMovieCalendarModule, 'useMovieCalendar').mockReturnValue({ - selectedDate: mockSelectedDate, - goToDate: jest.fn(), - displayCalendar: jest.fn(), - dates: [], - disableDates: jest.fn(), - displayDates: jest.fn(), - }) - }) - - it('should return filtered movie offers', async () => { - const mockOffers = [ - mockBuilder.offerResponseV2({ - id: 1, - subcategoryId: SubcategoryIdEnum.SEANCE_CINE, - stocks: [ - mockBuilder.offerStockResponse({ - beginningDatetime: '2023-05-15T10:00:00Z', - }), - ], - }), - mockBuilder.offerResponseV2({ - id: 2, - subcategoryId: SubcategoryIdEnum.SEANCE_CINE, - stocks: [ - mockBuilder.offerStockResponse({ - beginningDatetime: '2023-05-16T10:00:00Z', - }), - ], - }), - mockBuilder.offerResponseV2({ - id: 3, - subcategoryId: SubcategoryIdEnum.LIVRE_PAPIER, - stocks: [ - mockBuilder.offerStockResponse({ - beginningDatetime: '2023-05-15T10:00:00Z', - }), - ], - }), - ] - - getStocksByOfferIdsModuleSpy.mockResolvedValueOnce({ offers: mockOffers }) - - const { result } = renderHook(() => useMoviesScreeningsList(mockOfferIds), { - wrapper: ({ children }) => reactQueryProviderHOC(children), - }) - - await act(async () => {}) - - expect(result.current.moviesOffers).toStrictEqual([{ isUpcoming: false, offer: mockOffers[0] }]) - }) - - it('should return an empty array when no offers match the criteria', async () => { - getStocksByOfferIdsModuleSpy.mockResolvedValueOnce({ offers: [] }) - - const { result } = renderHook(() => useMoviesScreeningsList(mockOfferIds), { - wrapper: ({ children }) => reactQueryProviderHOC(children), - }) - - await act(async () => {}) - - expect(result.current.moviesOffers).toHaveLength(0) - }) -}) diff --git a/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.ts b/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.ts deleted file mode 100644 index 9e23e6bcc63..00000000000 --- a/src/features/offer/components/MoviesScreeningCalendar/hook/useMoviesScreeningsList.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useMemo } from 'react' - -import { SubcategoryIdEnum } from 'api/gen' -import { useOffersStocks } from 'features/offer/api/useOffersStocks' -import { filterOffersStocksByDate } from 'features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate' -import { - MovieOffer, - getNextMoviesByDate, -} from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' -import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' - -export const useMoviesScreeningsList = (offerIds: number[]) => { - const { selectedDate } = useMovieCalendar() - const { data: offersWithStocks } = useOffersStocks({ offerIds }) - - const moviesOffers: MovieOffer[] = useMemo(() => { - const filteredOffersWithStocks = filterOffersStocksByDate( - offersWithStocks?.offers || [], - selectedDate - ) - - const nextScreeningOffers = getNextMoviesByDate(offersWithStocks?.offers || [], selectedDate) - - return [...filteredOffersWithStocks, ...nextScreeningOffers] - }, [offersWithStocks?.offers, selectedDate]).filter( - (offer) => offer.offer.subcategoryId === SubcategoryIdEnum.SEANCE_CINE - ) - - return { - moviesOffers, - } -} From 4f681a9892f18754c3c3e19c4d02974df3c622ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 03:06:46 +0100 Subject: [PATCH 08/12] clean: remove spacer + use setFF in test --- .../components/VenueOffers/VenueMovies.tsx | 16 +++++++------- .../VenueOffers/VenueOffers.native.test.tsx | 21 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/features/venue/components/VenueOffers/VenueMovies.tsx b/src/features/venue/components/VenueOffers/VenueMovies.tsx index 424a0ba3989..8c6c08f76e0 100644 --- a/src/features/venue/components/VenueOffers/VenueMovies.tsx +++ b/src/features/venue/components/VenueOffers/VenueMovies.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react' import { InView } from 'react-native-intersection-observer' -import { useTheme } from 'styled-components' import styled from 'styled-components/native' import { MoviesScreeningCalendar } from 'features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar' @@ -8,13 +7,12 @@ import { useOfferCTA } from 'features/offer/components/OfferContent/OfferCTAProv import type { VenueOffers } from 'features/venue/types' import { Anchor } from 'ui/components/anchor/Anchor' import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext' -import { Spacer, TypoDS, getSpacing } from 'ui/theme' +import { TypoDS, getSpacing } from 'ui/theme' import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' const cinemaCTAButtonName = 'Accéder aux séances' export const VenueMovies: React.FC<{ venueOffers: VenueOffers }> = ({ venueOffers }) => { - const { isDesktopViewport } = useTheme() const { setButton, showButton } = useOfferCTA() const scrollToAnchor = useScrollToAnchor() @@ -29,8 +27,7 @@ export const VenueMovies: React.FC<{ venueOffers: VenueOffers }> = ({ venueOffer }, [scrollToAnchor, setButton]) return ( - - + { @@ -39,12 +36,17 @@ export const VenueMovies: React.FC<{ venueOffers: VenueOffers }> = ({ venueOffer {'Les films à l’affiche'} - + - + ) } +const Container = styled.View(({ theme }) => ({ + paddingTop: theme.isDesktopViewport ? getSpacing(10) : getSpacing(6), + gap: theme.isDesktopViewport ? getSpacing(10) : getSpacing(6), +})) + const MoviesTitle = styled(TypoDS.Title3).attrs(getHeadingAttrs(2))({ marginLeft: getSpacing(6), }) diff --git a/src/features/venue/components/VenueOffers/VenueOffers.native.test.tsx b/src/features/venue/components/VenueOffers/VenueOffers.native.test.tsx index b64a59d4ca6..46218dd6cbc 100644 --- a/src/features/venue/components/VenueOffers/VenueOffers.native.test.tsx +++ b/src/features/venue/components/VenueOffers/VenueOffers.native.test.tsx @@ -20,14 +20,13 @@ import { } from 'features/venue/fixtures/venueOffersResponseSnap' import type { VenueOffersArtists, VenueOffers as VenueOffersType } from 'features/venue/types' import { analytics } from 'libs/analytics' -import * as useFeatureFlag from 'libs/firebase/firestore/featureFlags/useFeatureFlag' +import { setFeatureFlags } from 'libs/firebase/firestore/featureFlags/__tests__/setFeatureFlags' +import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types' import { LocationMode } from 'libs/location/types' import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' import { render, screen, userEvent } from 'tests/utils' import { AnchorProvider } from 'ui/components/anchor/AnchorContext' -const mockFeatureFlag = jest.spyOn(useFeatureFlag, 'useFeatureFlag').mockReturnValue(false) - const playlists = gtlPlaylistAlgoliaSnapshot const mockVenue = venueDataTest const venueId = venueDataTest.id @@ -108,6 +107,10 @@ const user = userEvent.setup() jest.useFakeTimers() describe('', () => { + beforeEach(() => { + setFeatureFlags() + }) + it('should display skeleton if offers are fetching', () => { jest.spyOn(useVenueOffers, 'useVenueOffers').mockReturnValueOnce({ isLoading: true, @@ -220,8 +223,8 @@ describe('', () => { }) describe('Cinema venue', () => { - beforeAll(() => { - mockFeatureFlag.mockReturnValue(true) + beforeEach(() => { + setFeatureFlags([RemoteStoreFeatureFlags.WIP_ENABLE_NEW_XP_CINE_FROM_VENUE]) }) it('should display movie screening calendar if at least one offer is a movie screening', async () => { @@ -236,8 +239,8 @@ describe('', () => { describe('Artist playlist', () => { describe('When wipVenueArtistsPlaylist feature flag activated', () => { - beforeAll(() => { - mockFeatureFlag.mockReturnValue(true) + beforeEach(() => { + setFeatureFlags([RemoteStoreFeatureFlags.WIP_VENUE_ARTISTS_PLAYLIST]) }) it('should display artists playlist when venue offers have artists', () => { @@ -277,10 +280,6 @@ describe('', () => { }) describe('When wipVenueArtistsPlaylist feature flag deactivated', () => { - beforeAll(() => { - mockFeatureFlag.mockReturnValue(false) - }) - it('should not display artists playlist when venue offers have artists', () => { renderVenueOffers({ venue: venueDataTest, From acf110aca04fbff6b53a541f58cf1a91656389e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:42:11 +0100 Subject: [PATCH 09/12] dev review: make more accurate test and revert function change --- .../hook/getVenueMovieOffers.native.test.ts | 21 ++++++++++++++----- .../moviesOffer.builder.test.ts | 7 +++++-- .../moviesOffer.builder.ts | 8 +++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts index 1aa20e25605..bee51a5d3c5 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/hook/getVenueMovieOffers.native.test.ts @@ -12,6 +12,9 @@ const BEFORE_SELECTED_DATE = '2023-05-07T00:00:00.000Z' const SELECTED_DATE = '2023-05-15T00:00:00.000Z' const AFTER_SELECTED_DATE = '2023-05-20T00:00:00.000Z' const AT_LEAST_15_DAYS_AFTER_SELECTED_DATE = '2023-06-20T00:00:00.000Z' +const SELECTED_DATE_PLUS_1_SECOND = '2023-05-15T00:00:01.000Z' +const SELECTED_DATE_PLUS_2_SECOND = '2023-05-15T00:00:02.000Z' +const SELECTED_DATE_PLUS_3_SECOND = '2023-05-15T00:00:03.000Z' describe('useVenueMovieOffers', () => { beforeEach(() => { @@ -24,17 +27,23 @@ describe('useVenueMovieOffers', () => { mockBuilder.offerResponseV2({ id: 1, last30DaysBookings: 400, - stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + stocks: [ + mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE_PLUS_1_SECOND }), + ], }), mockBuilder.offerResponseV2({ id: 2, last30DaysBookings: 200, - stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + stocks: [ + mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE_PLUS_2_SECOND }), + ], }), mockBuilder.offerResponseV2({ id: 3, last30DaysBookings: 300, - stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + stocks: [ + mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE_PLUS_3_SECOND }), + ], }), ], } @@ -58,7 +67,9 @@ describe('useVenueMovieOffers', () => { offers: [ mockBuilder.offerResponseV2({ id: 1, - stocks: [mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE })], + stocks: [ + mockBuilder.offerStockResponse({ beginningDatetime: SELECTED_DATE_PLUS_1_SECOND }), + ], }), mockBuilder.offerResponseV2({ id: 2, @@ -119,7 +130,7 @@ describe('useVenueMovieOffers', () => { id: 1, stocks: [ mockBuilder.offerStockResponse({ - beginningDatetime: SELECTED_DATE, + beginningDatetime: SELECTED_DATE_PLUS_1_SECOND, }), ], }), diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts index a9c20a2f68c..6079bf0573b 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.test.ts @@ -1,4 +1,4 @@ -import { addDays } from 'date-fns' +import { addDays, addSeconds } from 'date-fns' import mockdate from 'mockdate' import { mockBuilder } from 'tests/mockBuilder' @@ -36,8 +36,11 @@ describe('moviesOfferBuilder', () => { describe('withoutMoviesOnDay', () => { it('should not return movies of the selected day', () => { + const selectedDatePlus1Second = addSeconds(selectedDate, 1) const offer1 = mockBuilder.offerResponseV2({ - stocks: [mockBuilder.offerStockResponse({ beginningDatetime: selectedDate.toString() })], + stocks: [ + mockBuilder.offerStockResponse({ beginningDatetime: selectedDatePlus1Second.toString() }), + ], }) const offer2 = mockBuilder.offerResponseV2({ stocks: [ diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts index 6c5903c95e9..28fbc1fafbb 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts @@ -27,12 +27,10 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => return true } const isSameDate = isSameDay(new Date(stock.beginningDatetime), selectedDate) - if ( - isSameDate || - (isSameDate && isAfter(new Date(stock.beginningDatetime), selectedDate)) - ) { - return true + if (isSameDate) { + return isAfter(new Date(stock.beginningDatetime), selectedDate) } + return false }) ) From 81664c675affe8c6497c7556a6457e721c2e819d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:56:55 +0100 Subject: [PATCH 10/12] test: add more coverage --- .../MovieOfferTile.native.test.tsx | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx diff --git a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx new file mode 100644 index 00000000000..1a9024c5e74 --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx @@ -0,0 +1,257 @@ +import { addDays } from 'date-fns' +import mockdate from 'mockdate' +import React, { createRef } from 'react' +import { ScrollView } from 'react-native' + +import { + CategoryIdEnum, + NativeCategoryIdEnumv2, + SearchGroupNameEnumv2, + SubcategoryIdEnum, +} from 'api/gen' +import { MovieCalendarProvider } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import * as MovieCalendarContext from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { MovieOfferTile } from 'features/offer/components/MoviesScreeningCalendar/MovieOfferTile' +import { NEXT_SCREENING_WORDING } from 'features/offer/components/MoviesScreeningCalendar/NextScreeningButton' +import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/types' +import { VenueOffers } from 'features/venue/types' +import { setFeatureFlags } from 'libs/firebase/firestore/featureFlags/__tests__/setFeatureFlags' +import { getDates } from 'shared/date/getDates' +import { mockBuilder } from 'tests/mockBuilder' +import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' +import { render, screen, userEvent } from 'tests/utils' +import { AnchorProvider } from 'ui/components/anchor/AnchorContext' + +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') +jest.mock('libs/firebase/analytics/analytics') +jest.mock('react-native/Libraries/Animated/createAnimatedComponent', () => { + return function createAnimatedComponent(Component: unknown) { + return Component + } +}) + +const MOCK_TIMESTAMP = '2024-05-08T12:50:00Z' +const MOCK_DATE = new Date(MOCK_TIMESTAMP) + +const VENUE_OFFERS_HIT = { + _geoloc: { lat: 47.8898, lng: -2.83593 }, + objectID: '1234', + offer: { + dates: [MOCK_DATE.getTime(), MOCK_DATE.getTime()], + isDigital: false, + isDuo: true, + name: 'Harry potter', + prices: [7], + subcategoryId: SubcategoryIdEnum.SEANCE_CINE, + thumbUrl: + 'https://storage.googleapis.com/passculture-metier-ehp-testing-assets-fine-grained/thumbs/mediations/AQBA', + }, + venue: {}, +} + +const VENUE_OFFERS_MOCK = { hits: [VENUE_OFFERS_HIT], nbHits: 1 } + +const MOCK_MOVIE_OFFER = { + isUpcoming: false, + nextDate: new Date('2025-02-07T20:30:10Z'), + offer: mockBuilder.offerResponseV2({ + id: 1, + stocks: [ + mockBuilder.offerStockResponse({ + beginningDatetime: MOCK_TIMESTAMP, + }), + ], + }), +} + +const mockSelectedDate = new Date('2024-05-02') +const mockGoToDate = jest.fn() +const mockDisplayCalendar = jest.fn() + +const mockUseOfferCTAButton = jest.fn() +const mockOnPressOfferCTA = jest.fn() + +const mockUseSubcategoriesMapping = jest.fn() +jest.mock('libs/subcategories/mappings', () => ({ + useSubcategoriesMapping: jest.fn(() => mockUseSubcategoriesMapping()), +})) + +const mockUseSearchGroupLabel = jest.fn() +jest.mock('libs/subcategories/useSearchGroupLabel', () => ({ + useSearchGroupLabel: jest.fn(() => mockUseSearchGroupLabel()), +})) + +jest.mock('features/offer/components/OfferCTAButton/useOfferCTAButton', () => ({ + useOfferCTAButton: jest.fn(() => mockUseOfferCTAButton()), +})) + +describe('MovieOfferTile', () => { + beforeEach(() => { + setFeatureFlags() + jest.spyOn(MovieCalendarContext, 'useMovieCalendar').mockReturnValue({ + selectedDate: mockSelectedDate, + goToDate: mockGoToDate, + displayCalendar: mockDisplayCalendar, + dates: [], + disableDates: jest.fn(), + displayDates: jest.fn(), + }) + + mockUseSubcategoriesMapping.mockReturnValue({ + [SubcategoryIdEnum.SEANCE_CINE]: { + isEvent: false, + categoryId: CategoryIdEnum.CINEMA, + nativeCategoryId: NativeCategoryIdEnumv2.SEANCES_DE_CINEMA, + }, + }) + + mockUseSearchGroupLabel.mockReturnValue(SearchGroupNameEnumv2.CINEMA) + + mockUseOfferCTAButton.mockReturnValue({ + ctaWordingAndAction: { + onPress: jest.fn(), + bottomBannerText: 'CTA', + externalNav: { + url: '', + }, + isDisabled: false, + wording: 'CTA', + navigateTo: { + screen: 'Offer', + }, + }, + showOfferModal: jest.fn(), + openModalOnNavigation: false, + onPress: mockOnPressOfferCTA, + CTAOfferModal: null, + movieScreeningUserData: {}, + }) + }) + + jest.useFakeTimers() + const user = userEvent.setup() + + describe('with screening on selected date', () => { + const ID = '4321' + const movieOffer = { ...MOCK_MOVIE_OFFER, offer: { ...MOCK_MOVIE_OFFER.offer, id: +ID } } + const venueOffersHit = { ...VENUE_OFFERS_HIT, objectID: ID } + const venueOffers = { ...VENUE_OFFERS_MOCK, hits: [...VENUE_OFFERS_MOCK.hits, venueOffersHit] } + + it('should render offer component', async () => { + renderMovieOfferTile({ movieOffer, venueOffers }) + + expect(await screen.findByText(VENUE_OFFERS_HIT.offer.name)).toBeOnTheScreen() + }) + }) + + describe('without screening on selected date', () => { + const ID = '4321' + const venueOffersHit = { ...VENUE_OFFERS_HIT, objectID: ID } + const venueOffers = { ...VENUE_OFFERS_MOCK, hits: [...VENUE_OFFERS_MOCK.hits, venueOffersHit] } + + it('should not render offer component', () => { + renderMovieOfferTile({ movieOffer: MOCK_MOVIE_OFFER, venueOffers }) + + expect(screen.queryByText(VENUE_OFFERS_HIT.offer.name)).not.toBeOnTheScreen() + }) + }) + + describe('with next screening date', () => { + beforeEach(() => mockdate.set(MOCK_DATE)) + + const TODAY_PLUS_20_DAYS = addDays(MOCK_DATE, 20) + const TODAY_PLUS_10_DAYS = addDays(MOCK_DATE, 10) + + it('should render next screening button', async () => { + renderMovieOfferTile({ + movieOffer: MOCK_MOVIE_OFFER, + venueOffers: VENUE_OFFERS_MOCK, + nextScreeningDate: TODAY_PLUS_20_DAYS, + }) + + expect(await screen.findByText(NEXT_SCREENING_WORDING)).toBeOnTheScreen() + }) + + it('should open booking modal on press when nextScreeningDate is at least 15 days after today', async () => { + renderMovieOfferTile({ + movieOffer: MOCK_MOVIE_OFFER, + venueOffers: VENUE_OFFERS_MOCK, + nextScreeningDate: TODAY_PLUS_20_DAYS, + }) + + const nextScreeningButton = await screen.findByText(NEXT_SCREENING_WORDING) + + await user.press(nextScreeningButton) + + expect(mockOnPressOfferCTA).toHaveBeenCalledWith() + }) + + it('should trigger calendar scroll on press when nextScreeningDate is within the next 15 days', async () => { + renderMovieOfferTile({ + movieOffer: MOCK_MOVIE_OFFER, + venueOffers: VENUE_OFFERS_MOCK, + nextScreeningDate: TODAY_PLUS_10_DAYS, + }) + + const nextScreeningButton = await screen.findByText(NEXT_SCREENING_WORDING) + + await user.press(nextScreeningButton) + + expect(mockGoToDate).toHaveBeenCalledWith(TODAY_PLUS_10_DAYS) + }) + }) + + describe('without next screening date', () => { + it('should render event card list component', async () => { + const ID = '4321' + const movieOffer = { ...MOCK_MOVIE_OFFER, offer: { ...MOCK_MOVIE_OFFER.offer, id: +ID } } + const venueOffersHit = { ...VENUE_OFFERS_HIT, objectID: ID } + const venueOffers = { + ...VENUE_OFFERS_MOCK, + hits: [...VENUE_OFFERS_MOCK.hits, venueOffersHit], + } + + renderMovieOfferTile({ + movieOffer, + venueOffers, + isDesktopViewport: true, + }) + + await screen.findByText(VENUE_OFFERS_HIT.offer.name) + + expect(await screen.findByTestId('desktop-event-card-list')).toBeOnTheScreen() + }) + }) +}) + +const next15Dates = getDates(new Date(), 15) + +const renderMovieOfferTile = ({ + movieOffer, + venueOffers, + nextScreeningDate, + isDesktopViewport = false, +}: { + movieOffer: MovieOffer + venueOffers: VenueOffers + nextScreeningDate?: Date + isDesktopViewport?: boolean +}) => { + render( + reactQueryProviderHOC( + ()} handleCheckScrollY={() => 0}> + + + + + ), + { + theme: { isDesktopViewport }, + } + ) +} From cbef60bad8b700c1820eb75f9d60ddae1f05927c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:57:26 +0100 Subject: [PATCH 11/12] refactor: remove some useMemo --- .../useGetVenueByDay/useGetVenuesByDay.ts | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts b/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts index 043f0b368ef..54cc102e5db 100644 --- a/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts +++ b/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts @@ -1,5 +1,4 @@ import { isToday } from 'date-fns' -import { useMemo } from 'react' import { OfferResponseV2 } from 'api/gen' import { moviesOfferBuilder } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' @@ -8,40 +7,27 @@ import { useUserLocation } from 'features/offer/helpers/useUserLocation/useUserL export const useGetVenuesByDay = (date: Date, offers: OfferResponseV2[]) => { const location = useUserLocation() - const dayOffers = useMemo( - () => - moviesOfferBuilder(offers) - .sortedByDistance(location) - .withScreeningsOnDay(date) - .withoutScreeningsAfterNbDays(15) - .build(), - [date, location, offers] - ) + const dayOffers = moviesOfferBuilder(offers) + .sortedByDistance(location) + .withScreeningsOnDay(date) + .build() - const nextOffers = useMemo(() => { - return moviesOfferBuilder(offers) - .withoutScreeningsAfterNbDays(15) - .withoutScreeningsOnDay(date) - .withNextScreeningFromDate(date) - .sortedByDistance(location) - .build() - }, [date, location, offers]) - - const after15DaysOffers = useMemo( - () => - moviesOfferBuilder(offers).sortedByDistance(location).withScreeningsAfterNbDays(15).build(), - [location, offers] - ) + const nextOffers = moviesOfferBuilder(offers) + .withoutScreeningsAfterNbDays(15) + .withoutScreeningsOnDay(date) + .withNextScreeningFromDate(date) + .sortedByDistance(location) + .build() - const movieOffers = useMemo( - () => [...dayOffers, ...nextOffers, ...after15DaysOffers], - [dayOffers, nextOffers, after15DaysOffers] - ) + const after15DaysOffers = moviesOfferBuilder(offers) + .sortedByDistance(location) + .withScreeningsAfterNbDays(15) + .build() - const hasStocksOnlyAfter15Days = useMemo( - () => !dayOffers.length && !nextOffers.length && after15DaysOffers.length > 0, - [after15DaysOffers.length, dayOffers.length, nextOffers.length] - ) + const movieOffers = [...dayOffers, ...nextOffers, ...after15DaysOffers] + + const hasStocksOnlyAfter15Days = + !dayOffers.length && !nextOffers.length && after15DaysOffers.length > 0 return { movieOffers, From 889f5af574e01137f7e6b92adc816f22c93957c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=A8s=20Mouandjo=20Lob=C3=A9?= <158567429+imouandjolobe-pass@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:30:16 +0100 Subject: [PATCH 12/12] clean: small cleaning in MovieOfferTile test --- .../MovieOfferTile.native.test.tsx | 115 +++++++----------- 1 file changed, 45 insertions(+), 70 deletions(-) diff --git a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx index 1a9024c5e74..515218ffb99 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx +++ b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.native.test.tsx @@ -65,72 +65,69 @@ const MOCK_MOVIE_OFFER = { } const mockSelectedDate = new Date('2024-05-02') -const mockGoToDate = jest.fn() const mockDisplayCalendar = jest.fn() - -const mockUseOfferCTAButton = jest.fn() -const mockOnPressOfferCTA = jest.fn() +const mockGoToDate = jest.fn() +jest.spyOn(MovieCalendarContext, 'useMovieCalendar').mockReturnValue({ + selectedDate: mockSelectedDate, + goToDate: mockGoToDate, + displayCalendar: mockDisplayCalendar, + dates: [], + disableDates: jest.fn(), + displayDates: jest.fn(), +}) const mockUseSubcategoriesMapping = jest.fn() +mockUseSubcategoriesMapping.mockReturnValue({ + [SubcategoryIdEnum.SEANCE_CINE]: { + isEvent: false, + categoryId: CategoryIdEnum.CINEMA, + nativeCategoryId: NativeCategoryIdEnumv2.SEANCES_DE_CINEMA, + }, +}) jest.mock('libs/subcategories/mappings', () => ({ - useSubcategoriesMapping: jest.fn(() => mockUseSubcategoriesMapping()), + useSubcategoriesMapping: () => mockUseSubcategoriesMapping(), })) const mockUseSearchGroupLabel = jest.fn() +mockUseSearchGroupLabel.mockReturnValue(SearchGroupNameEnumv2.CINEMA) jest.mock('libs/subcategories/useSearchGroupLabel', () => ({ - useSearchGroupLabel: jest.fn(() => mockUseSearchGroupLabel()), + useSearchGroupLabel: () => mockUseSearchGroupLabel(), })) +const mockOnPressOfferCTA = jest.fn() +const mockUseOfferCTAButton = jest.fn() +mockUseOfferCTAButton.mockReturnValue({ + ctaWordingAndAction: { + onPress: jest.fn(), + bottomBannerText: 'CTA', + externalNav: { + url: '', + }, + isDisabled: false, + wording: 'CTA', + navigateTo: { + screen: 'Offer', + }, + }, + showOfferModal: jest.fn(), + openModalOnNavigation: false, + onPress: mockOnPressOfferCTA, + CTAOfferModal: null, + movieScreeningUserData: {}, +}) jest.mock('features/offer/components/OfferCTAButton/useOfferCTAButton', () => ({ - useOfferCTAButton: jest.fn(() => mockUseOfferCTAButton()), + useOfferCTAButton: () => mockUseOfferCTAButton(), })) +jest.useFakeTimers() +const user = userEvent.setup() + describe('MovieOfferTile', () => { beforeEach(() => { + jest.clearAllMocks() setFeatureFlags() - jest.spyOn(MovieCalendarContext, 'useMovieCalendar').mockReturnValue({ - selectedDate: mockSelectedDate, - goToDate: mockGoToDate, - displayCalendar: mockDisplayCalendar, - dates: [], - disableDates: jest.fn(), - displayDates: jest.fn(), - }) - - mockUseSubcategoriesMapping.mockReturnValue({ - [SubcategoryIdEnum.SEANCE_CINE]: { - isEvent: false, - categoryId: CategoryIdEnum.CINEMA, - nativeCategoryId: NativeCategoryIdEnumv2.SEANCES_DE_CINEMA, - }, - }) - - mockUseSearchGroupLabel.mockReturnValue(SearchGroupNameEnumv2.CINEMA) - - mockUseOfferCTAButton.mockReturnValue({ - ctaWordingAndAction: { - onPress: jest.fn(), - bottomBannerText: 'CTA', - externalNav: { - url: '', - }, - isDisabled: false, - wording: 'CTA', - navigateTo: { - screen: 'Offer', - }, - }, - showOfferModal: jest.fn(), - openModalOnNavigation: false, - onPress: mockOnPressOfferCTA, - CTAOfferModal: null, - movieScreeningUserData: {}, - }) }) - jest.useFakeTimers() - const user = userEvent.setup() - describe('with screening on selected date', () => { const ID = '4321' const movieOffer = { ...MOCK_MOVIE_OFFER, offer: { ...MOCK_MOVIE_OFFER.offer, id: +ID } } @@ -200,28 +197,6 @@ describe('MovieOfferTile', () => { expect(mockGoToDate).toHaveBeenCalledWith(TODAY_PLUS_10_DAYS) }) }) - - describe('without next screening date', () => { - it('should render event card list component', async () => { - const ID = '4321' - const movieOffer = { ...MOCK_MOVIE_OFFER, offer: { ...MOCK_MOVIE_OFFER.offer, id: +ID } } - const venueOffersHit = { ...VENUE_OFFERS_HIT, objectID: ID } - const venueOffers = { - ...VENUE_OFFERS_MOCK, - hits: [...VENUE_OFFERS_MOCK.hits, venueOffersHit], - } - - renderMovieOfferTile({ - movieOffer, - venueOffers, - isDesktopViewport: true, - }) - - await screen.findByText(VENUE_OFFERS_HIT.offer.name) - - expect(await screen.findByTestId('desktop-event-card-list')).toBeOnTheScreen() - }) - }) }) const next15Dates = getDates(new Date(), 15)