From 06137edc5b804b4732d57a196620c10b0e2c7093 Mon Sep 17 00:00:00 2001 From: Sultan Date: Thu, 16 Jan 2025 16:38:41 +0100 Subject: [PATCH 1/4] feat(EMI-2187): Refresh Bids screen every 10 secs --- src/app/Scenes/MyBids/MyBids.tsx | 45 ++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/app/Scenes/MyBids/MyBids.tsx b/src/app/Scenes/MyBids/MyBids.tsx index 1f8b1260c83..7e7b07a0f2b 100644 --- a/src/app/Scenes/MyBids/MyBids.tsx +++ b/src/app/Scenes/MyBids/MyBids.tsx @@ -7,13 +7,17 @@ import { useScreenDimensions } from "app/utils/hooks" import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { ProvideScreenTrackingWithCohesionSchema } from "app/utils/track" import { screen } from "app/utils/track/helpers" -import { useEffect, useState } from "react" +import useAppState from "app/utils/useAppState" +import { useEffect, useRef, useState } from "react" import { RefreshControl } from "react-native" import { createRefetchContainer, graphql, QueryRenderer, RelayRefetchProp } from "react-relay" +import { useInterval } from "react-use" import { MyBidsPlaceholder, SaleCardFragmentContainer } from "./Components" import { LotStatusListItemContainer } from "./Components/LotStatusListItem" import { NoBids } from "./Components/NoBids" +const MY_BIDS_REFRESH_INTERVAL_MS = 10 * 1000 + export interface MyBidsProps { me: MyBids_me$data isActiveTab: boolean @@ -22,6 +26,8 @@ export interface MyBidsProps { const MyBids: React.FC = (props) => { const [isFetching, setIsFetching] = useState(false) + const appIsInForeground = useRef(false) + const hasViewedScreen = useRef(false) const { relay, isActiveTab, me } = props const { isSmallScreen } = useScreenDimensions() @@ -29,19 +35,42 @@ const MyBids: React.FC = (props) => { if (withSpinner) { setIsFetching(true) } - relay.refetch({}, null, (error) => { - if (error) { - console.error("MyBids/index.tsx #refreshMyBids", error.message) - // FIXME: Handle error - } - setIsFetching(false) - }) + relay.refetch( + {}, + null, + (error) => { + if (error) { + console.error("MyBids/index.tsx #refreshMyBids", error.message) + // FIXME: Handle error + } + setIsFetching(false) + }, + { force: true } + ) } + useAppState({ + onChange: (state) => { + appIsInForeground.current = state === "active" + }, + }) + + useInterval( + () => { + refreshMyBids() + }, + // starts when the tab is active, but only pauses when the app goes to the background + hasViewedScreen.current && appIsInForeground.current ? null : MY_BIDS_REFRESH_INTERVAL_MS + ) + useEffect(() => { if (isActiveTab) { refreshMyBids() } + + if (!hasViewedScreen.current) { + hasViewedScreen.current = true + } }, [isActiveTab]) const active = me?.myBids?.active ?? [] From 937474544a496ccd1b7fc227616d67274e52a27a Mon Sep 17 00:00:00 2001 From: Sultan Date: Thu, 16 Jan 2025 16:53:12 +0100 Subject: [PATCH 2/4] refactor tests --- src/app/Scenes/MyBids/MyBids.tests.tsx | 226 +++++++++---------------- 1 file changed, 78 insertions(+), 148 deletions(-) diff --git a/src/app/Scenes/MyBids/MyBids.tests.tsx b/src/app/Scenes/MyBids/MyBids.tests.tsx index 65e951523fa..2802ce3e4f6 100644 --- a/src/app/Scenes/MyBids/MyBids.tests.tsx +++ b/src/app/Scenes/MyBids/MyBids.tests.tsx @@ -1,171 +1,101 @@ +import { screen } from "@testing-library/react-native" import { MyBidsTestsQuery } from "__generated__/MyBidsTestsQuery.graphql" -import { PlaceholderText } from "app/utils/placeholders" -import { extractText } from "app/utils/tests/extractText" -import { renderWithWrappersLEGACY } from "app/utils/tests/renderWithWrappers" -import { graphql, QueryRenderer } from "react-relay" -import { act, ReactTestInstance } from "react-test-renderer" -import { createMockEnvironment, MockPayloadGenerator } from "relay-test-utils" -import { ActiveLotStanding } from "./Components/ActiveLotStanding" -import { ClosedLotStanding } from "./Components/ClosedLotStanding" -import { WatchedLot } from "./Components/WatchedLot" -import { MyBidsContainer, MyBidsQueryRenderer } from "./MyBids" - -const closedSectionLots = (root: ReactTestInstance): ReactTestInstance[] => { - const closedSection = root.findByProps({ testID: "closed-section" }) - return closedSection.findAllByType(ClosedLotStanding) -} - -const activeSectionLots = (root: ReactTestInstance): ReactTestInstance[] => { - const activeSection = root.findByProps({ testID: "active-section" }) - const ActiveLotStandings = activeSection.findAll((instance: ReactTestInstance) => { - return [ActiveLotStanding, ClosedLotStanding, WatchedLot].includes((instance as any).type) - }) - - return ActiveLotStandings -} +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" +import { MyBidsContainer } from "./MyBids" describe("My Bids", () => { - let env: ReturnType - - beforeEach(() => { - env = createMockEnvironment() - }) - - const TestRenderer = () => ( - - environment={env} - query={graphql` - query MyBidsTestsQuery @relay_test_operation { - me { - ...MyBids_me - } + const { renderWithRelay } = setupTestWrapper({ + Component: (props) => , + query: graphql` + query MyBidsTestsQuery @relay_test_operation { + me { + ...MyBids_me } - `} - variables={{}} - render={({ props, error }) => { - if (Boolean(props?.me)) { - return - } else if (Boolean(error)) { - console.log(error) - } - }} - /> - ) - - const getWrapper = (mockResolvers = {}) => { - const tree = renderWithWrappersLEGACY() - act(() => { - env.mock.resolveMostRecentOperation((operation) => - MockPayloadGenerator.generate(operation, mockResolvers) - ) - }) - return tree - } - - describe("MyBidsQueryRenderer loading state", () => { - it("shows placeholder until the operation resolves", () => { - const tree = renderWithWrappersLEGACY() - expect(tree.root.findAllByType(PlaceholderText).length).toBeGreaterThan(0) - }) - }) - - it("renders without throwing an error", () => { - getWrapper() + } + `, }) it("renders a lot standing from a closed sale in the closed section", () => { - const wrapper = getWrapper({ - Me: () => { - const sale = { - internalID: "sale1", - endAt: "2020-08-13T16:00:00+00:00", - timeZone: "America/New_York", - status: "closed", - liveStartAt: "2020-08-10T16:00:00+00:00", - } - - const saleArtworks = [ - { - isHighestBidder: true, - internalID: "saleartworks1", - lotState: { - soldStatus: "Passed", + renderWithRelay({ + Me: () => ({ + myBids: { + closed: [ + { + sale: mockClosedSale, + saleArtworks: [ + { + isHighestBidder: true, + internalID: "saleartworks1", + lotState: { soldStatus: "Passed" }, + sale: mockClosedSale, + artwork: { artistNames: "Artists" }, + }, + ], }, - sale, - }, - ] - - return { - myBids: { - closed: [{ sale, saleArtworks }], - }, - } - }, + ], + }, + }), }) - const ClosedLotStandings = closedSectionLots(wrapper.root).map(extractText) - expect(ClosedLotStandings[0]).toContain("artistNames") - expect(ClosedLotStandings[0]).toContain("Passed") - expect(ClosedLotStandings[0]).toContain("Closed Aug 13") + expect(screen.getByText("Artists")).toBeOnTheScreen() + expect(screen.getByText("Passed")).toBeOnTheScreen() + expect(screen.getByText("Closed Aug 13")).toBeOnTheScreen() }) it("renders a completed lot in an ongoing live sale in the 'active' section", () => { - const wrapper = getWrapper({ - Me: () => { - const sale = { - internalID: "sale-id", - status: "open", - liveStartAt: "2020-08-13T16:00:00+00:00", - } - - const saleArtworks = [ - { - internalID: "saleartworks1", - lotState: { - soldStatus: "Passed", - reserveStatus: "ReserveNotMet", - }, - sale, - }, - ] - return { - myBids: { - active: [{ sale, saleArtworks }], - }, - } - }, - }) - - const ActiveLotStandings = activeSectionLots(wrapper.root).map(extractText) - expect(ActiveLotStandings[0]).toContain("Passed") - }) - - it("renders the empty view when there are no lots to show", () => { - const wrapper = getWrapper({ + renderWithRelay({ Me: () => ({ myBids: { - active: [], - closed: [], + active: [ + { + sale: mockActiveSale, + saleArtworks: [ + { + internalID: "saleartworks1", + lotState: { soldStatus: "Passed", reserveStatus: "ReserveNotMet" }, + sale: mockActiveSale, + artwork: { artistNames: "Artists" }, + }, + ], + }, + ], }, }), }) - expect(extractText(wrapper.root)).toContain("Discover works for you at auction") - expect(extractText(wrapper.root)).toContain( - "Browse and bid in auctions around the world, from online-only sales to benefit auctions—all in the Artsy app" - ) + expect(screen.getByText("Artists")).toBeOnTheScreen() + expect(screen.getByText("Passed")).toBeOnTheScreen() }) - it("tells a user they have no bids on a registered sale", () => { - const wrapper = getWrapper({ - Me: () => { - return { - myBids: { - active: [{ saleArtworks: [] }], - }, - } - }, - }) - expect(extractText(wrapper.root)).toContain("You haven't placed any bids on this sale") + it("renders the empty state when there are no lots to show", () => { + renderWithRelay({ Me: () => ({ myBids: { active: [], closed: [] } }) }) + + expect(screen.getByText("Discover works for you at auction.")).toBeOnTheScreen() + expect( + screen.getByText( + "Browse and bid in auctions around the world, from online-only sales to benefit auctions—all in the Artsy app." + ) + ).toBeOnTheScreen() + }) + + it("renders no bids message on a registered sale", () => { + renderWithRelay({ Me: () => ({ myBids: { active: [{ saleArtworks: [] }] } }) }) + + expect(screen.getByText("You haven't placed any bids on this sale")).toBeOnTheScreen() }) }) + +const mockClosedSale = { + internalID: "sale1", + endAt: "2020-08-13T16:00:00+00:00", + timeZone: "America/New_York", + status: "closed", + liveStartAt: "2020-08-10T16:00:00+00:00", +} + +const mockActiveSale = { + internalID: "sale-id", + status: "open", + liveStartAt: "2020-08-13T16:00:00+00:00", +} From 01b3af3c03b44b597b9a6b4a41fb5b7b7bfcb6c0 Mon Sep 17 00:00:00 2001 From: Sultan Date: Fri, 17 Jan 2025 12:10:12 +0100 Subject: [PATCH 3/4] update logic --- src/app/Scenes/MyBids/MyBids.tsx | 14 +++++++------- src/app/utils/useAppState.tsx | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/Scenes/MyBids/MyBids.tsx b/src/app/Scenes/MyBids/MyBids.tsx index 7e7b07a0f2b..d469c39ad55 100644 --- a/src/app/Scenes/MyBids/MyBids.tsx +++ b/src/app/Scenes/MyBids/MyBids.tsx @@ -8,7 +8,7 @@ import { renderWithPlaceholder } from "app/utils/renderWithPlaceholder" import { ProvideScreenTrackingWithCohesionSchema } from "app/utils/track" import { screen } from "app/utils/track/helpers" import useAppState from "app/utils/useAppState" -import { useEffect, useRef, useState } from "react" +import { useEffect, useState } from "react" import { RefreshControl } from "react-native" import { createRefetchContainer, graphql, QueryRenderer, RelayRefetchProp } from "react-relay" import { useInterval } from "react-use" @@ -26,8 +26,8 @@ export interface MyBidsProps { const MyBids: React.FC = (props) => { const [isFetching, setIsFetching] = useState(false) - const appIsInForeground = useRef(false) - const hasViewedScreen = useRef(false) + const [appIsInForeground, setAppIsInForeground] = useState(true) + const [hasViewedScreen, setViewedScreen] = useState(false) const { relay, isActiveTab, me } = props const { isSmallScreen } = useScreenDimensions() @@ -51,7 +51,7 @@ const MyBids: React.FC = (props) => { useAppState({ onChange: (state) => { - appIsInForeground.current = state === "active" + setAppIsInForeground(state === "active") }, }) @@ -60,7 +60,7 @@ const MyBids: React.FC = (props) => { refreshMyBids() }, // starts when the tab is active, but only pauses when the app goes to the background - hasViewedScreen.current && appIsInForeground.current ? null : MY_BIDS_REFRESH_INTERVAL_MS + hasViewedScreen && appIsInForeground ? MY_BIDS_REFRESH_INTERVAL_MS : null ) useEffect(() => { @@ -68,8 +68,8 @@ const MyBids: React.FC = (props) => { refreshMyBids() } - if (!hasViewedScreen.current) { - hasViewedScreen.current = true + if (isActiveTab && !hasViewedScreen) { + setViewedScreen(true) } }, [isActiveTab]) diff --git a/src/app/utils/useAppState.tsx b/src/app/utils/useAppState.tsx index 606034525cc..3c89efd22d9 100644 --- a/src/app/utils/useAppState.tsx +++ b/src/app/utils/useAppState.tsx @@ -7,7 +7,7 @@ export interface AppStateProps { onBackground?: () => void } -export default function useAppState({ onForeground, onBackground }: AppStateProps) { +export default function useAppState({ onForeground, onBackground, onChange }: AppStateProps) { /** * App States * active - The app is running in the foreground @@ -32,6 +32,10 @@ export default function useAppState({ onForeground, onBackground }: AppStateProp onBackground() } + if (onChange) { + onChange(nextAppState) + } + appState.current = nextAppState } From 8f4bb1bf9619fc86f46375a1bcc0a1a8ef61322d Mon Sep 17 00:00:00 2001 From: Sultan Date: Mon, 27 Jan 2025 13:40:23 +0100 Subject: [PATCH 4/4] capture myBids errors in Sentry --- src/app/Scenes/MyBids/MyBids.tests.tsx | 2 +- src/app/Scenes/MyBids/MyBids.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/Scenes/MyBids/MyBids.tests.tsx b/src/app/Scenes/MyBids/MyBids.tests.tsx index 2802ce3e4f6..c76d5a95764 100644 --- a/src/app/Scenes/MyBids/MyBids.tests.tsx +++ b/src/app/Scenes/MyBids/MyBids.tests.tsx @@ -6,7 +6,7 @@ import { MyBidsContainer } from "./MyBids" describe("My Bids", () => { const { renderWithRelay } = setupTestWrapper({ - Component: (props) => , + Component: (props) => , query: graphql` query MyBidsTestsQuery @relay_test_operation { me { diff --git a/src/app/Scenes/MyBids/MyBids.tsx b/src/app/Scenes/MyBids/MyBids.tsx index d469c39ad55..e573f90cf69 100644 --- a/src/app/Scenes/MyBids/MyBids.tsx +++ b/src/app/Scenes/MyBids/MyBids.tsx @@ -1,5 +1,6 @@ import { OwnerType } from "@artsy/cohesion" import { Spacer, Flex, Text, Separator, Join, Tabs } from "@artsy/palette-mobile" +import { captureException } from "@sentry/react-native" import { MyBidsQuery } from "__generated__/MyBidsQuery.graphql" import { MyBids_me$data } from "__generated__/MyBids_me.graphql" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" @@ -41,7 +42,7 @@ const MyBids: React.FC = (props) => { (error) => { if (error) { console.error("MyBids/index.tsx #refreshMyBids", error.message) - // FIXME: Handle error + captureException(error, { tags: { source: "MyBids/index.tsx #refreshMyBids" } }) } setIsFetching(false) },