Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(EMI-2187): Refresh Bids screen every 10 secs #11407

Merged
merged 4 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 78 additions & 148 deletions src/app/Scenes/MyBids/MyBids.tests.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof createMockEnvironment>

beforeEach(() => {
env = createMockEnvironment()
})

const TestRenderer = () => (
<QueryRenderer<MyBidsTestsQuery>
environment={env}
query={graphql`
query MyBidsTestsQuery @relay_test_operation {
me {
...MyBids_me
}
const { renderWithRelay } = setupTestWrapper<MyBidsTestsQuery>({
Component: (props) => <MyBidsContainer isActiveTab me={props.me!} />,
query: graphql`
query MyBidsTestsQuery @relay_test_operation {
me {
...MyBids_me
}
`}
variables={{}}
render={({ props, error }) => {
if (Boolean(props?.me)) {
return <MyBidsContainer isActiveTab me={props!.me!} />
} else if (Boolean(error)) {
console.log(error)
}
}}
/>
)

const getWrapper = (mockResolvers = {}) => {
const tree = renderWithWrappersLEGACY(<TestRenderer />)
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(<MyBidsQueryRenderer />)
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",
}
44 changes: 37 additions & 7 deletions src/app/Scenes/MyBids/MyBids.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
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"
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 useAppState from "app/utils/useAppState"
import { useEffect, 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
Expand All @@ -22,26 +27,51 @@ export interface MyBidsProps {

const MyBids: React.FC<MyBidsProps> = (props) => {
const [isFetching, setIsFetching] = useState<boolean>(false)
const [appIsInForeground, setAppIsInForeground] = useState(true)
const [hasViewedScreen, setViewedScreen] = useState(false)
const { relay, isActiveTab, me } = props
const { isSmallScreen } = useScreenDimensions()

const refreshMyBids = (withSpinner = false) => {
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)
captureException(error, { tags: { source: "MyBids/index.tsx #refreshMyBids" } })
}
setIsFetching(false)
},
{ force: true }
)
}

useAppState({
onChange: (state) => {
setAppIsInForeground(state === "active")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: should "active" be constantized/enumized?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think since it's typed, it's okay to leave it as is?

},
})

useInterval(
() => {
refreshMyBids()
},
// starts when the tab is active, but only pauses when the app goes to the background
hasViewedScreen && appIsInForeground ? MY_BIDS_REFRESH_INTERVAL_MS : null
)

useEffect(() => {
if (isActiveTab) {
refreshMyBids()
}

if (isActiveTab && !hasViewedScreen) {
setViewedScreen(true)
}
}, [isActiveTab])

const active = me?.myBids?.active ?? []
Expand Down
6 changes: 5 additions & 1 deletion src/app/utils/useAppState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +32,10 @@ export default function useAppState({ onForeground, onBackground }: AppStateProp
onBackground()
}

if (onChange) {
onChange(nextAppState)
}

appState.current = nextAppState
}

Expand Down