diff --git a/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.test.tsx b/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.test.tsx new file mode 100644 index 0000000000..b2bcf5530e --- /dev/null +++ b/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.test.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import userEvent from '@testing-library/user-event' +import { render, screen, waitFor } from '@testing-library/react' +import ChallengeRegisterButton, { + ChallengeRegisterButtonProps, +} from './ChallengeRegisterButton' +import { createWrapper } from '../../testutils/TestingLibraryUtils' +import mockProject from '../../mocks/entity/mockProject' +import SynapseClient from '../../synapse-client' +import { mockUserProfileData } from '../../mocks/user/mock_user_profile' +import { + mockChallenge, + mockChallengeTeamMember, +} from '../../mocks/challenge/mockChallenge' +import { MOCK_TEAM_ID } from '../../mocks/team/mockTeam' +import { SynapseClientError } from '../../utils' + +const mockOnError = jest.fn() +const mockOnJoinClick = jest.fn() +const mockOnLeaveClick = jest.fn() + +jest + .spyOn(SynapseClient, 'getUserProfile') + .mockResolvedValue(mockUserProfileData) +jest.spyOn(SynapseClient, 'getEntityChallenge').mockResolvedValue(mockChallenge) +const mockGetIsUserMemberOfTeam = jest.spyOn( + SynapseClient, + 'getIsUserMemberOfTeam', +) +const mockGetSubmissionTeams = jest.spyOn(SynapseClient, 'getSubmissionTeams') + +function renderComponent() { + const props: ChallengeRegisterButtonProps = { + projectId: mockProject.id, + onError: mockOnError, + onJoinClick: mockOnJoinClick, + onLeaveClick: mockOnLeaveClick, + } + + const user = userEvent.setup() + const component = render(, { + wrapper: createWrapper(), + }) + + return { user, component } +} + +describe('ChallengeRegisterButton', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('Prompts to register when not a member of the participant team or any submission teams', async () => { + mockGetIsUserMemberOfTeam.mockResolvedValue(null) + mockGetSubmissionTeams.mockResolvedValue({ + results: [], + totalNumberOfResults: 0, + }) + + const { user } = renderComponent() + + const button = await screen.findByRole('button', { + name: 'Register for this Challenge', + }) + + await user.click(button) + + expect(mockOnJoinClick).toHaveBeenCalledTimes(1) + expect(mockOnLeaveClick).not.toHaveBeenCalled() + }) + + it('Prompts to register when not a member of the participant team', async () => { + mockGetIsUserMemberOfTeam.mockResolvedValue(null) + mockGetSubmissionTeams.mockResolvedValue({ + results: [String(MOCK_TEAM_ID)], + totalNumberOfResults: 1, + }) + + const { user } = renderComponent() + + const button = await screen.findByRole('button', { + name: 'Register for this Challenge', + }) + await user.click(button) + + expect(mockOnJoinClick).toHaveBeenCalledTimes(1) + expect(mockOnLeaveClick).not.toHaveBeenCalled() + }) + + it('Prompts to register when not a member of a submission team', async () => { + mockGetIsUserMemberOfTeam.mockResolvedValue(mockChallengeTeamMember) + mockGetSubmissionTeams.mockResolvedValue({ + results: [], + totalNumberOfResults: 0, + }) + + const { user } = renderComponent() + + const button = await screen.findByRole('button', { + name: 'Register for this Challenge', + }) + + await user.click(button) + + expect(mockOnJoinClick).toHaveBeenCalledTimes(1) + expect(mockOnLeaveClick).not.toHaveBeenCalled() + }) + + it('Prompts to leave when on both participant and submission teams', async () => { + mockGetIsUserMemberOfTeam.mockResolvedValue(mockChallengeTeamMember) + mockGetSubmissionTeams.mockResolvedValue({ + results: [String(MOCK_TEAM_ID)], + totalNumberOfResults: 1, + }) + + const { user } = renderComponent() + const button = await screen.findByRole('button', { + name: 'Leave Challenge', + }) + + await user.click(button) + + expect(mockOnLeaveClick).toHaveBeenCalledTimes(1) + expect(mockOnJoinClick).not.toHaveBeenCalled() + }) + + it('Invokes the callback on error', async () => { + const error = new SynapseClientError( + 500, + 'Simulated error in test', + expect.getState().currentTestName!, + ) + mockGetIsUserMemberOfTeam.mockRejectedValue(error) + mockGetSubmissionTeams.mockResolvedValue({ + results: [String(MOCK_TEAM_ID)], + totalNumberOfResults: 1, + }) + + renderComponent() + + await waitFor(() => { + expect(mockOnError).toHaveBeenCalledWith(error) + }) + }) +}) diff --git a/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.tsx b/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.tsx index 659b873509..b3ccdf4920 100644 --- a/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.tsx +++ b/packages/synapse-react-client/src/components/ChallengeRegisterButton/ChallengeRegisterButton.tsx @@ -1,102 +1,87 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import ExitToAppIcon from '@mui/icons-material/ExitToApp' -import { Box } from '@mui/system' +import { Box } from '@mui/material' import SpinnerButton from '../SpinnerButton/SpinnerButton' -import { useSynapseContext } from '../../utils' import { useGetCurrentUserProfile, useGetEntityChallenge, + useGetIsUserMemberOfTeam, useGetUserSubmissionTeams, } from '../../synapse-queries' -import { Challenge, PaginatedIds } from '@sage-bionetworks/synapse-types' -import { useGetIsUserMemberOfTeam } from '../../synapse-queries/team/useTeamMembers' -import { SynapseClientError } from '../../utils/SynapseClientError' +import { SynapseClientError, useSynapseContext } from '../../utils' export interface ChallengeRegisterButtonProps { projectId: string - onChallengeError?: (error: SynapseClientError) => void + onError?: (error: SynapseClientError) => void onJoinClick?: () => void onLeaveClick?: () => void } -const EMPTY_ID = '' - const ChallengeRegisterButton = ({ projectId, - onChallengeError, + onError, onJoinClick, onLeaveClick, }: ChallengeRegisterButtonProps) => { const { accessToken } = useSynapseContext() - const [challenge, setChallenge] = useState() - const { data: userProfile } = useGetCurrentUserProfile() - const [isRegistered, setIsRegistered] = useState(false) - const [hasSubmissionTeam, setHasSubmissionTeam] = useState(false) - const [loading, setLoading] = useState(true) - const [requestError, setRequestError] = useState() - - useEffect(() => { - if (requestError && onChallengeError) onChallengeError(requestError) - }, [requestError, onChallengeError]) + const isLoggedIn = !!accessToken + const { data: userProfile } = useGetCurrentUserProfile({ + enabled: isLoggedIn, + }) - useGetEntityChallenge(projectId, { - enabled: !!accessToken && !challenge, - onSettled: (data, error) => { - if (data) { - setChallenge(data) - } - if (error) { - setLoading(false) - setRequestError(error) - } - }, + const { + data: challenge, + isLoading: isLoadingChallenge, + error: getChallengeError, + } = useGetEntityChallenge(projectId, { + enabled: isLoggedIn, }) // Verify that user is a member of the participant team - useGetIsUserMemberOfTeam( - challenge?.participantTeamId ?? EMPTY_ID, - userProfile?.ownerId ?? EMPTY_ID, + const { + data: teamMembership, + isLoading: isLoadingTeamMembership, + error: getTeamMembershipError, + } = useGetIsUserMemberOfTeam( + challenge?.participantTeamId!, + userProfile?.ownerId!, { enabled: !!challenge && !!userProfile, - onSettled: (data, error) => { - if (data === null) { - // User is not a member of the participant team - setIsRegistered(false) - setLoading(false) - } - if (data !== null) { - // User is a member of the participant team, continue - setIsRegistered(true) - } - if (error) { - // Could not determine if user is a member of the participant team - setLoading(false) - setRequestError(error) - } - }, }, ) - useGetUserSubmissionTeams(challenge?.id ?? '0', 20, 0, { - enabled: !!challenge && !!accessToken, - onSettled: (data: PaginatedIds | undefined, error) => { - if (data) { - setHasSubmissionTeam(data.results.length > 0) - } - if (error) { - setRequestError(error) - } - setLoading(false) - }, + const isRegistered = Boolean(teamMembership) + + const { + data: userSubmissionTeams, + error: getSubmissionTeamsError, + isLoading: isLoadingSubmissionTeams, + } = useGetUserSubmissionTeams(challenge?.id!, 20, 0, { + enabled: !!challenge && isLoggedIn, }) - if (loading) { + const hasSubmissionTeam = + userSubmissionTeams && userSubmissionTeams.results.length > 0 + const isMemberOfBothParticipantAndSubmissionTeams = + isRegistered && hasSubmissionTeam + + const error = + getChallengeError || getTeamMembershipError || getSubmissionTeamsError + + useEffect(() => { + if (error && onError) onError(error) + }, [error, onError]) + + const isLoading = + isLoadingChallenge || isLoadingTeamMembership || isLoadingSubmissionTeams + + if (isLoading) { return Loading... } return ( - {(!isRegistered || !hasSubmissionTeam) && ( + {!isMemberOfBothParticipantAndSubmissionTeams && ( )} - {isRegistered && hasSubmissionTeam && ( + {isMemberOfBothParticipantAndSubmissionTeams && (