From 82465dfcc1af8b0f4387bbed71d768fc2022edae Mon Sep 17 00:00:00 2001 From: notjackl3 Date: Tue, 7 Jul 2026 10:41:21 -0400 Subject: [PATCH] added name hiding --- src/__tests__/projection-mode.test.tsx | 123 ++++++++++++++++++++++ src/app/room/classChat/ChatHeader.tsx | 46 ++++++-- src/app/room/classChat/index.tsx | 40 ++++++- src/app/room/classChat/post/PostUtils.tsx | 16 ++- 4 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 src/__tests__/projection-mode.test.tsx diff --git a/src/__tests__/projection-mode.test.tsx b/src/__tests__/projection-mode.test.tsx new file mode 100644 index 0000000..c99cfe6 --- /dev/null +++ b/src/__tests__/projection-mode.test.tsx @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; + +afterEach(cleanup); + +import ChatHeader from "@/app/room/classChat/ChatHeader"; +import { stripAuthors } from "@/app/room/classChat/post/PostUtils"; +import type { Question, Role } from "@/utils/types"; + +// --------------------------------------------------------------------------- +// stripAuthors — projection mode anonymization +// --------------------------------------------------------------------------- + +function makeQuestion(overrides: Partial = {}): Question { + return { + id: "q1", + type: "question", + user: { id: "u1", utorid: "student1", username: "Student One", pfp: "", role: "STUDENT" }, + timestamp: "10:00 AM", + content: "What is a pointer?", + upvotes: 3, + isResolved: false, + isAnonymous: false, + replies: [ + { + id: "a1", + type: "comment", + user: { id: "u2", utorid: "ta1", username: "TA One", pfp: "", role: "TA" }, + timestamp: "10:01 AM", + content: "A memory address.", + upvotes: 1, + isAnonymous: false, + }, + ], + visibility: "PUBLIC", + ...overrides, + }; +} + +describe("stripAuthors", () => { + it("removes the author from questions and all replies", () => { + const stripped = stripAuthors([makeQuestion()]); + expect(stripped[0].user).toBeNull(); + expect(stripped[0].replies[0].user).toBeNull(); + }); + + it("strips revealed anonymous authors too", () => { + // Simulates a question whose author arrived via question:author:revealed + const revealed = makeQuestion({ + isAnonymous: true, + user: { id: "u9", utorid: "revealed1", username: "Revealed Name", pfp: "", role: "STUDENT" }, + }); + const stripped = stripAuthors([revealed]); + expect(stripped[0].user).toBeNull(); + }); + + it("preserves content, upvotes, and resolution state", () => { + const stripped = stripAuthors([makeQuestion({ isResolved: true })]); + expect(stripped[0].content).toBe("What is a pointer?"); + expect(stripped[0].upvotes).toBe(3); + expect(stripped[0].isResolved).toBe(true); + expect(stripped[0].replies[0].content).toBe("A memory address."); + }); + + it("does not mutate the original questions", () => { + const original = makeQuestion(); + stripAuthors([original]); + expect(original.user?.username).toBe("Student One"); + expect(original.replies[0].user?.username).toBe("TA One"); + }); +}); + +// --------------------------------------------------------------------------- +// ChatHeader — toggle visibility and behaviour +// --------------------------------------------------------------------------- + +function renderHeader(role: Role, projectionMode = true, onToggle = vi.fn()) { + render( + + ); + return onToggle; +} + +const TOGGLE_LABEL = "Toggle name visibility"; + +describe("ChatHeader projection mode toggle", () => { + it("is visible to professors", () => { + renderHeader("PROFESSOR"); + expect(screen.getByLabelText(TOGGLE_LABEL)).toBeDefined(); + }); + + it("is visible to TAs", () => { + renderHeader("TA"); + expect(screen.getByLabelText(TOGGLE_LABEL)).toBeDefined(); + }); + + it("is not rendered for students", () => { + renderHeader("STUDENT"); + expect(screen.queryByLabelText(TOGGLE_LABEL)).toBeNull(); + }); + + it("reflects the projection state in the tooltip", () => { + renderHeader("PROFESSOR", true); + expect(screen.getByLabelText(TOGGLE_LABEL).getAttribute("title")).toContain("Names hidden"); + cleanup(); + renderHeader("PROFESSOR", false); + expect(screen.getByLabelText(TOGGLE_LABEL).getAttribute("title")).toContain("Names visible"); + }); + + it("calls the toggle callback on click", () => { + const onToggle = renderHeader("PROFESSOR"); + fireEvent.click(screen.getByLabelText(TOGGLE_LABEL)); + expect(onToggle).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/room/classChat/ChatHeader.tsx b/src/app/room/classChat/ChatHeader.tsx index 003f5bf..1491b0a 100644 --- a/src/app/room/classChat/ChatHeader.tsx +++ b/src/app/room/classChat/ChatHeader.tsx @@ -2,7 +2,16 @@ import { Input } from "@/components/ui/input"; import { useContext, useState } from "react"; -import { PanelRightClose, Users, GraduationCap, Search, X, UserPlus } from "lucide-react"; +import { + Eye, + EyeOff, + PanelRightClose, + Users, + GraduationCap, + Search, + X, + UserPlus, +} from "lucide-react"; import ManageTAsModal from "./ManageTAsModal"; import { useMediaQuery } from "@/hooks/use-media-query"; import { SlideUpdateContext } from "../SlideUpdateContext"; @@ -12,6 +21,8 @@ interface ChatHeaderProps { role: Role; answerMode: "all" | "instructors_only"; onToggleAnswerMode: () => void; + projectionMode: boolean; + onToggleProjectionMode: () => void; searchQuery: string; onSearchChange: (value: string) => void; } @@ -23,7 +34,7 @@ function SlideToggle() { if (!isMDsize) { return ( + {/* Projection mode (hide names) toggle — instructors only */} + {(role === "PROFESSOR" || role === "TA") && ( + + )} + {/* Answer mode toggle — professors only */} {role === "PROFESSOR" && (