From 856d452f6365669dc812e1ccec09fde17b7d793d Mon Sep 17 00:00:00 2001 From: araujobarret Date: Tue, 23 Jul 2024 16:25:50 +0200 Subject: [PATCH] refactor: adds dc32898d12ad5e9f1a00bc60b15567053d5b346c into release branch --- .../ArtworkFilter/useArtworkFilters.ts | 50 +- .../Scenes/Fair/Components/FairArtworks.tsx | 466 ++++++++++++----- .../Scenes/Fair/Components/FairEditorial.tsx | 1 - .../Components/FairFollowedArtistsRail.tsx | 8 +- src/app/Scenes/Fair/Components/FairHeader.tsx | 144 ++---- src/app/Scenes/Fair/Components/FairTiming.tsx | 6 +- src/app/Scenes/Fair/Fair.tests.tsx | 280 ++++------- src/app/Scenes/Fair/Fair.tsx | 467 +++++++----------- .../Fair/FairAllFollowedArtists.tests.tsx | 85 +--- .../Scenes/Fair/FairAllFollowedArtists.tsx | 4 +- src/app/Scenes/Fair/FairArtworks.tests.tsx | 114 ++--- src/app/Scenes/Fair/FairHeader.tests.tsx | 159 +----- src/app/Scenes/Fair/FairOverview.tests.tsx | 132 +++++ src/app/Scenes/Fair/FairOverview.tsx | 119 +++++ .../VanityURL/VanityURLEntity.tests.tsx | 27 +- src/app/Scenes/VanityURL/VanityURLEntity.tsx | 13 +- 16 files changed, 1059 insertions(+), 1016 deletions(-) create mode 100644 src/app/Scenes/Fair/FairOverview.tests.tsx create mode 100644 src/app/Scenes/Fair/FairOverview.tsx diff --git a/src/app/Components/ArtworkFilter/useArtworkFilters.ts b/src/app/Components/ArtworkFilter/useArtworkFilters.ts index 5c3868668f9..00aed63fdea 100644 --- a/src/app/Components/ArtworkFilter/useArtworkFilters.ts +++ b/src/app/Components/ArtworkFilter/useArtworkFilters.ts @@ -1,6 +1,7 @@ import { PAGE_SIZE } from "app/Components/constants" import { MutableRefObject, useEffect, useMemo } from "react" -import { RelayPaginationProp, Variables } from "react-relay" +import { RefetchFnDynamic, RelayPaginationProp, Variables } from "react-relay" +import { FragmentType, OperationType } from "relay-runtime" import { aggregationForFilter, filterArtworksParams, @@ -10,7 +11,13 @@ import { } from "./ArtworkFilterHelpers" import { ArtworksFiltersStore, selectedOptionsUnion } from "./ArtworkFilterStore" -interface UseArtworkFiltersOptions { +// Relay doesn't export their helper type(KeyType), so we have to redefine it here +type RelayData = { " $data"?: unknown; " $fragmentSpreads": FragmentType } | null | undefined + +type UseArtworkFiltersOptions = { + /** + * @deprecated use the prop `refetch` that is returned from relay hooks instead of HoC relay prop + */ relay?: RelayPaginationProp aggregations?: unknown pageSize?: number @@ -20,9 +27,10 @@ interface UseArtworkFiltersOptions { onApply?: () => void onRefetch?: (error?: Error | null) => void refetchRef?: MutableRefObject<() => void> + refetch?: RefetchFnDynamic } -export const useArtworkFilters = ({ +export const useArtworkFilters = ({ relay, aggregations, pageSize = PAGE_SIZE, @@ -32,7 +40,8 @@ export const useArtworkFilters = ({ onRefetch, type = "filter", refetchRef, -}: UseArtworkFiltersOptions) => { + refetch, +}: UseArtworkFiltersOptions) => { const setAggregationsAction = ArtworksFiltersStore.useStoreActions( (state) => state.setAggregationsAction ) @@ -47,10 +56,13 @@ export const useArtworkFilters = ({ setAggregationsAction(aggregations) }, []) - const refetch = () => { - if (relay !== undefined) { - const filterParams = filterArtworksParams(appliedFilters, filterType) + const _refetch = () => { + const filterParams = filterArtworksParams(appliedFilters, filterType) + const refetchArgs = refetchVariables ?? { + input: prepareFilterArtworksParamsForInput(filterParams), + } + if (!!relay) { relay.refetchConnection( pageSize, (error) => { @@ -64,18 +76,36 @@ export const useArtworkFilters = ({ throw new Error(errorMessage) } }, - refetchVariables ?? { input: prepareFilterArtworksParamsForInput(filterParams) } + refetchArgs + ) + return + } + + if (!!refetch) { + refetch( + { ...refetchArgs, count: pageSize }, + { + onComplete: (error) => { + onRefetch?.(error) + if (error) { + const errorMessage = componentPath + ? `${componentPath} ${type} error: ${error.message}` + : error.message + throw new Error(errorMessage) + } + }, + } ) } } if (refetchRef) { - refetchRef.current = refetch + refetchRef.current = _refetch } useEffect(() => { if (applyFilters) { - refetch() + _refetch() if (onApply) { onApply() diff --git a/src/app/Scenes/Fair/Components/FairArtworks.tsx b/src/app/Scenes/Fair/Components/FairArtworks.tsx index e8569734f35..e11a1152afe 100644 --- a/src/app/Scenes/Fair/Components/FairArtworks.tsx +++ b/src/app/Scenes/Fair/Components/FairArtworks.tsx @@ -1,6 +1,8 @@ import { OwnerType } from "@artsy/cohesion" -import { Box } from "@artsy/palette-mobile" -import { FairArtworks_fair$data } from "__generated__/FairArtworks_fair.graphql" +import { Flex, Spinner, Tabs, Text, useSpace } from "@artsy/palette-mobile" +import { MasonryFlashList } from "@shopify/flash-list" +import { FairArtworks_fair$key } from "__generated__/FairArtworks_fair.graphql" +import { ArtworkFilterNavigator, FilterModalMode } from "app/Components/ArtworkFilter" import { aggregationsType, aggregationsWithFollowedArtists, @@ -8,18 +10,25 @@ import { } from "app/Components/ArtworkFilter/ArtworkFilterHelpers" import { ArtworksFiltersStore } from "app/Components/ArtworkFilter/ArtworkFilterStore" import { useArtworkFilters } from "app/Components/ArtworkFilter/useArtworkFilters" +import ArtworkGridItem from "app/Components/ArtworkGrids/ArtworkGridItem" import { FilteredArtworkGridZeroState } from "app/Components/ArtworkGrids/FilteredArtworkGridZeroState" -import { InfiniteScrollArtworksGridContainer } from "app/Components/ArtworkGrids/InfiniteScrollArtworksGrid" +import { HeaderArtworksFilterWithTotalArtworks } from "app/Components/HeaderArtworksFilter/HeaderArtworksFilterWithTotalArtworks" import { FAIR2_ARTWORKS_PAGE_SIZE } from "app/Components/constants" +import { extractNodes } from "app/utils/extractNodes" import { useScreenDimensions } from "app/utils/hooks" +import { + ESTIMATED_MASONRY_ITEM_SIZE, + NUM_COLUMNS_MASONRY, + ON_END_REACHED_THRESHOLD_MASONRY, +} from "app/utils/masonryHelpers" +import { pluralize } from "app/utils/pluralize" import { Schema } from "app/utils/track" -import React, { useEffect } from "react" -import { createPaginationContainer, graphql, RelayPaginationProp } from "react-relay" +import React, { useEffect, useState } from "react" +import { graphql, usePaginationFragment } from "react-relay" import { useTracking } from "react-tracking" interface FairArtworksProps { - fair: FairArtworks_fair$data - relay: RelayPaginationProp + fair: FairArtworks_fair$key initiallyAppliedFilter?: FilterArray aggregations?: aggregationsType followedArtistCount?: number | null | undefined @@ -27,17 +36,178 @@ interface FairArtworksProps { export const FairArtworks: React.FC = ({ fair, - relay, initiallyAppliedFilter, aggregations, followedArtistCount, }) => { const tracking = useTracking() + const space = useSpace() + const { width } = useScreenDimensions() + const [isFilterArtworksModalVisible, setFilterArtworkModalVisible] = useState(false) + const { data, isLoadingNext, hasNext, loadNext, refetch } = usePaginationFragment(fragment, fair) - const screenWidth = useScreenDimensions().width + const setInitialFilterStateAction = ArtworksFiltersStore.useStoreActions( + (state) => state.setInitialFilterStateAction + ) + const setFiltersCountAction = ArtworksFiltersStore.useStoreActions( + (state) => state.setFiltersCountAction + ) + const counts = ArtworksFiltersStore.useStoreState((state) => state.counts) - const artworks = fair.fairArtworks + const artworks = data.fairArtworks const artworksTotal = artworks?.counts?.total ?? 0 + const dispatchFollowedArtistCount = + (followedArtistCount || artworks?.counts?.followedArtists) ?? 0 + const artworkAggregations = ((aggregations || artworks?.aggregations) ?? []) as aggregationsType + const dispatchAggregations = aggregationsWithFollowedArtists( + dispatchFollowedArtistCount, + artworkAggregations + ) + + useArtworkFilters({ + refetch, + aggregations: dispatchAggregations, + componentPath: "Fair/FairArtworks", + pageSize: FAIR2_ARTWORKS_PAGE_SIZE, + }) + + useEffect(() => { + if (initiallyAppliedFilter) { + setInitialFilterStateAction(initiallyAppliedFilter) + } + }, []) + + useEffect(() => { + setFiltersCountAction({ ...counts, total: artworksTotal }) + }, [artworksTotal]) + + if (!data) { + return null + } + + const handleOnEndReached = () => { + if (!isLoadingNext && hasNext) { + loadNext(FAIR2_ARTWORKS_PAGE_SIZE, { + onComplete: (error) => { + if (error) { + console.error("FairArtworks.tsx", error.message) + } + }, + }) + } + } + + const handleTrackClear = (id: string, slug: string) => { + tracking.trackEvent(tracks.trackClear(id, slug)) + } + + const handleFilterToggle = () => { + setFilterArtworkModalVisible((prev) => { + if (!prev) { + tracking.trackEvent(tracks.openArtworksFilter(fair)) + } else { + tracking.trackEvent(tracks.closeArtworksFilter(fair)) + } + return !prev + }) + } + + const filteredArtworks = extractNodes(data.fairArtworks) + const artworksCount = data.fairArtworks?.counts?.total ?? 0 + + return ( + <> + item.id} + numColumns={NUM_COLUMNS_MASONRY} + estimatedItemSize={ESTIMATED_MASONRY_ITEM_SIZE} + keyboardShouldPersistTaps="handled" + ListEmptyComponent={ + + + + } + // need to pass zIndex: 1 here in order for the SubTabBar to + // be visible above list content + ListHeaderComponentStyle={{ zIndex: 1 }} + ListHeaderComponent={ + <> + + + + + {`${artworksCount} ${pluralize( + "Artwork", + artworksCount + )}:`} + + } + ListFooterComponent={ + !!isLoadingNext ? ( + + + + ) : null + } + onEndReached={handleOnEndReached} + onEndReachedThreshold={ON_END_REACHED_THRESHOLD_MASONRY} + renderItem={({ item, index, columnIndex }) => { + const imgAspectRatio = item.image?.aspectRatio ?? 1 + const imgWidth = width / NUM_COLUMNS_MASONRY - space(2) - space(1) + const imgHeight = imgWidth / imgAspectRatio + + return ( + + + + ) + }} + /> + + + ) +} + +// Performant version of FairArtworks that doesn't use tabs +// left in the same file intentionally to make it easier to compare the two +// forcing changes in both components +export const FairArtworksWithoutTabs: React.FC = ({ + fair, + initiallyAppliedFilter, + aggregations, + followedArtistCount, +}) => { + const tracking = useTracking() + const space = useSpace() + const { width } = useScreenDimensions() + const [isFilterArtworksModalVisible, setFilterArtworkModalVisible] = useState(false) + const { data, isLoadingNext, hasNext, loadNext, refetch } = usePaginationFragment(fragment, fair) const setInitialFilterStateAction = ArtworksFiltersStore.useStoreActions( (state) => state.setInitialFilterStateAction @@ -47,6 +217,8 @@ export const FairArtworks: React.FC = ({ ) const counts = ArtworksFiltersStore.useStoreState((state) => state.counts) + const artworks = data.fairArtworks + const artworksTotal = artworks?.counts?.total ?? 0 const dispatchFollowedArtistCount = (followedArtistCount || artworks?.counts?.followedArtists) ?? 0 const artworkAggregations = ((aggregations || artworks?.aggregations) ?? []) as aggregationsType @@ -56,7 +228,7 @@ export const FairArtworks: React.FC = ({ ) useArtworkFilters({ - relay, + refetch, aggregations: dispatchAggregations, componentPath: "Fair/FairArtworks", pageSize: FAIR2_ARTWORKS_PAGE_SIZE, @@ -72,129 +244,177 @@ export const FairArtworks: React.FC = ({ setFiltersCountAction({ ...counts, total: artworksTotal }) }, [artworksTotal]) - const trackClear = (id: string, slug: string) => { - tracking.trackEvent({ - action_name: "clearFilters", - context_screen: Schema.ContextModules.ArtworkGrid, - context_screen_owner_type: Schema.OwnerEntityTypes.Fair, - context_screen_owner_id: id, - context_screen_owner_slug: slug, - action_type: Schema.ActionTypes.Tap, - }) + if (!data) { + return null } - if (artworksTotal === 0) { - return ( - - - - ) + const handleOnEndReached = () => { + if (!isLoadingNext && hasNext) { + loadNext(FAIR2_ARTWORKS_PAGE_SIZE, { + onComplete: (error) => { + if (error) { + console.error("FairArtworks.tsx", error.message) + } + }, + }) + } } + const handleTrackClear = (id: string, slug: string) => { + tracking.trackEvent(tracks.trackClear(id, slug)) + } + + const handleFilterToggle = () => { + setFilterArtworkModalVisible((prev) => { + if (!prev) { + tracking.trackEvent(tracks.openArtworksFilter(fair)) + } else { + tracking.trackEvent(tracks.closeArtworksFilter(fair)) + } + return !prev + }) + } + + const filteredArtworks = extractNodes(data.fairArtworks) + return ( - - + item.id} + numColumns={NUM_COLUMNS_MASONRY} + estimatedItemSize={ESTIMATED_MASONRY_ITEM_SIZE} + keyboardShouldPersistTaps="handled" + ListEmptyComponent={ + + + + } + ListHeaderComponent={} + ListFooterComponent={ + !!isLoadingNext ? ( + + + + ) : null + } + onEndReached={handleOnEndReached} + onEndReachedThreshold={ON_END_REACHED_THRESHOLD_MASONRY} + renderItem={({ item, index, columnIndex }) => { + const imgAspectRatio = item.image?.aspectRatio ?? 1 + const imgWidth = width / NUM_COLUMNS_MASONRY - space(2) - space(1) + const imgHeight = imgWidth / imgAspectRatio + + return ( + + + + ) + }} + /> + - + ) } -export const FairArtworksFragmentContainer = createPaginationContainer( - FairArtworks, - { - fair: graphql` - fragment FairArtworks_fair on Fair - @argumentDefinitions( - count: { type: "Int", defaultValue: 30 } - cursor: { type: "String" } - input: { type: "FilterArtworksInput" } - ) { - slug - internalID - fairArtworks: filterArtworksConnection( - first: $count - after: $cursor - aggregations: [ - ARTIST - ARTIST_NATIONALITY - COLOR - DIMENSION_RANGE - FOLLOWED_ARTISTS - LOCATION_CITY - MAJOR_PERIOD - MATERIALS_TERMS - MEDIUM - PARTNER - PRICE_RANGE - ] - input: $input - ) @connection(key: "Fair_fairArtworks") { - aggregations { - slice - counts { - count - name - value - } - } - edges { - node { - id - } - } - counts { - total - followedArtists - } - ...InfiniteScrollArtworksGrid_connection +const fragment = graphql` + fragment FairArtworks_fair on Fair + @refetchable(queryName: "FairArtworksPaginationQuery") + @argumentDefinitions( + count: { type: "Int", defaultValue: 30 } + cursor: { type: "String" } + input: { type: "FilterArtworksInput" } + ) { + slug + internalID + fairArtworks: filterArtworksConnection( + first: $count + after: $cursor + aggregations: [ + ARTIST + ARTIST_NATIONALITY + COLOR + DIMENSION_RANGE + FOLLOWED_ARTISTS + LOCATION_CITY + MAJOR_PERIOD + MATERIALS_TERMS + MEDIUM + PARTNER + PRICE_RANGE + ] + input: $input + ) @connection(key: "Fair_fairArtworks") { + aggregations { + slice + counts { + count + name + value } } - `, - }, - { - getConnectionFromProps(props) { - return props?.fair.fairArtworks - }, - getFragmentVariables(previousVariables, count) { - // Relay is unable to infer this for this component, I'm not sure why. - return { - ...previousVariables, - count, - } - }, - getVariables(props, { count, cursor }, fragmentVariables) { - return { - input: fragmentVariables.input, - props, - count, - cursor, - id: props.fair.slug, - } - }, - query: graphql` - query FairArtworksInfiniteScrollGridQuery( - $id: String! - $count: Int! - $cursor: String - $input: FilterArtworksInput - ) { - fair(id: $id) { - ...FairArtworks_fair @arguments(count: $count, cursor: $cursor, input: $input) + edges { + node { + ...ArtworkGridItem_artwork @arguments(includeAllImages: false) + id + image(includeAll: false) { + aspectRatio + } } } - `, + counts { + total + followedArtists + } + } } -) +` + +const tracks = { + closeArtworksFilter: (fair: any) => ({ + action_name: "closeFilterWindow", + context_screen_owner_type: Schema.OwnerEntityTypes.Fair, + context_screen: Schema.PageNames.FairPage, + context_screen_owner_id: fair.internalID, + context_screen_owner_slug: fair.slug, + action_type: Schema.ActionTypes.Tap, + }), + openArtworksFilter: (fair: any) => ({ + action_name: "filter", + context_screen_owner_type: Schema.OwnerEntityTypes.Fair, + context_screen: Schema.PageNames.FairPage, + context_screen_owner_id: fair.internalID, + context_screen_owner_slug: fair.slug, + action_type: Schema.ActionTypes.Tap, + }), + trackClear: (id: string, slug: string) => ({ + action_name: "clearFilters", + context_screen: Schema.ContextModules.ArtworkGrid, + context_screen_owner_type: Schema.OwnerEntityTypes.Fair, + context_screen_owner_id: id, + context_screen_owner_slug: slug, + action_type: Schema.ActionTypes.Tap, + }), +} diff --git a/src/app/Scenes/Fair/Components/FairEditorial.tsx b/src/app/Scenes/Fair/Components/FairEditorial.tsx index 68fe0cfbdc7..172ed2a921d 100644 --- a/src/app/Scenes/Fair/Components/FairEditorial.tsx +++ b/src/app/Scenes/Fair/Components/FairEditorial.tsx @@ -36,7 +36,6 @@ export const FairEditorial: React.FC = ({ fair, ...rest }) = return ( = ( return ( <> - + = ( { + if (!artwork.href) { + return + } + trackEvent( tracks.tappedArtwork(fair, artwork?.internalID ?? "", artwork?.slug ?? "", position) ) - navigate(artwork?.href!) + navigate(artwork.href) }} /> diff --git a/src/app/Scenes/Fair/Components/FairHeader.tsx b/src/app/Scenes/Fair/Components/FairHeader.tsx index d11cc65d6b7..b68ffc6c662 100644 --- a/src/app/Scenes/Fair/Components/FairHeader.tsx +++ b/src/app/Scenes/Fair/Components/FairHeader.tsx @@ -1,57 +1,30 @@ -import { Spacer, ChevronIcon, Flex, Box, Text } from "@artsy/palette-mobile" -import { FairHeader_fair$data } from "__generated__/FairHeader_fair.graphql" -import OpaqueImageView from "app/Components/OpaqueImageView/OpaqueImageView" -import { ReadMore } from "app/Components/ReadMore" -import { shouldShowLocationMap } from "app/Scenes/Fair/FairMoreInfo" -import { navigate } from "app/system/navigation/navigate" -import { truncatedTextLimit } from "app/utils/hardware" -import { Dimensions, TouchableOpacity } from "react-native" -import { createFragmentContainer, graphql } from "react-relay" +import { Spacer, Flex, Text, useScreenDimensions, Image } from "@artsy/palette-mobile" +import { FairHeader_fair$key } from "__generated__/FairHeader_fair.graphql" +import { FC } from "react" +import { graphql, useFragment } from "react-relay" import { FairTimingFragmentContainer as FairTiming } from "./FairTiming" - interface FairHeaderProps { - fair: FairHeader_fair$data + fair: FairHeader_fair$key } -export const FairHeader: React.FC = ({ fair }) => { - const { - name, - slug, - about, - image, - tagline, - location, - ticketsLink, - fairHours, - fairLinks, - fairContact, - summary, - fairTickets, - } = fair - const screenWidth = Dimensions.get("screen").width - const profileImageUrl = fair?.profile?.icon?.imageUrl - const previewText = summary || about +export const FairHeader: FC = ({ fair }) => { + const data = useFragment(fragment, fair) + const { width } = useScreenDimensions() + + if (!data) { + return null + } - const canShowMoreInfoLink = - !!about || - !!tagline || - !!location?.summary || - shouldShowLocationMap(location?.coordinates) || - !!ticketsLink || - !!fairHours || - !!fairLinks || - !!fairContact || - !!summary || - !!fairTickets + const profileImageUrl = data.profile?.icon?.imageUrl return ( - - {!!image ? ( - - + {!!data.image ? ( + + {!!profileImageUrl && ( = ({ fair }) => { bottom={0} left={2} > - + )} ) : ( - + )} - + - {name} + {data.name} - - {!!previewText && ( - - )} - {!!canShowMoreInfoLink && ( - navigate(`/fair/${slug}/info`)}> - - More info - - - - )} - - + + + ) } -const SafeTopMargin = () => - -export const FairHeaderFragmentContainer = createFragmentContainer(FairHeader, { - fair: graphql` - fragment FairHeader_fair on Fair { - about - summary - name - slug - profile { - icon { - imageUrl: url(version: "untouched-png") - } +const fragment = graphql` + fragment FairHeader_fair on Fair { + name + profile { + icon { + imageUrl: url(version: "untouched-png") } - image { - imageUrl: url(version: "large_rectangle") - aspectRatio - } - # Used to figure out if we should render the More info link - tagline - location { - summary - coordinates { - lat - lng - } - } - ticketsLink - fairHours: hours(format: MARKDOWN) # aliased to avoid conflicts in the VanityURLQueryRenderer - fairLinks: links(format: MARKDOWN) # aliased to avoid conflicts in the VanityURLQueryRenderer - fairTickets: tickets(format: MARKDOWN) # aliased to avoid conflicts in the VanityURLQueryRenderer - fairContact: contact(format: MARKDOWN) # aliased to avoid conflicts in the VanityURLQueryRenderer - ...FairTiming_fair } - `, -}) + image { + imageUrl: url(version: "large_rectangle") + aspectRatio + } + ...FairTiming_fair + } +` diff --git a/src/app/Scenes/Fair/Components/FairTiming.tsx b/src/app/Scenes/Fair/Components/FairTiming.tsx index b00f8314b15..fb0e63a9a01 100644 --- a/src/app/Scenes/Fair/Components/FairTiming.tsx +++ b/src/app/Scenes/Fair/Components/FairTiming.tsx @@ -1,4 +1,4 @@ -import { Box, Text } from "@artsy/palette-mobile" +import { Flex, Text } from "@artsy/palette-mobile" import { FairTiming_fair$data } from "__generated__/FairTiming_fair.graphql" import { EventTiming } from "app/Components/EventTiming" import { WithCurrentTime } from "app/Components/WithCurrentTime" @@ -12,7 +12,7 @@ export const FairTiming: React.FC = ({ fair: { exhibitionPeriod, startAt, endAt }, }) => { return ( - + {exhibitionPeriod} @@ -28,7 +28,7 @@ export const FairTiming: React.FC = ({ }} - + ) } diff --git a/src/app/Scenes/Fair/Fair.tests.tsx b/src/app/Scenes/Fair/Fair.tests.tsx index 947d31106bf..1248583cfe1 100644 --- a/src/app/Scenes/Fair/Fair.tests.tsx +++ b/src/app/Scenes/Fair/Fair.tests.tsx @@ -1,235 +1,127 @@ +import { Tabs, Text } from "@artsy/palette-mobile" +import { screen } from "@testing-library/react-native" import { FairTestsQuery } from "__generated__/FairTestsQuery.graphql" -import { NavigationalTabs, Tab } from "app/Components/LegacyTabs" -import { extractText } from "app/utils/tests/extractText" -import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { graphql, QueryRenderer } from "react-relay" -import { act } from "react-test-renderer" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" import { useTracking } from "react-tracking" -import { createMockEnvironment, MockPayloadGenerator } from "relay-test-utils" -import { FairArtworksFragmentContainer } from "./Components/FairArtworks" -import { FairCollectionsFragmentContainer } from "./Components/FairCollections" -import { FairEditorialFragmentContainer } from "./Components/FairEditorial" -import { FairExhibitorsFragmentContainer } from "./Components/FairExhibitors" -import { FairFollowedArtistsRailFragmentContainer } from "./Components/FairFollowedArtistsRail" -import { FairHeaderFragmentContainer } from "./Components/FairHeader" -import { Fair, FairFragmentContainer } from "./Fair" +import { Fair } from "./Fair" + +jest.mock("@artsy/palette-mobile", () => { + const palette = jest.requireActual("@artsy/palette-mobile") + + return { + ...palette, + Tabs: { + ...palette.Tabs, + TabsWithHeader: jest.fn(), + }, + } +}) describe("Fair", () => { + const TabsWithHeader = Tabs.TabsWithHeader as jest.Mock const trackEvent = useTracking().trackEvent - let env: ReturnType - beforeEach(() => { - env = createMockEnvironment() - }) - - const TestRenderer = () => ( - - environment={env} - query={graphql` - query FairTestsQuery($fairID: String!) @relay_test_operation { - fair(id: $fairID) { - ...Fair_fair - } - } - `} - variables={{ fairID: "art-basel-hong-kong-2020" }} - render={({ props, error }) => { - if (props?.fair) { - return - } else if (error) { - console.log(error) + const { renderWithRelay } = setupTestWrapper({ + Component: ({ fair }) => , + query: graphql` + query FairTestsQuery($fairID: String!) @relay_test_operation { + fair(id: $fairID) @required(action: NONE) { + ...Fair_fair } - }} - /> - ) - - const getWrapper = (mockResolvers = {}) => { - const tree = renderWithWrappersLEGACY() - act(() => { - env.mock.resolveMostRecentOperation((operation) => - MockPayloadGenerator.generate(operation, mockResolvers) - ) - }) - return tree - } - - it("renders without throwing an error", () => { - const wrapper = getWrapper() - expect(wrapper.root.findAllByType(Fair)).toHaveLength(1) + } + `, }) - it("renders the necessary components when fair is active", () => { - const wrapper = getWrapper({ - Fair: () => ({ - isActive: true, - counts: { - artworks: 42, - partnerShows: 42, - }, - }), - }) - - expect(wrapper.root.findAllByType(FairHeaderFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairEditorialFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairCollectionsFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(NavigationalTabs)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairExhibitorsFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairFollowedArtistsRailFragmentContainer)).toHaveLength(1) + beforeEach(() => { + jest.clearAllMocks() }) - it("renders fewer components when fair is inactive", () => { - const wrapper = getWrapper({ - Fair: () => ({ - isActive: false, - }), + it("renders without throwing an error", () => { + TabsWithHeader.mockImplementation((props) => { + return ( + <> + {props.title} + {props.BelowTitleHeaderComponent()} + {props.children} + + ) }) + renderWithRelay() - expect(wrapper.root.findAllByType(FairHeaderFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairEditorialFragmentContainer)).toHaveLength(1) - expect(extractText(wrapper.root)).toMatch("This fair is currently unavailable.") - - expect(wrapper.root.findAllByType(FairCollectionsFragmentContainer)).toHaveLength(0) - expect(wrapper.root.findAllByType(NavigationalTabs)).toHaveLength(0) - expect(wrapper.root.findAllByType(FairExhibitorsFragmentContainer)).toHaveLength(0) - expect(wrapper.root.findAllByType(FairFollowedArtistsRailFragmentContainer)).toHaveLength(0) + expect(screen.getByText("Overview")).toBeOnTheScreen() + expect(screen.getByText("Exhibitors")).toBeOnTheScreen() + expect(screen.getByText("Artworks")).toBeOnTheScreen() }) - it("does not render components when there is no data for them", () => { - const wrapper = getWrapper({ - Fair: () => ({ - articles: { - edges: [], - }, - marketingCollections: [], - counts: { - artworks: 0, - partnerShows: 0, - }, - }), - }) - expect(wrapper.root.findAllByType(FairHeaderFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairEditorialFragmentContainer)).toHaveLength(0) - expect(wrapper.root.findAllByType(FairCollectionsFragmentContainer)).toHaveLength(0) - expect(wrapper.root.findAllByType(NavigationalTabs)).toHaveLength(0) - expect(wrapper.root.findAllByType(FairExhibitorsFragmentContainer)).toHaveLength(0) - expect(wrapper.root.findAllByType(FairArtworksFragmentContainer)).toHaveLength(0) - }) + it("tracks tap navigating to the exhibitors tab", () => { + TabsWithHeader.mockImplementation((props) => { + props.onTabChange?.({ tabName: "Exhibitors" }) - it("renders the collections component if there are collections", () => { - const wrapper = getWrapper({ - Fair: () => ({ - isActive: true, - marketingCollections: [ - { - slug: "great-collection", - }, - ], - }), + return ( + <> + {props.title} + {props.BelowTitleHeaderComponent()} + {props.children} + + ) }) - expect(wrapper.root.findAllByType(FairCollectionsFragmentContainer)).toHaveLength(1) - }) + renderWithRelay() - it("renders the editorial component if there are articles", () => { - const wrapper = getWrapper({ + renderWithRelay({ Fair: () => ({ isActive: true, - articles: { - edges: [ - { - __typename: "Article", - node: { - slug: "great-article", - }, - }, - ], + slug: "art-basel-hong-kong-2020", + internalID: "fair1244", + counts: { + artworks: 100, + partnerShows: 20, }, }), }) - expect(wrapper.root.findAllByType(FairEditorialFragmentContainer)).toHaveLength(1) - }) - it("renders the artists you follow rail if there are any artworks", () => { - let wrapper = getWrapper({ - Fair: () => ({ - isActive: true, - filterArtworksConnection: { - edges: [], - }, - }), + expect(trackEvent).toHaveBeenCalledWith({ + action: "tappedNavigationTab", + context_module: "artworksTab", + context_screen_owner_type: "fair", + context_screen_owner_slug: "art-basel-hong-kong-2020", + context_screen_owner_id: "fair1244", + subject: "Exhibitors", }) + }) - expect(wrapper.root.findAllByType(FairFollowedArtistsRailFragmentContainer)).toHaveLength(0) + it("tracks tap navigating to the artworks tab", () => { + TabsWithHeader.mockImplementation((props) => { + props.onTabChange?.({ tabName: "Artworks" }) - wrapper = getWrapper({ - Fair: () => ({ - isActive: true, - followedArtistArtworks: { - edges: [ - { - __typename: "FilterArtworkEdge", - artwork: { - slug: "an-artwork", - }, - }, - ], - }, - }), + return ( + <> + {props.title} + {props.BelowTitleHeaderComponent()} + {props.children} + + ) }) - expect(wrapper.root.findAllByType(FairFollowedArtistsRailFragmentContainer)).toHaveLength(1) - }) - - it("renders the artworks/exhibitors component and tabs if there are artworks and exhibitors", () => { - const wrapper = getWrapper({ + renderWithRelay({ Fair: () => ({ isActive: true, + slug: "art-basel-hong-kong-2020", + internalID: "fair1244", counts: { artworks: 100, partnerShows: 20, }, }), }) - expect(wrapper.root.findAllByType(Tab)).toHaveLength(2) - expect(wrapper.root.findAllByType(FairExhibitorsFragmentContainer)).toHaveLength(1) - expect(wrapper.root.findAllByType(FairArtworksFragmentContainer)).toHaveLength(0) - }) - - describe("tracks taps navigating between the artworks tab and exhibitors tab", () => { - it("When Using Palette V3", () => { - const wrapper = getWrapper({ - Fair: () => ({ - isActive: true, - slug: "art-basel-hong-kong-2020", - internalID: "fair1244", - counts: { - artworks: 100, - partnerShows: 20, - }, - }), - }) - const tabs = wrapper.root.findAllByType(Tab) - const exhibitorsTab = tabs[0] - const artworksTab = tabs[1] - - act(() => artworksTab.props.onPress()) - expect(trackEvent).toHaveBeenCalledWith({ - action: "tappedNavigationTab", - context_module: "exhibitorsTab", - context_screen_owner_type: "fair", - context_screen_owner_slug: "art-basel-hong-kong-2020", - context_screen_owner_id: "fair1244", - subject: "Artworks", - }) - act(() => exhibitorsTab.props.onPress()) - expect(trackEvent).toHaveBeenCalledWith({ - action: "tappedNavigationTab", - context_module: "artworksTab", - context_screen_owner_type: "fair", - context_screen_owner_slug: "art-basel-hong-kong-2020", - context_screen_owner_id: "fair1244", - subject: "Exhibitors", - }) + expect(trackEvent).toHaveBeenCalledWith({ + action: "tappedNavigationTab", + context_module: "exhibitorsTab", + context_screen_owner_type: "fair", + context_screen_owner_slug: "art-basel-hong-kong-2020", + context_screen_owner_id: "fair1244", + subject: "Artworks", }) }) }) diff --git a/src/app/Scenes/Fair/Fair.tsx b/src/app/Scenes/Fair/Fair.tsx index 8dde65fb12e..10b25375211 100644 --- a/src/app/Scenes/Fair/Fair.tsx +++ b/src/app/Scenes/Fair/Fair.tsx @@ -1,318 +1,231 @@ import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion" -import { Spacer, ChevronIcon, Flex, Box, Separator } from "@artsy/palette-mobile" +import { + Flex, + Tabs, + Skeleton, + SkeletonBox, + useScreenDimensions, + SkeletonText, + useSpace, + ShareIcon, +} from "@artsy/palette-mobile" import { FairQuery } from "__generated__/FairQuery.graphql" -import { Fair_fair$data } from "__generated__/Fair_fair.graphql" -import { ArtworkFilterNavigator, FilterModalMode } from "app/Components/ArtworkFilter" +import { Fair_fair$data, Fair_fair$key } from "__generated__/Fair_fair.graphql" import { ArtworkFiltersStoreProvider } from "app/Components/ArtworkFilter/ArtworkFilterStore" -import { PlaceholderGrid } from "app/Components/ArtworkGrids/GenericGrid" -import { HeaderArtworksFilterWithTotalArtworks as HeaderArtworksFilter } from "app/Components/HeaderArtworksFilter/HeaderArtworksFilterWithTotalArtworks" -import { HeaderButton } from "app/Components/HeaderButton" -import { NavigationalTabs, TabsType } from "app/Components/LegacyTabs" +import { getShareURL } from "app/Components/ShareSheet/helpers" +import { useToast } from "app/Components/Toast/toastHook" +import { FairArtworks } from "app/Scenes/Fair/Components/FairArtworks" +import { FairExhibitorsFragmentContainer } from "app/Scenes/Fair/Components/FairExhibitors" +import { FairOverview } from "app/Scenes/Fair/FairOverview" import { goBack } from "app/system/navigation/navigate" -import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" -import { useScreenDimensions } from "app/utils/hooks" -import { PlaceholderBox, PlaceholderText } from "app/utils/placeholders" -import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { ProvideScreenTracking, Schema } from "app/utils/track" -import React, { useRef, useState } from "react" -import { FlatList } from "react-native" -import Animated, { runOnJS, useAnimatedScrollHandler } from "react-native-reanimated" -import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" +import { useClientQuery } from "app/utils/useClientQuery" +import React from "react" +import { TouchableOpacity } from "react-native" +import RNShare from "react-native-share" +import { createFragmentContainer, graphql, useFragment } from "react-relay" import { useTracking } from "react-tracking" -import { FairArtworksFragmentContainer } from "./Components/FairArtworks" -import { FairCollectionsFragmentContainer } from "./Components/FairCollections" -import { FairEditorialFragmentContainer } from "./Components/FairEditorial" -import { FairEmptyStateFragmentContainer } from "./Components/FairEmptyState" -import { FairExhibitorsFragmentContainer } from "./Components/FairExhibitors" -import { FairFollowedArtistsRailFragmentContainer } from "./Components/FairFollowedArtistsRail" -import { FairHeaderFragmentContainer } from "./Components/FairHeader" - -const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) -const BACK_ICON_SIZE = 21 -const HEADER_SCROLL_THRESHOLD = 50 - -interface FairQueryRendererProps { - fairID: string -} +import { FairHeader } from "./Components/FairHeader" interface FairProps { - fair: Fair_fair$data + fair: Fair_fair$key } -const tabs: TabsType = [ - { - label: "Exhibitors", - }, - { - label: "Artworks", - }, -] - export const Fair: React.FC = ({ fair }) => { - const { isActive } = fair - const hasArticles = !!fair.articles?.edges?.length - const hasCollections = !!fair.marketingCollections.length - const hasArtworks = !!(fair.counts?.artworks ?? 0 > 0) - const hasExhibitors = !!(fair.counts?.partnerShows ?? 0 > 0) - const hasFollowedArtistArtworks = !!(fair.followedArtistArtworks?.edges?.length ?? 0 > 0) - + const data = useFragment(fragment, fair) const tracking = useTracking() - const [activeTab, setActiveTab] = useState(0) - - const flatListRef = useRef(null) - const [isFilterArtworksModalVisible, setFilterArtworkModalVisible] = useState(false) - const [shouldHideButtons, setShouldHideButtons] = useState(false) - - const sections = isActive - ? [ - "fairHeader", - ...(hasArticles ? ["fairEditorial"] : []), - ...(hasCollections ? ["fairCollections"] : []), - ...(hasFollowedArtistArtworks ? ["fairFollowedArtistsRail"] : []), - ...(hasArtworks && hasExhibitors ? ["fairTabsAndFilter", "fairTabChildContent"] : []), - ] - : ["fairHeader", ...(hasArticles ? ["fairEditorial"] : []), "notActive"] - - const stickyIndex = sections.indexOf("fairTabsAndFilter") - - const { safeAreaInsets } = useScreenDimensions() - - const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 30 }) + const toast = useToast() - const handleFilterArtworksModal = () => { - setFilterArtworkModalVisible(!isFilterArtworksModalVisible) + if (!data) { + return null } - const trackTappedNavigationTab = (destinationTab: number) => { - const trackTappedArtworkTabProps = { - action: ActionType.tappedNavigationTab, - context_screen_owner_type: OwnerType.fair, - context_screen_owner_id: fair.internalID, - context_screen_owner_slug: fair.slug, - context_module: ContextModule.exhibitorsTab, - subject: "Artworks", - } - const trackTappedExhibitorsTabProps = { - action: ActionType.tappedNavigationTab, - context_screen_owner_type: OwnerType.fair, - context_screen_owner_id: fair.internalID, - context_screen_owner_slug: fair.slug, - context_module: ContextModule.artworksTab, - subject: "Exhibitors", - } - - if (activeTab !== destinationTab) { - if (tabs[destinationTab].label === "Artworks") { - tracking.trackEvent(trackTappedArtworkTabProps) - } else { - tracking.trackEvent(trackTappedExhibitorsTabProps) + const handleSharePress = async () => { + try { + const url = getShareURL(`/fair/${data.slug}?utm_content=fair-share`) + const message = `View ${data.name} on Artsy` + + await RNShare.open({ + title: data.name || "", + message: message + "\n" + url, + failOnCancel: true, + }) + toast.show("Copied to Clipboard", "middle", { Icon: ShareIcon }) + } catch (error) { + if (typeof error === "string" && error.includes("User did not share")) { + console.error("Collection.tsx", error) } } } - const openFilterArtworksModal = () => { - tracking.trackEvent({ - action_name: "filter", - context_screen_owner_type: Schema.OwnerEntityTypes.Fair, - context_screen: Schema.PageNames.FairPage, - context_screen_owner_id: fair.internalID, - context_screen_owner_slug: fair.slug, - action_type: Schema.ActionTypes.Tap, - }) - handleFilterArtworksModal() - } - - const closeFilterArtworksModal = () => { - tracking.trackEvent({ - action_name: "closeFilterWindow", - context_screen_owner_type: Schema.OwnerEntityTypes.Fair, - context_screen: Schema.PageNames.FairPage, - context_screen_owner_id: fair.internalID, - context_screen_owner_slug: fair.slug, - action_type: Schema.ActionTypes.Tap, - }) - handleFilterArtworksModal() + const handleTabChange = (tabName: string) => { + switch (tabName) { + case "Exhibitors": + tracking.trackEvent(tracks.tappedExhibitorsTabProps(data)) + break + case "Artworks": + tracking.trackEvent(tracks.tappedArtworkTabProps(data)) + break + } } - const scrollHandler = useAnimatedScrollHandler((event) => { - const hideButtons = event.contentOffset.y > HEADER_SCROLL_THRESHOLD - return runOnJS(setShouldHideButtons)(hideButtons) - }) + const hasExhibitors = !!data._exhibitors?.totalCount return ( - - } - ListFooterComponent={} - keyExtractor={(_item, index) => String(index)} - stickyHeaderIndices={[stickyIndex]} - onScroll={scrollHandler} - scrollEventThrottle={100} - keyboardShouldPersistTaps="handled" - renderItem={({ item }): null | any => { - switch (item) { - case "fairHeader": { - return ( - <> - - - - ) - } - case "notActive": { - return - } - case "fairFollowedArtistsRail": { - return - } - case "fairEditorial": { - return - } - case "fairCollections": { - return - } - case "fairTabsAndFilter": { - const tabToShow = tabs ? tabs[activeTab] : null - return ( - - { - trackTappedNavigationTab(index as number) - setActiveTab(index) - }} - activeTab={activeTab} - tabs={tabs} - /> - {tabToShow?.label === "Artworks" && ( - - )} - - ) - } - case "fairTabChildContent": { - const tabToShow = tabs ? tabs[activeTab] : null - - if (!tabToShow) { - return null - } - - if (tabToShow.label === "Exhibitors") { - return - } - - if (tabToShow.label === "Artworks") { - return ( - - - - - ) - } - } - } - }} - /> - - - goBack()} position="left"> - - + } + onTabChange={({ tabName }) => handleTabChange(tabName)} + headerProps={{ + onBack: goBack, + rightElements: ( + { + handleSharePress() + }} + > + + + ), + }} + > + + + + + + + {!!hasExhibitors ? ( + + + + + + ) : null} + + + + + + + ) } -export const FairFragmentContainer = createFragmentContainer(Fair, { - fair: graphql` - fragment Fair_fair on Fair { - internalID - slug - isActive - articles: articlesConnection(first: 5, sort: PUBLISHED_AT_DESC) { - edges { - __typename - } - } - marketingCollections(size: 5) { - __typename - } - counts { - artworks - partnerShows - } - followedArtistArtworks: filterArtworksConnection( - first: 20 - input: { includeArtworksByFollowedArtists: true } - ) { - edges { - __typename - } - } - ...FairHeader_fair - ...FairEmptyState_fair - ...FairEditorial_fair - ...FairCollections_fair - ...FairArtworks_fair @arguments(input: { sort: "-decayed_merch" }) - ...FairExhibitors_fair - ...FairFollowedArtistsRail_fair +const fragment = graphql` + fragment Fair_fair on Fair { + ...FairOverview_fair + ...FairHeader_fair + ...FairArtworks_fair @arguments(input: { sort: "-decayed_merch" }) + ...FairExhibitors_fair + + internalID + slug + name + # Used to figure out if we should render the exhibitors tab + _exhibitors: showsConnection(first: 1, sort: FEATURED_ASC) { + totalCount } - `, + } +` + +export const FairFragmentContainer = createFragmentContainer(Fair, { + fair: fragment, }) +const query = graphql` + query FairQuery($fairID: String!) { + fair(id: $fairID) @principalField { + ...Fair_fair + } + } +` + +interface FairQueryRendererProps { + fairID: string +} + export const FairQueryRenderer: React.FC = ({ fairID }) => { + const res = useClientQuery({ query, variables: { fairID } }) + + if (res.loading) { + return + } + + if (!res.data?.fair) { + return null + } + + return +} + +export const FairPlaceholder: React.FC = () => { + const { safeAreaInsets } = useScreenDimensions() + const space = useSpace() + return ( - - environment={getRelayEnvironment()} - query={graphql` - query FairQuery($fairID: String!) { - fair(id: $fairID) @principalField { - ...Fair_fair - } - } - `} - variables={{ fairID }} - render={renderWithPlaceholder({ - Container: FairFragmentContainer, - renderPlaceholder: () => , - })} - /> + + + + + + + + + + Fair Text Long Placeholder + + + Fair Date + + + + Overview + Exhibitors + Artworks + + + Fair Text Long Placeholder + + + ) } -export const FairPlaceholder: React.FC = () => ( - - - - - - {/* Fair name */} - - {/* Fair info */} - - - - - - - - {/* masonry grid */} - - -) +const tracks = { + tappedArtworkTabProps: (fair: Fair_fair$data) => ({ + action: ActionType.tappedNavigationTab, + context_screen_owner_type: OwnerType.fair, + context_screen_owner_id: fair.internalID, + context_screen_owner_slug: fair.slug, + context_module: ContextModule.exhibitorsTab, + subject: "Artworks", + }), + tappedExhibitorsTabProps: (fair: Fair_fair$data) => ({ + action: ActionType.tappedNavigationTab, + context_screen_owner_type: OwnerType.fair, + context_screen_owner_id: fair.internalID, + context_screen_owner_slug: fair.slug, + context_module: ContextModule.artworksTab, + subject: "Exhibitors", + }), +} diff --git a/src/app/Scenes/Fair/FairAllFollowedArtists.tests.tsx b/src/app/Scenes/Fair/FairAllFollowedArtists.tests.tsx index 77a666790d6..eb17f5d0aec 100644 --- a/src/app/Scenes/Fair/FairAllFollowedArtists.tests.tsx +++ b/src/app/Scenes/Fair/FairAllFollowedArtists.tests.tsx @@ -1,76 +1,27 @@ +import { screen } from "@testing-library/react-native" import { FairAllFollowedArtistsTestsQuery } from "__generated__/FairAllFollowedArtistsTestsQuery.graphql" -import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { graphql, QueryRenderer } from "react-relay" -import { act } from "react-test-renderer" -import { createMockEnvironment, MockPayloadGenerator } from "relay-test-utils" -import { FairArtworksFragmentContainer } from "./Components/FairArtworks" -import { - FairAllFollowedArtists, - FairAllFollowedArtistsFragmentContainer, -} from "./FairAllFollowedArtists" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" +import { FairAllFollowedArtistsFragmentContainer } from "./FairAllFollowedArtists" describe("FairAllFollowedArtists", () => { - let env: ReturnType - - beforeEach(() => { - env = createMockEnvironment() - }) - - const TestRenderer = () => ( - - environment={env} - query={graphql` - query FairAllFollowedArtistsTestsQuery($fairID: String!) @relay_test_operation { - fair(id: $fairID) { - ...FairAllFollowedArtists_fair - } - fairForFilters: fair(id: $fairID) { - ...FairAllFollowedArtists_fairForFilters - } + const { renderWithRelay } = setupTestWrapper({ + Component: FairAllFollowedArtistsFragmentContainer, + query: graphql` + query FairAllFollowedArtistsTestsQuery @relay_test_operation { + fair(id: "fair-id") @required(action: NONE) { + ...FairAllFollowedArtists_fair } - `} - variables={{ fairID: "art-basel-hong-kong-2019" }} - render={({ props, error }) => { - if (props?.fair && props?.fairForFilters) { - return ( - - ) - } else if (error) { - console.log(error) + fairForFilters: fair(id: "fair-id") @required(action: NONE) { + ...FairAllFollowedArtists_fairForFilters } - }} - /> - ) - - const getWrapper = (mockResolvers = {}) => { - const tree = renderWithWrappersLEGACY() - act(() => { - env.mock.resolveMostRecentOperation((operation) => - MockPayloadGenerator.generate(operation, mockResolvers) - ) - }) - return tree - } - - it("renders without throwing an error", () => { - const wrapper = getWrapper() - expect(wrapper.root.findAllByType(FairAllFollowedArtists)).toHaveLength(1) + } + `, }) - it("renders a grid of artworks in the fair filtered by artists the user follows", () => { - const wrapper = getWrapper() - expect(wrapper.root.findAllByType(FairArtworksFragmentContainer)).toHaveLength(1) - expect( - wrapper.root.findAllByType(FairArtworksFragmentContainer)[0].props.initiallyAppliedFilter - ).toStrictEqual([ - { - displayText: "All Artists I Follow", - paramName: "includeArtworksByFollowedArtists", - paramValue: true, - }, - ]) + it.only("renders", () => { + renderWithRelay() + + expect(screen.getByText("Artworks")).toBeOnTheScreen() }) }) diff --git a/src/app/Scenes/Fair/FairAllFollowedArtists.tsx b/src/app/Scenes/Fair/FairAllFollowedArtists.tsx index f518354ebb1..aee0c7375b0 100644 --- a/src/app/Scenes/Fair/FairAllFollowedArtists.tsx +++ b/src/app/Scenes/Fair/FairAllFollowedArtists.tsx @@ -20,7 +20,7 @@ import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import React, { useState } from "react" import { ScrollView } from "react-native" import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" -import { FairArtworksFragmentContainer } from "./Components/FairArtworks" +import { FairArtworksWithoutTabs } from "./Components/FairArtworks" interface FairAllFollowedArtistsProps { fair: FairAllFollowedArtists_fair$data @@ -53,7 +53,7 @@ export const FairAllFollowedArtists: React.FC = ({ - { - let mockEnvironment: ReturnType - - const TestRenderer = () => ( - - query={query} - environment={mockEnvironment} - variables={{ - fairID: "art-basel-hong-kong-2020", - }} - render={({ props, error }) => { - if (error) { - console.log(error) - return null - } - - if (!props?.fair) { - return null + const { renderWithRelay } = setupTestWrapper({ + Component: ({ fair }) => ( + + + + ), + query: graphql` + query FairArtworksTestsQuery @relay_test_operation { + fair(id: "fair-id") @required(action: NONE) { + ...FairArtworks_fair } - - return ( - - - - ) - }} - /> - ) - - beforeEach(() => { - mockEnvironment = getMockRelayEnvironment() - }) - - it("renders a grid of artworks", async () => { - renderWithWrappers() - - resolveMostRecentRelayOperation(mockEnvironment, { - Fair: () => fair, - }) - - await flushPromiseQueue() - - expect(screen.getByText("Artwork Title")).toBeTruthy() + } + `, }) - it("requests artworks in batches of 30", () => { - renderWithWrappers() - - resolveMostRecentRelayOperation(mockEnvironment, { - Fair: () => fair, + it("renders", async () => { + renderWithRelay({ + Fair: () => ({ + fairArtworks: { + edges: [{ node: { title: "Artwork Title" } }], + counts: { + total: 1, + }, + }, + }), }) - const artworksGridContainer = screen.UNSAFE_getByType(InfiniteScrollArtworksGridContainer) - expect(artworksGridContainer.props).toMatchObject({ pageSize: 30 }) + expect(screen.getByText("Artwork Title")).toBeOnTheScreen() }) it("renders empty view if there are no artworks", async () => { - renderWithWrappers() - - resolveMostRecentRelayOperation(mockEnvironment, { + renderWithRelay({ Fair: () => ({ fairArtworks: { edges: [], @@ -80,32 +48,6 @@ describe("FairArtworks", () => { }), }) - await flushPromiseQueue() - - expect(screen.getByText(/No results found/)).toBeTruthy() + expect(screen.getByText(/No results found/)).toBeOnTheScreen() }) }) - -const query = graphql` - query FairArtworksTestsQuery($fairID: String!) @relay_test_operation { - fair(id: $fairID) { - ...FairArtworks_fair - } - } -` - -const artwork = { - slug: "artwork-slug", - id: "artwork-id", - internalID: "artwork-internalID", - title: "Artwork Title", -} - -const fair = { - fairArtworks: { - edges: [{ node: artwork }], - counts: { - total: 1, - }, - }, -} diff --git a/src/app/Scenes/Fair/FairHeader.tests.tsx b/src/app/Scenes/Fair/FairHeader.tests.tsx index 95f7539968f..55b143b7119 100644 --- a/src/app/Scenes/Fair/FairHeader.tests.tsx +++ b/src/app/Scenes/Fair/FairHeader.tests.tsx @@ -1,96 +1,35 @@ -import { Spacer } from "@artsy/palette-mobile" -import { FairHeaderTestsQuery } from "__generated__/FairHeaderTestsQuery.graphql" -import OpaqueImageView from "app/Components/OpaqueImageView/OpaqueImageView" -import { FairHeader, FairHeaderFragmentContainer } from "app/Scenes/Fair/Components/FairHeader" -import { navigate } from "app/system/navigation/navigate" -import { extractText } from "app/utils/tests/extractText" -import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { TouchableOpacity } from "react-native" -import { graphql, QueryRenderer } from "react-relay" -import { act } from "react-test-renderer" -import { createMockEnvironment, MockPayloadGenerator } from "relay-test-utils" -import { FairTimingFragmentContainer } from "./Components/FairTiming" +import { screen } from "@testing-library/react-native" +import { FairHeader } from "app/Scenes/Fair/Components/FairHeader" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" describe("FairHeader", () => { - let env: ReturnType - - beforeEach(() => { - env = createMockEnvironment() - }) - - const TestRenderer = () => ( - - environment={env} - query={graphql` - query FairHeaderTestsQuery($fairID: String!) @relay_test_operation { - fair(id: $fairID) { - ...FairHeader_fair - } + const { renderWithRelay } = setupTestWrapper({ + Component: FairHeader, + query: graphql` + query FairHeaderTestsQuery @relay_test_operation { + fair(id: "fair-id") { + ...FairHeader_fair } - `} - variables={{ fairID: "art-basel-hong-kong-2020" }} - render={({ props, error }) => { - if (props?.fair) { - return - } else if (error) { - console.log(error) - } - }} - /> - ) + } + `, + }) - const getWrapper = (mockResolvers = {}) => { - const tree = renderWithWrappersLEGACY() - act(() => { - env.mock.resolveMostRecentOperation((operation) => - MockPayloadGenerator.generate(operation, mockResolvers) - ) - }) - return tree - } + it("renders", () => { + renderWithRelay() - it("renders without throwing an error", () => { - const wrapper = getWrapper() - expect(wrapper.root.findAllByType(FairHeader)).toHaveLength(1) + expect(screen.getByText(/mock-value-for-field-"name"/)).toBeOnTheScreen() + expect(screen.getByText(/mock-value-for-field-"exhibitionPeriod"/)).toBeOnTheScreen() }) it("renders the fair title", () => { - const wrapper = getWrapper({ - Fair: () => ({ - name: "Art Basel Hong Kong 2020", - }), - }) - expect(wrapper.root.findByProps({ variant: "lg-display" }).props.children).toBe( - "Art Basel Hong Kong 2020" - ) - }) + renderWithRelay({ Fair: () => ({ name: "Art Basel Hong Kong 2020" }) }) - it("renders the fair main image when present", () => { - const wrapper = getWrapper({ - Fair: () => ({ - image: { - imageUrl: "https://testing.artsy.net/art-basel-hong-kong-image", - }, - }), - }) - const mainImage = wrapper.root.findAllByType(OpaqueImageView)[0] - expect(mainImage.props).toMatchObject({ - imageURL: "https://testing.artsy.net/art-basel-hong-kong-image", - }) - }) - - it("renders a spacer instead when the fair main image is absent", () => { - const wrapper = getWrapper({ - Fair: () => ({ - image: null, - }), - }) - expect(wrapper.root.findAllByType(OpaqueImageView)).toHaveLength(0) - expect(wrapper.root.findAllByType(Spacer)).not.toHaveLength(0) + expect(screen.getByText("Art Basel Hong Kong 2020")).toBeOnTheScreen() }) it("renders the fair icon", () => { - const wrapper = getWrapper({ + renderWithRelay({ Fair: () => ({ profile: { icon: { @@ -99,67 +38,17 @@ describe("FairHeader", () => { }, }), }) - expect(wrapper.root.findAllByType(OpaqueImageView)[1].props.imageURL).toBe( - "https://testing.artsy.net/art-basel-hong-kong-icon" - ) - }) - - it("renders the fair description", () => { - const wrapper = getWrapper({ - Fair: () => ({ - summary: "The biggest art fair in Hong Kong", - }), - }) - expect(extractText(wrapper.root)).toMatch("The biggest art fair in Hong Kong") - }) - - it("falls back to About when Summary isn't available", () => { - const wrapper = getWrapper({ - Fair: () => ({ - about: "A great place to buy art", - summary: "", - }), - }) - expect(extractText(wrapper.root)).toMatch("A great place to buy art") - }) - - it("navigates to the fair info page on press of More Info", () => { - const wrapper = getWrapper({ - Fair: () => ({ - slug: "art-basel-hong-kong-2020", - }), - }).root.findByType(TouchableOpacity) - wrapper.props.onPress() - expect(navigate).toHaveBeenCalledWith("/fair/art-basel-hong-kong-2020/info") - }) - it("does not show the More Info link if there is no info to show", () => { - const wrapper = getWrapper({ - Fair: () => ({ - about: "", - fairContact: "", - fairHours: "", - fairLinks: "", - fairTickets: "", - location: { - summary: "", - coordinates: null, - }, - summary: "", - tagline: "", - ticketsLink: "", - }), - }) - expect(wrapper.root.findAllByType(TouchableOpacity).length).toBe(0) + expect(screen.getByTestId("fair-profile-image")).toBeOnTheScreen() }) it("displays the timing info", () => { - const wrapper = getWrapper({ + renderWithRelay({ Fair: () => ({ endAt: "2020-09-19T08:00:00+00:00", }), }) - expect(wrapper.root.findAllByType(FairTimingFragmentContainer).length).toBe(1) - expect(extractText(wrapper.root)).toMatch("Closed") + + expect(screen.getByText("Closed")).toBeOnTheScreen() }) }) diff --git a/src/app/Scenes/Fair/FairOverview.tests.tsx b/src/app/Scenes/Fair/FairOverview.tests.tsx new file mode 100644 index 00000000000..186e5b6fb1c --- /dev/null +++ b/src/app/Scenes/Fair/FairOverview.tests.tsx @@ -0,0 +1,132 @@ +import { fireEvent, screen } from "@testing-library/react-native" +import { FairOverviewTestsQuery } from "__generated__/FairOverviewTestsQuery.graphql" +import { FairOverview } from "app/Scenes/Fair/FairOverview" +import { navigate } from "app/system/navigation/navigate" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" + +describe("FairOverview", () => { + const { renderWithRelay } = setupTestWrapper({ + Component: ({ fair }) => , + query: graphql` + query FairOverviewTestsQuery @relay_test_operation { + fair(id: "example") @required(action: NONE) { + ...FairOverview_fair + } + } + `, + }) + + it("renders", () => { + renderWithRelay() + + expect(screen.getByText(/mock-value-for-field-"summary"/)).toBeOnTheScreen() + expect(screen.getByText("More info")).toBeOnTheScreen() + }) + + it("renders the necessary components when fair is active", () => { + renderWithRelay({ + Fair: () => ({ + isActive: true, + summary: "The biggest art fair in Hong Kong", + counts: { + artworks: 42, + partnerShows: 42, + }, + marketingCollections: [{ title: "collection-1" }], + }), + }) + + expect(screen.getByText("The biggest art fair in Hong Kong")).toBeOnTheScreen() + expect(screen.getByText("View all")).toBeOnTheScreen() + expect(screen.getByText("collection-1")).toBeOnTheScreen() + expect(screen.getByText("Works by Artists You Follow")).toBeOnTheScreen() + }) + + it("renders the About when Summary isn't available", () => { + renderWithRelay({ + Fair: () => ({ + about: "A great place to buy art", + summary: "", + }), + }) + + expect(screen.getByText("A great place to buy art")).toBeOnTheScreen() + }) + + it("renders fewer components when fair is inactive", () => { + renderWithRelay({ + Fair: () => ({ + isActive: false, + }), + }) + + expect(screen.getByText("View all")).toBeOnTheScreen() + expect(screen.queryByText("collection-1")).not.toBeOnTheScreen() + expect(screen.queryByText("Works by Artists You Follow")).not.toBeOnTheScreen() + }) + + it("does not render components when there is no data for them", () => { + renderWithRelay({ + Fair: () => ({ + articles: { + edges: [], + }, + marketingCollections: [], + counts: { + artworks: 0, + partnerShows: 0, + }, + }), + }) + + expect(screen.queryByText("View all")).not.toBeOnTheScreen() + expect(screen.queryByText("collection-1")).not.toBeOnTheScreen() + expect(screen.queryByText("Works by Artists You Follow")).not.toBeOnTheScreen() + }) + + it("doest not render the artists you follow rail if there is no artwork", () => { + renderWithRelay({ + Fair: () => ({ + isActive: true, + filterArtworksConnection: { + edges: [], + }, + }), + }) + + expect(screen.queryByText("Works by Artists You Follow")).not.toBeOnTheScreen() + }) + + it("navigates to the fair info page on press of More Info", () => { + renderWithRelay({ + Fair: () => ({ + slug: "art-basel-hong-kong-2020", + }), + }) + + fireEvent.press(screen.getByText("More info")) + expect(navigate).toHaveBeenCalledWith("/fair/art-basel-hong-kong-2020/info") + }) + + it("does not show the More Info link if there is no info to show", () => { + renderWithRelay({ + Fair: () => ({ + about: "", + contact: "", + hours: "", + links: "", + tickets: "", + location: { + summary: "", + coordinates: null, + }, + summary: "", + tagline: "", + ticketsLink: "", + }), + }) + + expect(screen.queryByText("More info")).not.toBeOnTheScreen() + }) +}) diff --git a/src/app/Scenes/Fair/FairOverview.tsx b/src/app/Scenes/Fair/FairOverview.tsx new file mode 100644 index 00000000000..57295af0947 --- /dev/null +++ b/src/app/Scenes/Fair/FairOverview.tsx @@ -0,0 +1,119 @@ +import { ChevronIcon, Flex, Spacer, Tabs, Text, Touchable, useSpace } from "@artsy/palette-mobile" +import { FairOverview_fair$key } from "__generated__/FairOverview_fair.graphql" +import { ReadMore } from "app/Components/ReadMore" +import { FairCollectionsFragmentContainer } from "app/Scenes/Fair/Components/FairCollections" +import { FairEditorialFragmentContainer } from "app/Scenes/Fair/Components/FairEditorial" +import { FairEmptyStateFragmentContainer } from "app/Scenes/Fair/Components/FairEmptyState" +import { FairFollowedArtistsRailFragmentContainer } from "app/Scenes/Fair/Components/FairFollowedArtistsRail" +import { shouldShowLocationMap } from "app/Scenes/Fair/FairMoreInfo" +import { navigate } from "app/system/navigation/navigate" +import { truncatedTextLimit } from "app/utils/hardware" +import { FC } from "react" +import { graphql, useFragment } from "react-relay" + +interface FairOverviewProps { + fair: FairOverview_fair$key +} + +export const FairOverview: FC = ({ fair }) => { + const space = useSpace() + const data = useFragment(fragment, fair) + + if (!data) { + return null + } + + const hasArticles = !!data.articlesConnection?.totalCount + const hasCollections = !!data.marketingCollections.length + const hasFollowedArtistArtworks = !!data.filterArtworksConnection?.edges?.length + const hasPreviewText = data.summary || data.about + // TOFIX: Must be a better way to determine if there is more info to show + const canShowMoreInfoLink = + !!data.about || + !!data.tagline || + !!data.location?.summary || + shouldShowLocationMap(data.location?.coordinates) || + !!data.ticketsLink || + !!data.hours || + !!data.links || + !!data.contact || + !!data.summary || + !!data.tickets + + return ( + + + {!!hasPreviewText && ( + + )} + {!!canShowMoreInfoLink && ( + navigate(`/fair/${data.slug}/info`)}> + + More info + + + + )} + + {!!data.isActive ? ( + <> + {!!hasArticles && } + {!!hasCollections && } + {!!hasFollowedArtistArtworks && ( + + )} + + ) : ( + <> + + {!!hasArticles ? ( + + ) : ( + + )} + + )} + + + + + ) +} + +const fragment = graphql` + fragment FairOverview_fair on Fair { + ...FairEditorial_fair + ...FairCollections_fair + ...FairFollowedArtistsRail_fair + ...FairEmptyState_fair + + isActive + slug + summary + about + tagline + location { + summary + coordinates { + lat + lng + } + } + ticketsLink + hours(format: MARKDOWN) + links(format: MARKDOWN) + tickets(format: MARKDOWN) + contact(format: MARKDOWN) + articlesConnection(first: 1, sort: PUBLISHED_AT_DESC) { + totalCount + } + marketingCollections(size: 5) { + __typename + } + filterArtworksConnection(first: 20, input: { includeArtworksByFollowedArtists: true }) { + edges { + __typename + } + } + } +` diff --git a/src/app/Scenes/VanityURL/VanityURLEntity.tests.tsx b/src/app/Scenes/VanityURL/VanityURLEntity.tests.tsx index 973fecf16a6..328ed40d08b 100644 --- a/src/app/Scenes/VanityURL/VanityURLEntity.tests.tsx +++ b/src/app/Scenes/VanityURL/VanityURLEntity.tests.tsx @@ -1,5 +1,6 @@ import { Spinner } from "@artsy/palette-mobile" -import { Fair, FairFragmentContainer, FairPlaceholder } from "app/Scenes/Fair/Fair" +import { waitFor } from "@testing-library/react-native" +import { Fair, FairPlaceholder } from "app/Scenes/Fair/Fair" import { PartnerContainer, PartnerSkeleton } from "app/Scenes/Partner/Partner" import { getMockRelayEnvironment } from "app/system/relay/defaultEnvironment" import { __renderWithPlaceholderTestUtils__ } from "app/utils/renderWithPlaceholder" @@ -28,6 +29,17 @@ describe("VanityURLEntity", () => { env = getMockRelayEnvironment() }) + // const {} = setupTestWrapper({ + // Component: VanityURLEntity, + // query: graphql` + // query VanityURLEntityQuery($slug: String!) { + // vanityURLEntity(slug: $slug) { + // ...VanityURLEntity_fairOrPartner + // } + // } + // `, + // }) + it("renders a VanityURLPossibleRedirect when 404", () => { if (__renderWithPlaceholderTestUtils__) { __renderWithPlaceholderTestUtils__.allowFallbacksAtTestTime = true @@ -39,16 +51,19 @@ describe("VanityURLEntity", () => { expect(UNSAFE_getAllByType(VanityURLPossibleRedirect)).toHaveLength(1) }) - it("renders a fairQueryRenderer when given a fair id", () => { + it("renders a fairQueryRenderer when given a fair id", async () => { const tree = renderWithWrappersLEGACY( ) - expect(env.mock.getMostRecentOperation().request.node.operation.name).toBe("FairQuery") + + await waitFor(() => + expect(env.mock.getMostRecentOperation().request.node.operation.name).toBe("FairQuery") + ) act(() => { env.mock.resolveMostRecentOperation((operation) => MockPayloadGenerator.generate(operation)) }) - const fairComponent = tree.root.findByType(Fair) - expect(fairComponent).toBeDefined() + + await waitFor(() => expect(tree.root.findByType(Fair)).toBeDefined()) }) describe("rendering a profile", () => { @@ -121,7 +136,7 @@ describe("VanityURLEntity", () => { }) ) }) - const fairComponent = tree.root.findByType(FairFragmentContainer) + const fairComponent = tree.root.findByType(Fair) expect(fairComponent).toBeDefined() }) diff --git a/src/app/Scenes/VanityURL/VanityURLEntity.tsx b/src/app/Scenes/VanityURL/VanityURLEntity.tsx index 5e4c327c5d5..f7bd5884515 100644 --- a/src/app/Scenes/VanityURL/VanityURLEntity.tsx +++ b/src/app/Scenes/VanityURL/VanityURLEntity.tsx @@ -1,7 +1,7 @@ import { Flex, Spinner } from "@artsy/palette-mobile" import { VanityURLEntityQuery } from "__generated__/VanityURLEntityQuery.graphql" import { VanityURLEntity_fairOrPartner$data } from "__generated__/VanityURLEntity_fairOrPartner.graphql" -import { FairFragmentContainer, FairPlaceholder, FairQueryRenderer } from "app/Scenes/Fair/Fair" +import { Fair, FairPlaceholder, FairQueryRenderer } from "app/Scenes/Fair/Fair" import { PartnerContainer, PartnerSkeleton } from "app/Scenes/Partner/Partner" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" @@ -13,19 +13,20 @@ interface EntityProps { fairOrPartner: VanityURLEntity_fairOrPartner$data } -const VanityURLEntity: React.FC = ({ fairOrPartner, originalSlug }) => { +export const VanityURLEntity: React.FC = ({ fairOrPartner, originalSlug }) => { // Because `__typename` is not allowed in fragments anymore, we need to check for the existance of `slug` or `id` in the fragment // https://github.com/facebook/relay/commit/ed53bb095ddd494092819884cb4f46df94b45b79#diff-4e3d961b12253787bd61506608bc366be34ab276c09690de7df17203de7581e8 const isFair = fairOrPartner.__typename === "Fair" || "slug" in fairOrPartner const isPartner = fairOrPartner.__typename === "Partner" || "id" in fairOrPartner if (isFair) { - return - } else if (isPartner) { + return + } + if (isPartner) { return - } else { - return } + + return } const VanityURLEntityFragmentContainer = createFragmentContainer(VanityURLEntity, {