// @vitest-environment jsdom // // The two shared channel write flows report their own failures, with the real error. // Before: the channel detail page's tag toggle and both pages' priority stepper only refetched, so a // refused write just snapped back; and the manager reported a failed tag toggle by handing the // CHANNEL ID to the reporter as though it were the error. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ReactNode } from "react"; import { act, renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("./notifications", () => ({ notify: vi.fn(() => 0) })); import { api, HttpError } from "./api"; import { notify } from "./notifications"; import { useChannelTagActions } from "./useChannelTagActions"; import { useChannelPriority } from "./useChannelPriority"; import i18n from "../i18n"; const notifyMock = vi.mocked(notify); beforeEach(() => notifyMock.mockClear()); afterEach(() => vi.restoreAllMocks()); const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); describe("useChannelTagActions", () => { it("names a refused tag toggle with its real reason, then lets the caller revert", async () => { vi.spyOn(api, "attachChannelTag").mockRejectedValue(new HttpError(403, "nope")); const onError = vi.fn(); const { result } = renderHook( () => useChannelTagActions({ optimisticToggle: () => {}, onError }), { wrapper }, ); act(() => result.current.toggleTag("UC1", 7, false)); await waitFor(() => expect(onError).toHaveBeenCalledWith("UC1")); expect(notifyMock).toHaveBeenCalledWith({ level: "error", title: i18n.t("channels.tags.updateFailed"), message: i18n.t("errors.reason.forbidden"), }); }); it("names a refused create-and-attach", async () => { vi.spyOn(api, "createTag").mockRejectedValue(new HttpError(429, "slow")); const { result } = renderHook( () => useChannelTagActions({ optimisticToggle: () => {}, onError: () => {} }), { wrapper }, ); act(() => result.current.createAndAttachTag("UC1", "Music")); await waitFor(() => expect(notifyMock).toHaveBeenCalledWith({ level: "error", title: i18n.t("channels.tags.createFailed"), message: i18n.t("errors.reason.rateLimited"), }), ); }); }); describe("useChannelPriority", () => { it("names a refused priority save, then lets the caller revert", async () => { vi.spyOn(api, "updateChannel").mockRejectedValue(new TypeError("Failed to fetch")); const onError = vi.fn(); let priority = 1; const { result } = renderHook(() => useChannelPriority({ read: () => priority, optimistic: (_id, next) => void (priority = next), onError, delay: 0, }), ); act(() => result.current.bumpPriority("UC1", 1)); await waitFor(() => expect(onError).toHaveBeenCalledWith("UC1")); expect(notifyMock).toHaveBeenCalledWith({ level: "error", title: i18n.t("errors.writeFailed.channelPriority"), message: i18n.t("errors.reason.unreachable"), }); }); });