feat(feed): toolbar pills named by a glyph, Sort and View on the filters' row
The watch-state, content-type and sort pills on the feed, a channel page and Plex Browse show a glyph where their group's word was; the word moves to the tooltip and stays in the accessible name. Sort + View end the toolbar's row instead of a row of their own, the chip row shows only with a chip, and a channel page drops the count pill its header already carries. Also logs the docked bar's glyph-only decision in DESIGN.md.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// The feed's toolbar, on the feed and on a channel page. The filter pills name their group by a glyph
|
||||
// (the word moved to the tooltip and the accessible name), Sort + View end the first row instead of
|
||||
// taking a row of their own, and a channel page drops the count pill: its header already says how
|
||||
// many videos the channel has.
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
vi.mock("../../features/wizard/wizardContext", () => ({
|
||||
useWizard: () => ({ openWizard: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./VirtualFeed", () => ({ default: () => null }));
|
||||
vi.mock("../../features/filters/SelectedViewChip", () => ({ default: () => null }));
|
||||
vi.mock("../../features/filters/ActiveFilterChips", () => ({ default: () => null }));
|
||||
|
||||
import Feed from "./Feed";
|
||||
import { api, DEFAULT_TAG_MODES, type FeedFilters } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { PageToolbarSlot } from "../../components/ui/pageToolbarSlot";
|
||||
|
||||
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 FILTERS: FeedFilters = {
|
||||
tags: [],
|
||||
tagModes: DEFAULT_TAG_MODES,
|
||||
channelIds: [],
|
||||
q: "",
|
||||
sort: "newest",
|
||||
scope: "my",
|
||||
includeNormal: true,
|
||||
includeShorts: false,
|
||||
includeLive: false,
|
||||
show: "unwatched",
|
||||
};
|
||||
|
||||
const slot = document.body.appendChild(document.createElement("div"));
|
||||
|
||||
async function renderFeed(channelScoped = false) {
|
||||
vi.spyOn(api, "feed").mockResolvedValue({ items: [], next_cursor: null } as never);
|
||||
vi.spyOn(api, "feedSummary").mockResolvedValue({ count: 12, facets: {} } as never);
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
qc.setQueryData(qk.me(), { id: 1, can_write: true, prefs: {} });
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<PageToolbarSlot.Provider value={slot}>
|
||||
<Feed
|
||||
filters={FILTERS}
|
||||
setFilters={() => {}}
|
||||
view="cards"
|
||||
setView={() => {}}
|
||||
canRead
|
||||
ytSearch=""
|
||||
onYtSearch={() => {}}
|
||||
onExitYtSearch={() => {}}
|
||||
channelScoped={channelScoped}
|
||||
/>
|
||||
</PageToolbarSlot.Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
// The toolbar renders once the first page has answered.
|
||||
await screen.findByTestId("feed-show");
|
||||
}
|
||||
|
||||
const trigger = (name: string) =>
|
||||
screen.getAllByRole("button").find((b) => b.getAttribute("aria-label")?.startsWith(`${name}:`))!;
|
||||
const sortTrigger = () => trigger("feed.sortLabel");
|
||||
|
||||
describe("Feed toolbar", () => {
|
||||
it.each([
|
||||
["feed-show", "feed.showLabel"],
|
||||
["feed-content", "feed.contentLabel"],
|
||||
])("the %s pill shows a glyph, not its group's word, and names itself", async (id, word) => {
|
||||
await renderFeed();
|
||||
const pill = screen.getByTestId(id);
|
||||
expect(pill.textContent).not.toContain(word);
|
||||
expect(pill.querySelector("svg")).not.toBeNull();
|
||||
expect(pill.getAttribute("title")).toBe(word);
|
||||
expect(pill.getAttribute("aria-label")).toMatch(new RegExp(`^${word}: `));
|
||||
});
|
||||
|
||||
it("the sort pill shows a glyph, not the word Sort, and names itself", async () => {
|
||||
await renderFeed();
|
||||
const sort = sortTrigger();
|
||||
expect(sort.textContent).not.toContain("feed.sortLabel");
|
||||
expect(sort.getAttribute("title")).toBe("feed.sortLabel");
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"Sort and View end the filters' row instead of a row of their own (channel page: %s)",
|
||||
async (channelScoped) => {
|
||||
await renderFeed(channelScoped);
|
||||
const row = screen.getByTestId("feed-show").parentElement!;
|
||||
expect(row.contains(sortTrigger())).toBe(true);
|
||||
expect(row.contains(trigger("feed.viewLabel"))).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("a channel page shows no count pill; the feed keeps it", async () => {
|
||||
await renderFeed(true);
|
||||
expect(screen.queryByTestId("item-count")).toBeNull();
|
||||
cleanup();
|
||||
await renderFeed(false);
|
||||
expect(screen.getByTestId("item-count")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user