From 719ae26205f448414af1c794a4de7c1eedd753ff Mon Sep 17 00:00:00 2001
From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com>
Date: Sun, 31 May 2026 20:32:06 +0200
Subject: [PATCH 1/3] fix(feed): let users change poll votes
---
src/components/feed/PollDisplay.test.tsx | 65 ++++++++++++++++++++++++
src/components/feed/PollDisplay.tsx | 31 ++++++++---
2 files changed, 88 insertions(+), 8 deletions(-)
create mode 100644 src/components/feed/PollDisplay.test.tsx
diff --git a/src/components/feed/PollDisplay.test.tsx b/src/components/feed/PollDisplay.test.tsx
new file mode 100644
index 00000000..753a1499
--- /dev/null
+++ b/src/components/feed/PollDisplay.test.tsx
@@ -0,0 +1,65 @@
+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();
+ await userEvent.click(await screen.findByRole("button", { 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("button", { name: /Second/ })).toHaveAttribute(
+ "aria-pressed",
+ "true"
+ );
+ });
+ });
+});
diff --git a/src/components/feed/PollDisplay.tsx b/src/components/feed/PollDisplay.tsx
index 6a28212b..b918b449 100644
--- a/src/components/feed/PollDisplay.tsx
+++ b/src/components/feed/PollDisplay.tsx
@@ -80,14 +80,8 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
const isWinner = showResults && option.votes === Math.max(...options.map((o) => o.votes)) && option.votes > 0;
if (showResults) {
- return (
-
+ const result = (
+ <>
{/* Background bar */}
+ >
+ );
+ const className = cn(
+ "relative rounded-md border overflow-hidden transition-colors",
+ isSelected ? "border-primary" : "border-border"
+ );
+
+ return isLoggedIn ? (
+
+ ) : (
+
+ {result}
);
}
From 476e8b309345238944aa3d1fc720588ceb64b9e2 Mon Sep 17 00:00:00 2001
From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com>
Date: Sun, 31 May 2026 20:36:23 +0200
Subject: [PATCH 2/3] fix(feed): avoid redundant poll revotes
---
src/components/feed/PollDisplay.test.tsx | 15 ++++++++++++---
src/components/feed/PollDisplay.tsx | 11 ++++++++---
2 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/src/components/feed/PollDisplay.test.tsx b/src/components/feed/PollDisplay.test.tsx
index 753a1499..1a4bee33 100644
--- a/src/components/feed/PollDisplay.test.tsx
+++ b/src/components/feed/PollDisplay.test.tsx
@@ -45,7 +45,7 @@ describe("PollDisplay", () => {
.mockResolvedValueOnce(jsonResponse(changedResults));
render(
);
- await userEvent.click(await screen.findByRole("button", { name: /Second/ }));
+ await userEvent.click(await screen.findByRole("radio", { name: /Second/ }));
expect(fetch).toHaveBeenNthCalledWith(
2,
@@ -56,10 +56,19 @@ describe("PollDisplay", () => {
})
);
await waitFor(() => {
- expect(screen.getByRole("button", { name: /Second/ })).toHaveAttribute(
- "aria-pressed",
+ 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(
);
+ await userEvent.click(await screen.findByRole("radio", { name: /First/ }));
+
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/src/components/feed/PollDisplay.tsx b/src/components/feed/PollDisplay.tsx
index b918b449..16908414 100644
--- a/src/components/feed/PollDisplay.tsx
+++ b/src/components/feed/PollDisplay.tsx
@@ -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 {
@@ -74,7 +74,11 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
const showResults = hasVoted || !isLoggedIn;
return (
-
e.stopPropagation()}>
+
e.stopPropagation()}
+ role={showResults && isLoggedIn ? "radiogroup" : undefined}
+ >
{options.map((option) => {
const isSelected = userVote === option.id;
const isWinner = showResults && option.votes === Math.max(...options.map((o) => o.votes)) && option.votes > 0;
@@ -114,7 +118,8 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
className={cn(className, "block w-full text-left disabled:opacity-50")}
onClick={() => handleVote(option.id)}
disabled={voting}
- aria-pressed={isSelected}
+ role="radio"
+ aria-checked={isSelected}
>
{result}
From 03f68d7a558439bbcfce4b362e49cc88730f4e94 Mon Sep 17 00:00:00 2001
From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com>
Date: Sun, 31 May 2026 20:40:51 +0200
Subject: [PATCH 3/3] fix(feed): label poll choice groups
---
src/components/feed/PollDisplay.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/feed/PollDisplay.tsx b/src/components/feed/PollDisplay.tsx
index 16908414..276c3d47 100644
--- a/src/components/feed/PollDisplay.tsx
+++ b/src/components/feed/PollDisplay.tsx
@@ -78,6 +78,7 @@ export function PollDisplay({ postId, isLoggedIn }: PollDisplayProps) {
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;