// @vitest-environment jsdom // // The channel page's Block and Hide toggles (deep-review E11 round 2) and its Subscribe / Unsubscribe // button (round 3: the same row, missed) while their request runs. A // focused button that turns natively `disabled` drops the keyboard to in Chrome (measured for // ui/Button in E11 S2), so a busy toggle blocks the click with aria-disabled instead — focusable, and // still pressed only once. import type { ReactNode } from "react"; import { describe, it, expect, vi, beforeAll, afterEach, onTestFinished } from "vitest"; import { render, screen, fireEvent, waitFor, cleanup, act } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), })); vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ closeChannel: vi.fn(), openChannel: vi.fn() }), })); vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true })); vi.mock("./feedFiltersContext", () => ({ useFeedFilters: () => ({ includeNormal: true, includeShorts: false, includeLive: false }), })); vi.mock("./Feed", () => ({ default: () => null })); vi.mock("./ChannelAbout", () => ({ default: () => null })); // The page's chrome portals into the shell's fixed toolbar band, which this test has no shell for. vi.mock("./ui/PageShell", async (importOriginal) => ({ ...(await importOriginal()), PageToolbar: ({ children }: { children?: ReactNode }) => <>{children}, })); import ChannelPage from "./ChannelPage"; import { api, type ChannelDetail, type Me } from "../lib/api"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const CH = "UCsXVk37bltHxD1rDPwtNM8Q"; describe("ChannelPage — a busy toggle keeps the keyboard", () => { it.each([ ["channel.block", "blockChannel", true], ["channel.hide", "updateChannel", true], ["channel.subscribe", "subscribeChannel", false], ["channel.subscribedState", "unsubscribeChannel", true], ] as const)( "%s stays focusable and fires once while its request runs", async (label, call, subscribed) => { vi.spyOn(api, "channelDetail").mockResolvedValue({ id: CH, title: "LEMMiNO", subscribed, blocked: false, hidden: false, deep_requested: false, backfill_done: true, stored_videos: 10, } as unknown as ChannelDetail); vi.spyOn(api, "tags").mockResolvedValue([] as never); const pending = vi.spyOn(api, call).mockReturnValue(new Promise(() => {}) as never); render( {}} onShowInFeed={() => {}} /> , ); const button = await screen.findByRole("button", { name: label }); act(() => button.focus()); fireEvent.click(button); await waitFor(() => expect(button.getAttribute("aria-disabled")).toBe("true")); expect(button.hasAttribute("disabled")).toBe(false); expect(document.activeElement).toBe(button); fireEvent.click(button); expect(pending).toHaveBeenCalledTimes(1); }, ); }); // The banner is retried on error (googleusercontent drops a burst); while an attempt fails and the // next waits, a visible shows the browser's broken-image glyph (seen live, E28). It stays // invisible until a source has actually loaded. describe("ChannelPage — the banner", () => { it("is invisible until it loads, and again while a failed attempt is retried", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); onTestFinished(() => { vi.useRealTimers(); }); vi.spyOn(api, "channelDetail").mockResolvedValue({ id: CH, title: "LEMMiNO", subscribed: true, blocked: false, hidden: false, deep_requested: false, backfill_done: true, stored_videos: 10, banner_url: "https://yt3.example/banner", } as unknown as ChannelDetail); vi.spyOn(api, "tags").mockResolvedValue([] as never); const { container } = render( {}} onShowInFeed={() => {}} /> , ); const banner = () => container.querySelector('img[src^="https://yt3.example/banner"]')!; await waitFor(() => expect(banner()).toBeTruthy()); expect(banner().classList.contains("opacity-0")).toBe(true); fireEvent.error(banner()); await act(async () => { await vi.advanceTimersByTimeAsync(600); }); expect(banner().getAttribute("src")).toContain("?r=1"); expect(banner().classList.contains("opacity-0")).toBe(true); fireEvent.load(banner()); expect(banner().classList.contains("opacity-0")).toBe(false); }); });