Skip to content
Open
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
74 changes: 74 additions & 0 deletions src/components/feed/PollDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PollDisplay } from "./PollDisplay";

const initialResults = {
options: [
{ id: "option-1", text: "First", votes: 1, percentage: 100 },
{ id: "option-2", text: "Second", votes: 0, percentage: 0 },
],
total_votes: 1,
user_vote: "option-1",
};

const changedResults = {
options: [
{ id: "option-1", text: "First", votes: 0, percentage: 0 },
{ id: "option-2", text: "Second", votes: 1, percentage: 100 },
],
total_votes: 1,
user_vote: "option-2",
};

function jsonResponse(body: unknown) {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}

describe("PollDisplay", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});

afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});

it("lets an authenticated viewer change an existing vote", async () => {
vi.mocked(fetch)
.mockResolvedValueOnce(jsonResponse(initialResults))
.mockResolvedValueOnce(jsonResponse({ success: true }))
.mockResolvedValueOnce(jsonResponse(changedResults));

render(<PollDisplay postId="post-1" isLoggedIn />);
await userEvent.click(await screen.findByRole("radio", { name: /Second/ }));

expect(fetch).toHaveBeenNthCalledWith(
2,
"/api/posts/post-1/poll/vote",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ option_id: "option-2" }),
})
);
await waitFor(() => {
expect(screen.getByRole("radio", { name: /Second/ })).toHaveAttribute(
"aria-checked",
"true"
);
});
});

it("does not submit the already-selected option again", async () => {
vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(initialResults));

render(<PollDisplay postId="post-1" isLoggedIn />);
await userEvent.click(await screen.findByRole("radio", { name: /First/ }));

expect(fetch).toHaveBeenCalledTimes(1);
});
});
41 changes: 31 additions & 10 deletions src/components/feed/PollDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
}, [postId]);

const handleVote = async (optionId: string) => {
if (!isLoggedIn || voting) return;
if (!isLoggedIn || voting || optionId === userVote) return;
setVoting(true);

try {
Expand Down Expand Up @@ -74,20 +74,19 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
const showResults = hasVoted || !isLoggedIn;

return (
<div className="mt-3 space-y-2" onClick={(e) => e.stopPropagation()}>
<div
className="mt-3 space-y-2"
onClick={(e) => e.stopPropagation()}
role={showResults && isLoggedIn ? "radiogroup" : undefined}
aria-label={showResults && isLoggedIn ? "Poll choices" : undefined}
>
{options.map((option) => {
const isSelected = userVote === option.id;
const isWinner = showResults && option.votes === Math.max(...options.map((o) => o.votes)) && option.votes > 0;

if (showResults) {
return (
<div
key={option.id}
className={cn(
"relative rounded-md border overflow-hidden transition-colors",
isSelected ? "border-primary" : "border-border"
)}
>
const result = (
<>
{/* Background bar */}
<div
className={cn(
Expand All @@ -106,6 +105,28 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
{option.percentage}%
</span>
</div>
</>
);
const className = cn(
"relative rounded-md border overflow-hidden transition-colors",
isSelected ? "border-primary" : "border-border"
);

return isLoggedIn ? (
<button
key={option.id}
type="button"
className={cn(className, "block w-full text-left disabled:opacity-50")}
onClick={() => handleVote(option.id)}
disabled={voting}
role="radio"
aria-checked={isSelected}
>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
{result}
</button>
) : (
<div key={option.id} className={className}>
{result}
</div>
);
}
Expand Down