// @vitest-environment jsdom // // applySyncFilter is the sync-status pre-filter the top chips drive (the Sync column was retired and // its status folded into a Stored charm). It replaced a column `filter.test`, so it carries the exact // "fully" | "needsFull" | "" vocab + predicate — asserted here so a regression can't silently change // which channels the top chips show. import { describe, it, expect } from "vitest"; import { applySyncFilter, buildChannelColumns, tagFilterChips, tagsColumnTest, } from "./channelColumns"; import { NO_TAG_SENTINEL, type ManagedChannel, type Tag } from "../../lib/api"; import type { TFunction } from "i18next"; import { render, screen } from "@testing-library/react"; import { formatDate } from "../../lib/format"; const ch = (over: Partial): ManagedChannel => ({ id: "c", title: null, handle: null, thumbnail_url: null, subscriber_count: null, video_count: null, description: null, stored_videos: 0, last_video_at: null, added_at: "2026-06-10T08:00:00+00:00", total_duration_seconds: 0, priority: 0, hidden: false, deep_requested: false, deep_in_queue: false, tag_ids: [], details_synced: false, recent_synced: false, backfill_done: false, ...over, }); const rows = [ ch({ id: "fully", recent_synced: true, backfill_done: true }), ch({ id: "recentOnly", recent_synced: true, backfill_done: false }), ch({ id: "none", recent_synced: false, backfill_done: false }), ]; describe("applySyncFilter", () => { it("returns every row for the empty/all value (and unknown values)", () => { expect(applySyncFilter(rows, "").map((r) => r.id)).toEqual(["fully", "recentOnly", "none"]); expect(applySyncFilter(rows, undefined)).toHaveLength(3); expect(applySyncFilter(rows, "bogus")).toHaveLength(3); }); it("'fully' keeps only channels with recent uploads AND full history in", () => { expect(applySyncFilter(rows, "fully").map((r) => r.id)).toEqual(["fully"]); }); it("'needsFull' keeps every channel whose back-catalogue is not done", () => { expect(applySyncFilter(rows, "needsFull").map((r) => r.id)).toEqual(["recentOnly", "none"]); }); }); describe("tagsColumnTest (item B — 'No tag')", () => { const tagged = ch({ id: "tagged", tag_ids: [1, 2] }); const untagged = ch({ id: "untagged", tag_ids: [] }); it("the No-tag sentinel keeps only channels with no user tag", () => { expect(tagsColumnTest(tagged, [NO_TAG_SENTINEL])).toBe(false); expect(tagsColumnTest(untagged, [NO_TAG_SENTINEL])).toBe(true); }); it("real tag values match ANY selected id (sentinel absent)", () => { expect(tagsColumnTest(tagged, ["2"])).toBe(true); expect(tagsColumnTest(tagged, ["3"])).toBe(false); expect(tagsColumnTest(untagged, ["1"])).toBe(false); }); it("the sentinel wins if it somehow co-exists with real ids", () => { expect(tagsColumnTest(tagged, [NO_TAG_SENTINEL, "1"])).toBe(false); }); }); describe("tagFilterChips — a pick whose tag is gone", () => { const mine = { id: 1, name: "Music", system: false } as Tag; const t = ((k: string) => k) as unknown as TFunction; type State = "loaded" | "loading" | "failed"; const chips = (state: State, picked: string[]) => tagFilterChips({ userTags: state === "loaded" ? [mine] : [], tags: { data: state === "loaded" ? [mine] : undefined, isSuccess: state === "loaded", isPending: state === "loading", }, picked, noTagOn: false, toggle: () => {}, toggleNoTag: () => {}, t, }); it("stays a visible, active chip instead of an empty table with no reason", () => { expect(chips("loaded", ["1", "77"]).map((c) => [c.key, c.label, c.active])).toEqual([ ["t77", "common.deletedTag", true], // first: the "+N" overflow must not fold it away ["t1", "Music", true], [NO_TAG_SENTINEL, "channels.tags.noTag", false], ]); }); // Before, a pick had NO chip at all until the list loaded — or ever, after a failed // load — while it still narrowed the table. it("is a placeholder while the tag list loads, never called deleted", () => { expect(chips("loading", ["77"]).map((c) => [c.key, c.label, c.pending])).toEqual([ ["t77", "77", true], [NO_TAG_SENTINEL, "channels.tags.noTag", undefined], ]); }); it("shows the id after a failed load, still a visible, removable chip", () => { expect(chips("failed", ["77"]).map((c) => [c.key, c.label, c.pending, c.active])).toEqual([ ["t77", "77", false, true], [NO_TAG_SENTINEL, "channels.tags.noTag", undefined, false], ]); }); }); describe("buildChannelColumns — the row's end", () => { it('ends every row in the channel\'s "…", with no action buttons before it', () => { const more = (c: ManagedChannel) => `more:${c.id}`; const noop = () => {}; const cols = buildChannelColumns({ t: ((k: string) => k) as never, lang: "en", prioOptions: [], userTags: [], bumpPriority: noop, setPriority: noop, more, onView: noop, toggleTag: noop, createAndAttachTag: noop, onFilterByTag: noop, suggestionsFor: () => [], onAcceptSuggestion: noop, onDismissSuggestion: noop, }); expect(cols[cols.length - 1]!.key).toBe("actions"); expect(cols.filter((c) => c.key === "actions")).toHaveLength(1); expect(cols[cols.length - 1]!.render(ch({ id: "UCz" }))).toBe("more:UCz"); }); }); describe("buildChannelColumns — Added", () => { const noop = () => {}; const cols = buildChannelColumns({ t: ((k: string) => k) as never, lang: "en", prioOptions: [], userTags: [], bumpPriority: noop, setPriority: noop, more: () => null, onView: noop, toggleTag: noop, createAndAttachTag: noop, onFilterByTag: noop, suggestionsFor: () => [], onAcceptSuggestion: noop, onDismissSuggestion: noop, }); const added = cols.find((c) => c.key === "added"); it("follows Last upload, and sorts by when the follow reached the list", () => { expect(added?.header).toBe("channels.cols.added"); const keys = cols.map((c) => c.key); expect(keys.indexOf("added")).toBe(keys.indexOf("lastUpload") + 1); expect(added?.sortable).toBe(true); const early = ch({ added_at: "2026-06-10T08:00:00+00:00" }); const late = ch({ added_at: "2026-10-12T08:00:00+00:00" }); expect(added!.sortValue!(late) as number).toBeGreaterThan(added!.sortValue!(early) as number); }); it("shows the day, not a relative time", () => { render(<>{added!.render(ch({ added_at: "2026-10-12T08:00:00+00:00" }))}); expect(screen.getByText(formatDate("2026-10-12T08:00:00+00:00", "en"))).toBeTruthy(); }); });