Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -140,39 +140,40 @@ describe('VotingPopup', () => {
expect(onAddQuote).toHaveBeenCalled()
})

it('disables voting buttons when user has already voted', () => {
render(<VotingPopup {...defaultProps} hasVoted={true} userVoteType="up" />)
it('does not disable voting buttons when user has already voted, allowing retraction', async () => {
const onDeleteVote = jest.fn()
render(<VotingPopup {...defaultProps} hasVoted={true} userVoteType="up" onDeleteVote={onDeleteVote} />)

const upvoteButton = screen
.getByTestId('like-icon')
.closest('button')
expect(upvoteButton).toBeDisabled()
expect(upvoteButton).not.toBeDisabled()

if (upvoteButton) {
fireEvent.click(upvoteButton)
}
await waitFor(() => {
expect(onDeleteVote).toHaveBeenCalled()
})
})

it('shows tooltip when user has already voted', async () => {
render(
<VotingPopup
{...defaultProps}
hasVoted={true}
userVoteType="up"
/>,
)
it('allows vote switching when user has already voted', async () => {
const onDeleteVote = jest.fn()
render(<VotingPopup {...defaultProps} hasVoted={true} userVoteType="up" onDeleteVote={onDeleteVote} />)

const upvoteButton = screen
.getByTestId('like-icon')
const downvoteButton = screen
.getByTestId('dislike-icon')
.closest('button')
expect(upvoteButton).toBeInTheDocument()
expect(upvoteButton).toBeDisabled()
expect(downvoteButton).not.toBeDisabled()

// Tooltip content is rendered but may not be visible until hover
// In Radix UI, tooltips are rendered in a portal and may need user interaction
// For this test, we verify the button is disabled and the tooltip structure exists
const tooltipContent = screen.queryByText(/You have already upvoted this post/)
// Tooltip may not be visible until hover, but the structure should exist
// If not found, that's okay - tooltips in Radix UI require proper interaction
if (tooltipContent) {
expect(tooltipContent).toBeInTheDocument()
if (downvoteButton) {
fireEvent.click(downvoteButton)
}

// Wait for the opposite vote tags to expand
await waitFor(() => {
expect(screen.getByText('#false')).toBeInTheDocument()
})
})

it('calls onVote when downvote option is selected', async () => {
Expand Down Expand Up @@ -359,14 +360,16 @@ describe('VotingPopup', () => {
})
})

it('does not allow voting when hasVoted is true', async () => {
it('does not expand tags options when clicking user\'s active vote type', async () => {
const onVote = jest.fn()
const onDeleteVote = jest.fn()
render(
<VotingPopup
{...defaultProps}
hasVoted={true}
userVoteType="up"
onVote={onVote}
onDeleteVote={onDeleteVote}
/>,
)

Expand All @@ -384,19 +387,36 @@ describe('VotingPopup', () => {
expect(onVote).not.toHaveBeenCalled()
})

it('shows tooltip for downvote when user has downvoted', () => {
it('allows switching vote when user has downvoted and onDeleteVote is provided', async () => {
const onDeleteVote = jest.fn()
render(
<VotingPopup
{...defaultProps}
hasVoted={true}
userVoteType="down"
onDeleteVote={onDeleteVote}
/>,
)

const downvoteButton = screen
.getByTestId('dislike-icon')
const upvoteButton = screen
.getByTestId('like-icon')
.closest('button')
expect(downvoteButton).toBeDisabled()
expect(upvoteButton).not.toBeDisabled()
})

it('disables buttons and shows clear restriction state when user has voted but onDeleteVote is not provided', () => {
render(
<VotingPopup
{...defaultProps}
hasVoted={true}
userVoteType="up"
/>,
)

const upvoteButton = screen
.getByTestId('like-icon')
.closest('button')
expect(upvoteButton).toBeDisabled()
})

it('handles window resize for responsive layout', () => {
Expand Down Expand Up @@ -492,5 +512,34 @@ describe('VotingPopup', () => {
.closest('button')
expect(upvoteButton).toBeInTheDocument()
})

it('does not trigger onDeleteVote when clicking on showUpvoteTooltip/showDownvoteTooltip buttons since hasVoted is false', async () => {
const onDeleteVote = jest.fn()
const votedBy = [
{
userId: 'user123',
type: 'up' as const,
_id: 'vote1',
},
]

render(
<VotingPopup
{...defaultProps}
votedBy={votedBy}
hasVoted={false}
onDeleteVote={onDeleteVote}
/>,
)

const upvoteButton = screen
.getByTestId('like-icon')
.closest('button')
if (upvoteButton) {
fireEvent.click(upvoteButton)
}

expect(onDeleteVote).not.toHaveBeenCalled()
})
})

48 changes: 45 additions & 3 deletions quotevote-frontend/src/components/Post/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
APPROVE_POST,
REJECT_POST,
DELETE_POST,
DELETE_VOTE,
} from '@/graphql/mutations'
import {
GET_POST,
Expand Down Expand Up @@ -91,6 +92,14 @@ export default function Post({
],
})

const [removeVote] = useMutation(DELETE_VOTE, {
update() { refetchPost?.() },
refetchQueries: [
{ query: GET_TOP_POSTS, variables: { limit: 5, offset: 0, searchKey: '' } },
{ query: GET_POST, variables: { postId: _id } },
],
})

const [addComment] = useMutation(ADD_COMMENT, {
refetchQueries: [
{ query: GET_TOP_POSTS, variables: { limit: 5, offset: 0, searchKey: '' } },
Expand Down Expand Up @@ -168,18 +177,50 @@ export default function Post({
(v) => v.user?._id?.toString() === userIdStr && !(v as { deleted?: boolean }).deleted
)

const getUserVoteType = () => {
const getUserVote = () => {
if (!hasVoted) return null
const userVote = votedBy.find(
return votedBy.find(
(v) => v.user?._id?.toString() === userIdStr && !(v as { deleted?: boolean }).deleted
)
}

const getUserVoteType = () => {
const userVote = getUserVote()
return userVote ? userVote.type : null
}

const handleDeleteVote = async () => {
if (!ensureAuth()) return
const userVote = getUserVote()
if (!userVote) return
try {
await removeVote({
variables: {
voteId: userVote._id,
},
})
toast.success('Vote removed successfully')
} catch (err) {
toast.error(`Error removing vote: ${err instanceof Error ? err.message : 'Unknown'}`)
}
}

const handleVoting = async (obj: { type: VoteType; tags: VoteOption }) => {
if (!ensureAuth()) return
if (hasVoted) { toast('You have already voted on this post'); return }
const userVote = getUserVote()
try {
if (userVote) {
if (userVote.type === obj.type) {
await handleDeleteVote()
return
}
// Switch vote: synchronously delete existing vote first
await removeVote({
variables: {
voteId: userVote._id,
},
})
}
await addVote({
variables: {
vote: {
Expand Down Expand Up @@ -546,6 +587,7 @@ export default function Post({
selectedText={selection}
hasVoted={hasVoted}
userVoteType={getUserVoteType() as VoteType | null}
onDeleteVote={handleDeleteVote}
/>
</Suspense>
)}
Expand Down
Loading
Loading