From 3b79edbd2d58d9c3279b6064c94b527a5b6bda28 Mon Sep 17 00:00:00 2001 From: Nour Abosaif Date: Sun, 3 May 2026 05:46:09 +0300 Subject: [PATCH] Revert "M12 library" --- src/components/Premium/HeroSection.test.tsx | 34 ++ src/components/layout/AuthMainLayout.test.tsx | 84 ++++ src/components/library/useLibraryData.ts | 47 +-- .../test/PlaylistSidebarAndComponent.test.tsx | 188 +++++++++ src/pages/admin/tests/AdminUsersPage.test.tsx | 371 ++++++++++++++++++ .../upload/tests/UploadDetailsForm.test.tsx | 112 +++--- src/pages/you/albums/AlbumSlugPage.tsx | 2 +- src/pages/you/library/LibraryPage.test.tsx | 21 +- 8 files changed, 777 insertions(+), 82 deletions(-) create mode 100644 src/components/Premium/HeroSection.test.tsx create mode 100644 src/components/layout/AuthMainLayout.test.tsx create mode 100644 src/components/playlist/test/PlaylistSidebarAndComponent.test.tsx create mode 100644 src/pages/admin/tests/AdminUsersPage.test.tsx diff --git a/src/components/Premium/HeroSection.test.tsx b/src/components/Premium/HeroSection.test.tsx new file mode 100644 index 00000000..f007ce4a --- /dev/null +++ b/src/components/Premium/HeroSection.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import HeroSection from "./HeroSection"; + +describe("HeroSection", () => { + it("renders the hero copy, CTA, and plan link", () => { + render( + , + ); + + expect(screen.getByRole("heading", { name: "Reach more listeners." })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Get Premium" })).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "See plan" })).toHaveAttribute( + "href", + "#pricing-cards", + ); + expect(screen.getByText("Grow your audience")).toBeInTheDocument(); + expect(screen.getByText("Know your audience")).toBeInTheDocument(); + expect(screen.getByText("Upload unlimited tracks")).toBeInTheDocument(); + expect(screen.getByText("Distribution is included")).toBeInTheDocument(); + }); + + it("shows the loading label when the CTA is starting", () => { + render( + , + ); + + expect(screen.getByRole("button", { name: /loading/i })).toBeDisabled(); + }); +}); diff --git a/src/components/layout/AuthMainLayout.test.tsx b/src/components/layout/AuthMainLayout.test.tsx new file mode 100644 index 00000000..f07d0a99 --- /dev/null +++ b/src/components/layout/AuthMainLayout.test.tsx @@ -0,0 +1,84 @@ +import { render, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import AuthMainLayout from "@/components/layout/AuthMainLayout"; + +const mockSetUser = vi.fn(); +const mockHydrateFromApi = vi.fn(); +const mockGetMe = vi.fn(); +const mockGetMySubscription = vi.fn(); + +vi.mock("@/stores/auth.store", () => ({ + useAuthStore: (selector: (state: { isAuthenticated: boolean; setUser: typeof mockSetUser }) => unknown) => + selector({ isAuthenticated: true, setUser: mockSetUser }), +})); + +vi.mock("@/stores/likes.store", () => ({ + useLikesStore: (selector: (state: { hydrateFromApi: typeof mockHydrateFromApi }) => unknown) => + selector({ hydrateFromApi: mockHydrateFromApi }), +})); + +vi.mock("@/services/auth.service", () => ({ + getMe: () => mockGetMe(), + normalizeDateOfBirth: (value: string | null | undefined) => value ?? null, +})); + +vi.mock("@/services/api/upload/subscription.service", () => ({ + getMySubscription: () => mockGetMySubscription(), +})); + +vi.mock("@/components/layout/MainNavbar", () => ({ + default: () =>
, +})); + +vi.mock("@/components/player/StickyPlayer", () => ({ + default: () =>
, +})); + +describe("AuthMainLayout", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetMe.mockResolvedValue({ + data: { + id: "u1", + username: "me", + display_name: "Me", + first_name: "Test", + last_name: "User", + bio: "", + email: "me@example.com", + role: "listener", + profile_picture: null, + cover_photo: null, + city: null, + country: null, + following_ids: [], + followers_ids: [], + date_of_birth: "2001-02-02", + gender: "female", + }, + }); + mockGetMySubscription.mockResolvedValue({ + data: { user_subscription_id: null }, + }); + }); + + it("hydrates auth store with basic profile fields from the backend", async () => { + render( + + + , + ); + + await waitFor(() => { + expect(mockSetUser).toHaveBeenCalledWith( + expect.objectContaining({ + date_of_birth: "2001-02-02", + gender: "female", + }), + ); + }); + + expect(mockHydrateFromApi).toHaveBeenCalled(); + }); +}); diff --git a/src/components/library/useLibraryData.ts b/src/components/library/useLibraryData.ts index 05261e51..66381ce8 100644 --- a/src/components/library/useLibraryData.ts +++ b/src/components/library/useLibraryData.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { getRecentlyPlayed } from "@/services/api/discover.service"; import { mapRecentlyPlayedEntry } from "@/services/api/discover.mapper"; -import { getMyPlaylists as getMyPlaylistsLib, getMyFollowing } from "@/services/api/library.service"; +import { getMyFollowing } from "@/services/api/library.service"; import { getUserById } from "@/services/user.service"; import { getMyPlaylists as getMyPlaylistsApi, @@ -15,7 +15,6 @@ import { useLikesStore } from "@/stores/likes.store"; import { useHistoryStore } from "@/stores/history.store"; import { useAuthStore } from "@/stores/auth.store"; import { mapPlaylistToCard, mapAlbumToCard, mapFollowingToUser } from "./library.mappers"; -import { mockRecentlyPlayedTracks } from "@/services/mocks/discover"; import type { FilterOption } from "./PlaylistFilterDropdown"; const isAlbum = (p: Playlist) => p.is_album_view || p.subtype === "album"; @@ -28,13 +27,13 @@ export function useLibraryData() { const [playlistFilter, setPlaylistFilter] = useState("All"); const { - likedTracks = [], - likedStations = [], - likedPlaylists = [], - likedAlbums: storeLikedAlbums = [], - likedRadioTracks = [], - likedMixes = [], - likedGenres = [], + likedTracks, + likedStations, + likedPlaylists, + likedAlbums: storeLikedAlbums, + likedRadioTracks, + likedMixes, + likedGenres, } = useLikesStore(); const { user } = useAuthStore(); const { entries } = useHistoryStore(); @@ -62,21 +61,18 @@ export function useLibraryData() { useEffect(() => { const displayName = user?.displayName ?? user?.username ?? ""; - getMyPlaylistsLib() - .then((items) => - setPlaylists( - items.map((p) => mapPlaylistToCard(p as unknown as Playlist, displayName || p.owner_user_id)), - ), - ) - .catch(() => setPlaylists([])); - }, []); - useEffect(() => { Promise.all([ getMyPlaylistsApi({ limit: 50 }), getLikedPlaylists({ limit: 50 }), ]) .then(([created, liked]) => { + setPlaylists( + created.data.items + .filter((p) => !isAlbum(p)) + .map((p) => mapPlaylistToCard(p, displayName || p.owner_user_id)), + ); + const merged = [ ...created.data.items.filter(isAlbum), ...liked.data.items.filter(isAlbum), @@ -90,16 +86,17 @@ export function useLibraryData() { }), ); }) - .catch(() => setAlbums([])); + .catch(() => { + setPlaylists([]); + setAlbums([]); + }); }, []); const recentEntries = (() => { const list = entries.length > 0 ? entries - : recentlyPlayedApi.length > 0 - ? recentlyPlayedApi.map((t) => ({ type: "track" as const, item: t, playedAt: "" })) - : mockRecentlyPlayedTracks.map((t) => ({ type: "track" as const, item: t, playedAt: "" })); + : recentlyPlayedApi.map((t) => ({ type: "track" as const, item: t, playedAt: "" })); const seen = new Set(); return list.filter((e) => { @@ -140,9 +137,7 @@ export function useLibraryData() { })(); const displayedFollowing = (() => { - const followingIdSet = new Set(user?.following_ids ?? []); - const filtered = followingUsers.filter((u) => followingIdSet.has(u.username)); - const seenUsernames = new Set(filtered.map((u) => u.username)); + const seenUsernames = new Set(followingUsers.map((u) => u.username)); const synthetic: User[] = (user?.following_ids ?? []) .filter((username) => !seenUsernames.has(username)) .map((username, i) => ({ @@ -151,7 +146,7 @@ export function useLibraryData() { displayName: username, followers: 0, })); - return [...filtered, ...synthetic]; + return [...followingUsers, ...synthetic]; })(); return { diff --git a/src/components/playlist/test/PlaylistSidebarAndComponent.test.tsx b/src/components/playlist/test/PlaylistSidebarAndComponent.test.tsx new file mode 100644 index 00000000..26616045 --- /dev/null +++ b/src/components/playlist/test/PlaylistSidebarAndComponent.test.tsx @@ -0,0 +1,188 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import React from "react"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import PlaylistSidebar from "../PlaylistSidebar"; +import PlaylistComponent from "../PlaylistComponent"; +import { + getPlaylistsByUser, + getPlaylistLikers, + getPlaylistReposters, +} from "@/services/api/playlist/playlist.service"; +import { getUserById } from "@/services/user.service"; +import { usePlayerStore } from "@/stores/player.store"; +import { useAuthStore } from "@/stores/auth.store"; + +// ── Mocks ──────────────────────────────────────────────────── + +vi.mock("@/services/api/playlist/playlist.service", () => ({ + getPlaylistsByUser: vi.fn(), + getPlaylistLikers: vi.fn(), + getPlaylistReposters: vi.fn(), +})); + +vi.mock("@/services/user.service", () => ({ + getUserById: vi.fn(), +})); + +vi.mock("@/stores/auth.store", () => ({ + useAuthStore: Object.assign(vi.fn(), { + subscribe: vi.fn(), + }), +})); + +vi.mock("@/stores/player.store", () => ({ + usePlayerStore: vi.fn(), +})); + +vi.mock("@/pages/[username]/[trackSlug]/components/SharePopup", () => ({ + default: ({ onClose }: any) => ( +
+ +
+ ), +})); + +vi.mock("@/services/audioService", () => ({ + audio: { src: "" }, + seekAudio: vi.fn(), + setGlobalWaveSurfer: vi.fn(), + setTrackLoadedLocally: vi.fn(), +})); + +vi.mock("wavesurfer.js", () => ({ + default: { + create: vi.fn(() => ({ + on: vi.fn(), + destroy: vi.fn(), + })), + }, +})); + +// ── Shared test data ───────────────────────────────────────────────────────── + +const mockUser = { id: "u-1", username: "testuser", displayName: "Test User" }; + +const mockPlaylistDetails = { + playlist_id: "pl-1", + name: "Test Playlist", + is_public: true, + track_count: 3, + owner_user_id: "u-1", + tracks: [{ track_id: "t-1", title: "Track 1", artist_name: "Artist A" }], +}; + +const sidebarPlaylists = [ + { playlist_id: "pl-2", name: "Another Set" }, + { playlist_id: "pl-3", name: "Third Set" }, +]; + +describe("PlaylistSidebar", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useAuthStore).mockReturnValue({ user: mockUser } as any); + vi.mocked(getUserById).mockResolvedValue({ + id: "u-1", + username: "testuser", + display_name: "Test User", + } as any); + vi.mocked(getPlaylistsByUser).mockResolvedValue({ + data: { items: sidebarPlaylists, meta: { total: 2 } }, + } as any); + vi.mocked(getPlaylistLikers).mockResolvedValue({ + data: [], + } as any); + vi.mocked(getPlaylistReposters).mockResolvedValue({ + data: [], + } as any); + }); + + it("renders the sidebar container", async () => { + render( + + + , + ); + await waitFor(() => + expect(screen.getByTestId("playlist-sidebar")).toBeInTheDocument(), + ); + }); + + it("renders 'Playlists from this user' heading", () => { + render( + + + , + ); + expect(screen.getByText(/Playlists from this user/i)).toBeInTheDocument(); + }); + + it("renders 'View all' link", () => { + render( + + + , + ); + expect(screen.getByText("View all")).toBeInTheDocument(); + }); + + it("navigates 'View all' to the user's sets page", async () => { + render( + + + , + ); + await waitFor(() => + expect(screen.getByRole("link", { name: "View all" })).toHaveAttribute( + "href", + "/testuser", + ), + ); + }); + + it("renders other playlists after load", async () => { + render( + + + , + ); + await waitFor(() => + expect(screen.getByText("Another Set")).toBeInTheDocument(), + ); + }); + + it("does not render the current playlist in the sidebar list", async () => { + render( + + + , + ); + await waitFor(() => + expect(screen.queryByText("Test Playlist")).not.toBeInTheDocument(), + ); + }); + + it("shows spinner while loading", () => { + vi.mocked(getPlaylistsByUser).mockReturnValue(new Promise(() => {})); + render( + + + , + ); + expect(document.querySelector(".animate-spin")).toBeInTheDocument(); + }); + + it("shows 'No other playlists' when list is empty", async () => { + vi.mocked(getPlaylistsByUser).mockResolvedValue({ + data: { items: [] }, + } as any); + render( + + + , + ); + await waitFor(() => + expect(screen.getByText(/No other playlists/)).toBeInTheDocument(), + ); + }); +}); diff --git a/src/pages/admin/tests/AdminUsersPage.test.tsx b/src/pages/admin/tests/AdminUsersPage.test.tsx new file mode 100644 index 00000000..a8b698e5 --- /dev/null +++ b/src/pages/admin/tests/AdminUsersPage.test.tsx @@ -0,0 +1,371 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import AdminUsersPage from "../users/AdminUsersPage"; + +const mockAxiosGet = vi.hoisted(() => vi.fn()); +const mockAdminSuspendUser = vi.hoisted(() => vi.fn()); +const mockAdminReinstateUser = vi.hoisted(() => vi.fn()); +const mockAdminWarnUser = vi.hoisted(() => vi.fn()); + +vi.mock("@/services/api/axiosInstance", () => ({ + default: { get: mockAxiosGet }, +})); + +vi.mock("@/services/api/admin.service", () => ({ + adminSuspendUser: mockAdminSuspendUser, + adminReinstateUser: mockAdminReinstateUser, + adminWarnUser: mockAdminWarnUser, +})); + +const makeApiUser = (overrides: Record = {}) => ({ + id: "u1", + display_name: "Jane Doe", + username: "janedoe", + role: "listener", + is_verified: false, + follower_count: 120, + profile_picture: null, + ...overrides, +}); + +const usersResponse = (users: ReturnType[]) => ({ + data: { data: { users }, pagination: { total: users.length } }, +}); + +beforeEach(() => { + mockAxiosGet.mockResolvedValue(usersResponse([])); + mockAdminSuspendUser.mockResolvedValue(undefined); + mockAdminReinstateUser.mockResolvedValue(undefined); + mockAdminWarnUser.mockResolvedValue(undefined); +}); + +describe("AdminUsersPage", () => { + it("renders the User Management heading", () => { + render( + + + , + ); + expect(screen.getByText("User Management")).toBeInTheDocument(); + }); + + it("renders the user search input", () => { + render( + + + , + ); + expect(screen.getByTestId("input-user-search")).toBeInTheDocument(); + }); + + it("renders the role filter select", () => { + render( + + + , + ); + expect(screen.getByTestId("select-role-filter")).toBeInTheDocument(); + }); + + it("renders the refresh button", () => { + render( + + + , + ); + expect(screen.getByTestId("btn-refresh")).toBeInTheDocument(); + }); + + it("shows 'Type a name to search users' when search is empty", async () => { + render( + + + , + ); + await waitFor(() => { + expect(screen.getByText("Type a name to search users")).toBeInTheDocument(); + }); + }); + + it("does not call API when search is empty on mount", () => { + render( + + + , + ); + expect(mockAxiosGet).not.toHaveBeenCalled(); + }); + + it("renders user rows after search returns results", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser()])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor( + () => { + expect(screen.getByText("Jane Doe")).toBeInTheDocument(); + }, + { timeout: 1000 }, + ); + }); + + it("shows 'No users found' when search returns empty", async () => { + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "nobody" }, + }); + await waitFor( + () => { + expect(screen.getByText("No users found")).toBeInTheDocument(); + }, + { timeout: 1000 }, + ); + }); + + it("shows action button for non-admin users", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor( + () => { + expect(screen.getByTestId("btn-user-actions-u1")).toBeInTheDocument(); + }, + { timeout: 1000 }, + ); + }); + + it("opens user action dropdown on button click", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + expect(screen.getByTestId("btn-send-warning")).toBeInTheDocument(); + expect(screen.getByTestId("btn-suspend-account")).toBeInTheDocument(); + }); + + it("opens suspend modal when Suspend Account is clicked", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + expect(screen.getByText("Suspend Account")).toBeInTheDocument(); + }); + + it("closes suspend modal when cancel is clicked", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + fireEvent.click(screen.getByTestId("btn-suspend-cancel")); + expect(screen.queryByText("Suspend Account")).not.toBeInTheDocument(); + }); + + it("confirm button is disabled when suspend reason is empty", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + const confirmBtn = screen.getByTestId("btn-suspend-confirm"); + expect(confirmBtn).toBeDisabled(); + }); + + it("enables confirm button after entering a suspend reason", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + fireEvent.change(screen.getByTestId("textarea-suspend-reason"), { + target: { value: "Violation of terms" }, + }); + expect(screen.getByTestId("btn-suspend-confirm")).not.toBeDisabled(); + }); + + it("calls adminSuspendUser when suspend is confirmed", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + fireEvent.change(screen.getByTestId("textarea-suspend-reason"), { + target: { value: "Repeated violations" }, + }); + fireEvent.click(screen.getByTestId("btn-suspend-confirm")); + await waitFor(() => { + expect(mockAdminSuspendUser).toHaveBeenCalledWith("u1", "Repeated violations"); + }); + }); + + it("opens warn modal when Send Warning is clicked", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-send-warning")); + // "Send Warning" appears in both the modal heading and the confirm button + expect(screen.getAllByText("Send Warning").length).toBeGreaterThanOrEqual(1); + }); + + it("renders warning category buttons in warn modal", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-send-warning")); + expect( + screen.getByTestId("btn-warn-reason-copyright_strike"), + ).toBeInTheDocument(); + expect( + screen.getByTestId("btn-warn-reason-content_policy_violation"), + ).toBeInTheDocument(); + }); + + it("calls adminWarnUser when warn is confirmed", async () => { + mockAxiosGet.mockResolvedValue(usersResponse([makeApiUser({ id: "u1", role: "listener" })])); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-send-warning")); + fireEvent.click(screen.getByTestId("btn-warn-confirm")); + await waitFor(() => { + expect(mockAdminWarnUser).toHaveBeenCalledWith( + "u1", + expect.objectContaining({ reason: "content_policy_violation" }), + ); + }); + }); + + it("clears search when clear button is clicked", async () => { + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + fireEvent.click(screen.getByTestId("btn-clear-search")); + const input = screen.getByTestId("input-user-search") as HTMLInputElement; + expect(input.value).toBe(""); + }); + + it("shows reinstate button for suspended users", async () => { + mockAxiosGet.mockResolvedValue( + usersResponse([makeApiUser({ id: "u1", role: "listener" })]), + ); + render( + + + , + ); + fireEvent.change(screen.getByTestId("input-user-search"), { + target: { value: "jane" }, + }); + await waitFor(() => expect(screen.getByText("Active")).toBeInTheDocument(), { + timeout: 1000, + }); + await waitFor(() => fireEvent.click(screen.getByTestId("btn-user-actions-u1")), { + timeout: 1000, + }); + fireEvent.click(screen.getByTestId("btn-suspend-account")); + fireEvent.change(screen.getByTestId("textarea-suspend-reason"), { + target: { value: "Reason" }, + }); + fireEvent.click(screen.getByTestId("btn-suspend-confirm")); + await waitFor(() => { + expect(mockAdminSuspendUser).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/creator/upload/tests/UploadDetailsForm.test.tsx b/src/pages/creator/upload/tests/UploadDetailsForm.test.tsx index 7e5371c7..17ec7e2a 100644 --- a/src/pages/creator/upload/tests/UploadDetailsForm.test.tsx +++ b/src/pages/creator/upload/tests/UploadDetailsForm.test.tsx @@ -37,7 +37,7 @@ const renderForm = (props = {}) => { const utils = render( - , + ); return { ref, ...utils }; }; @@ -55,36 +55,33 @@ describe("UploadDetailsForm", () => { // ── Rendering ────────────────────────────────────────────────────────────── + it("renders the form container", () => { + renderForm(); + expect(screen.getByTestId("upload-details-form")).toBeInTheDocument(); + }); + it("auto-populates title from the audio file name", () => { renderForm(); - const titleInput = screen.getByTestId( - "upload-title-input", - ) as HTMLInputElement; + const titleInput = screen.getByTestId("upload-title-input") as HTMLInputElement; expect(titleInput.value).toBe("my-track"); }); it("sets title to 'Recorded Audio' when audioData is a Blob", () => { const blob = new Blob(["audio"], { type: "audio/wav" }); renderForm({ audioData: blob }); - const titleInput = screen.getByTestId( - "upload-title-input", - ) as HTMLInputElement; + const titleInput = screen.getByTestId("upload-title-input") as HTMLInputElement; expect(titleInput.value).toBe("Recorded Audio"); }); it("auto-populates track link slug from the file name", () => { renderForm(); - const linkInput = screen.getByTestId( - "upload-track-link-input", - ) as HTMLInputElement; + const linkInput = screen.getByTestId("upload-track-link-input") as HTMLInputElement; expect(linkInput.value).toBe("my-track"); }); it("auto-populates artist field with the logged-in username", () => { renderForm(); - const artistInput = screen.getByTestId( - "upload-artists-input", - ) as HTMLInputElement; + const artistInput = screen.getByTestId("upload-artists-input") as HTMLInputElement; expect(artistInput.value).toBe("testuser"); }); @@ -105,17 +102,13 @@ describe("UploadDetailsForm", () => { it("renders public privacy option selected by default", () => { renderForm(); - const publicRadio = screen.getByTestId( - "upload-privacy-public-radio", - ) as HTMLInputElement; + const publicRadio = screen.getByTestId("upload-privacy-public-radio") as HTMLInputElement; expect(publicRadio.checked).toBe(true); }); it("renders private privacy option unselected by default", () => { renderForm(); - const privateRadio = screen.getByTestId( - "upload-privacy-private-radio", - ) as HTMLInputElement; + const privateRadio = screen.getByTestId("upload-privacy-private-radio") as HTMLInputElement; expect(privateRadio.checked).toBe(false); }); @@ -136,9 +129,7 @@ describe("UploadDetailsForm", () => { const titleInput = screen.getByTestId("upload-title-input"); await userEvent.clear(titleInput); await userEvent.type(titleInput, "Hello World"); - const linkInput = screen.getByTestId( - "upload-track-link-input", - ) as HTMLInputElement; + const linkInput = screen.getByTestId("upload-track-link-input") as HTMLInputElement; expect(linkInput.value).toBe("hello-world"); }); @@ -147,9 +138,7 @@ describe("UploadDetailsForm", () => { const titleInput = screen.getByTestId("upload-title-input"); await userEvent.clear(titleInput); await userEvent.type(titleInput, "My New Song"); - const linkInput = screen.getByTestId( - "upload-track-link-input", - ) as HTMLInputElement; + const linkInput = screen.getByTestId("upload-track-link-input") as HTMLInputElement; expect(linkInput.value).toBe("my-new-song"); }); @@ -158,9 +147,7 @@ describe("UploadDetailsForm", () => { const titleInput = screen.getByTestId("upload-title-input"); await userEvent.clear(titleInput); await userEvent.type(titleInput, "UPPERCASE"); - const linkInput = screen.getByTestId( - "upload-track-link-input", - ) as HTMLInputElement; + const linkInput = screen.getByTestId("upload-track-link-input") as HTMLInputElement; expect(linkInput.value).toBe("uppercase"); }); @@ -168,9 +155,7 @@ describe("UploadDetailsForm", () => { it("switches to private when private radio is clicked", async () => { renderForm(); - const privateRadio = screen.getByTestId( - "upload-privacy-private-radio", - ) as HTMLInputElement; + const privateRadio = screen.getByTestId("upload-privacy-private-radio") as HTMLInputElement; await userEvent.click(privateRadio); expect(privateRadio.checked).toBe(true); }); @@ -178,9 +163,7 @@ describe("UploadDetailsForm", () => { it("deselects public when private is chosen", async () => { renderForm(); await userEvent.click(screen.getByTestId("upload-privacy-private-radio")); - const publicRadio = screen.getByTestId( - "upload-privacy-public-radio", - ) as HTMLInputElement; + const publicRadio = screen.getByTestId("upload-privacy-public-radio") as HTMLInputElement; expect(publicRadio.checked).toBe(false); }); @@ -188,9 +171,7 @@ describe("UploadDetailsForm", () => { renderForm(); await userEvent.click(screen.getByTestId("upload-privacy-private-radio")); await userEvent.click(screen.getByTestId("upload-privacy-public-radio")); - const publicRadio = screen.getByTestId( - "upload-privacy-public-radio", - ) as HTMLInputElement; + const publicRadio = screen.getByTestId("upload-privacy-public-radio") as HTMLInputElement; expect(publicRadio.checked).toBe(true); }); @@ -229,7 +210,7 @@ describe("UploadDetailsForm", () => { const { ref } = renderForm({ onSuccess }); await ref.current!.triggerSubmit(); await waitFor(() => - expect(onSuccess).toHaveBeenCalledWith("new-track-id-123"), + expect(onSuccess).toHaveBeenCalledWith("new-track-id-123") ); }); @@ -248,10 +229,10 @@ describe("UploadDetailsForm", () => { await userEvent.clear(titleInput); await ref.current!.triggerSubmit(); await waitFor(() => - expect(screen.getByTestId("upload-error-message")).toBeInTheDocument(), + expect(screen.getByTestId("upload-error-message")).toBeInTheDocument() ); expect(screen.getByTestId("upload-error-message")).toHaveTextContent( - /track title is required/i, + /track title is required/i ); }); @@ -261,15 +242,15 @@ describe("UploadDetailsForm", () => { await act(() => ref.current!.triggerSubmit()); expect(mockUploadTrack).not.toHaveBeenCalled(); }); - + it("shows error when audioData is null", async () => { const { ref } = renderForm({ audioData: null }); await ref.current!.triggerSubmit(); await waitFor(() => - expect(screen.getByTestId("upload-error-message")).toBeInTheDocument(), + expect(screen.getByTestId("upload-error-message")).toBeInTheDocument() ); expect(screen.getByTestId("upload-error-message")).toHaveTextContent( - /missing audio data/i, + /missing audio data/i ); }); @@ -289,10 +270,10 @@ describe("UploadDetailsForm", () => { const { ref } = renderForm(); await ref.current!.triggerSubmit(); await waitFor(() => - expect(screen.getByTestId("upload-error-message")).toBeInTheDocument(), + expect(screen.getByTestId("upload-error-message")).toBeInTheDocument() ); expect(screen.getByTestId("upload-error-message")).toHaveTextContent( - /upload limit reached/i, + /upload limit reached/i ); }); @@ -301,7 +282,9 @@ describe("UploadDetailsForm", () => { const setIsLoadingParent = vi.fn(); const { ref } = renderForm({ setIsLoadingParent }); await ref.current!.triggerSubmit(); - await waitFor(() => expect(setIsLoadingParent).toHaveBeenCalledWith(false)); + await waitFor(() => + expect(setIsLoadingParent).toHaveBeenCalledWith(false) + ); }); it("does not call onSuccess when upload fails", async () => { @@ -313,6 +296,43 @@ describe("UploadDetailsForm", () => { expect(onSuccess).not.toHaveBeenCalled(); }); + it("submits optional metadata fields and cover image", async () => { + const user = userEvent.setup(); + const onProgress = vi.fn(); + const { ref } = renderForm({ onProgress }); + + const descriptionInput = screen.getByTestId("upload-description-input"); + const artistsInput = screen.getByTestId("upload-artists-input"); + const tagsInput = screen.getByTestId("upload-tags-input"); + const coverInput = screen.getByTestId("cover-image-input") as HTMLInputElement; + const genreInput = screen.getByTestId("dropdown-input"); + const coverFile = new File(["cover"], "cover.png", { type: "image/png" }); + + await user.clear(descriptionInput); + await user.type(descriptionInput, " A detailed description "); + await user.clear(artistsInput); + await user.type(artistsInput, "Artist One, Artist Two"); + await user.clear(tagsInput); + await user.type(tagsInput, "tag-1, tag-2"); + await user.click(screen.getByTestId("upload-privacy-private-radio")); + await user.upload(coverInput, coverFile); + await user.click(genreInput); + await user.click(await screen.findByText("Electronic")); + + await ref.current!.triggerSubmit(); + await waitFor(() => expect(mockUploadTrack).toHaveBeenCalledTimes(1)); + + const [payload, progressCallback] = mockUploadTrack.mock.calls[0]; + expect(payload.description).toBe("A detailed description"); + expect(payload.genre).toBe("Electronic"); + expect(payload.artists).toBe("Artist One, Artist Two"); + expect(payload.is_public).toBe(false); + expect(payload.cover_image).toBe(coverFile); + expect(payload.tags).toEqual(["tag-1", "tag-2"]); + expect(typeof progressCallback).toBe("function"); + expect(onProgress).not.toHaveBeenCalled(); + }); + it("forwards upload progress updates from the service", async () => { const progress = vi.fn(); mockUploadTrack.mockImplementationOnce(async (_payload, onProgress) => { diff --git a/src/pages/you/albums/AlbumSlugPage.tsx b/src/pages/you/albums/AlbumSlugPage.tsx index 55705db0..5e9fc828 100644 --- a/src/pages/you/albums/AlbumSlugPage.tsx +++ b/src/pages/you/albums/AlbumSlugPage.tsx @@ -237,7 +237,7 @@ function AlbumSlugPage() { return (
{/* Hero Section using the fetched playlist data */} ({
), })); +vi.mock("@/components/playlist/PlaylistCard", () => ({ + default: ({ playlist }: { playlist: { playlist_id: string } }) => ( +
+ ), +})); vi.mock("@/components/UI/UserCard/UserCard", () => ({ default: ({ user }: { user: { id: string } }) => (
@@ -137,8 +142,6 @@ const defaultLikes = { likedPlaylists: [], likedAlbums: [], likedRadioTracks: [], - likedMixes: [], - likedGenres: [], }; const defaultHistory = { entries: [] }; @@ -368,7 +371,7 @@ describe("LibraryPage", () => { it("calls getMyPlaylists (library service) on mount", async () => { await renderPage(); - expect(getMyPlaylistsApi).toHaveBeenCalledTimes(1); + expect(getMyPlaylistsLib).toHaveBeenCalledTimes(1); }); it("calls getMyFollowing on mount", async () => { @@ -576,7 +579,7 @@ describe("LibraryPage", () => { (getMyPlaylistsApi as ReturnType).mockRejectedValue(new Error("Network error")); await renderPage(); await waitFor(() => {}); - expect(screen.queryAllByTestId(/^playlist-card-/)).toHaveLength(0); + expect(screen.queryAllByTestId(/^album-card-/)).toHaveLength(0); }); // ── Recently Played — API success path ─────────────────── @@ -615,7 +618,7 @@ describe("LibraryPage", () => { ], }); await renderPage(); - expect(screen.getByTestId("playlist-card-la1")).toBeInTheDocument(); + expect(screen.getByTestId("album-card-la1")).toBeInTheDocument(); }); it("renders album cards from API when items have is_album_view true", async () => { @@ -628,7 +631,7 @@ describe("LibraryPage", () => { }); await renderPage(); await waitFor(() => { - expect(screen.getByTestId("playlist-card-a1")).toBeInTheDocument(); + expect(screen.getByTestId("album-card-a1")).toBeInTheDocument(); }); }); @@ -642,7 +645,7 @@ describe("LibraryPage", () => { }); await renderPage(); await waitFor(() => {}); - expect(screen.queryByTestId("playlist-card-p1")).not.toBeInTheDocument(); + expect(screen.queryByTestId("album-card-p1")).not.toBeInTheDocument(); }); it("deduplicates albums with the same id from both APIs", async () => { @@ -651,7 +654,7 @@ describe("LibraryPage", () => { (getLikedPlaylists as ReturnType).mockResolvedValue({ data: { items: [albumItem] } }); await renderPage(); await waitFor(() => { - const cards = screen.getAllByTestId("playlist-card-shared-album"); + const cards = screen.getAllByTestId("album-card-shared-album"); expect(cards).toHaveLength(1); }); }); @@ -665,7 +668,7 @@ describe("LibraryPage", () => { }); await renderPage(); await waitFor(() => { - const cards = screen.getAllByTestId("playlist-card-dup-album"); + const cards = screen.getAllByTestId("album-card-dup-album"); expect(cards).toHaveLength(1); }); });