From b4dae04631621335d23174e26556f103c6c4340e Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 15 Aug 2026 05:30:40 +0200 Subject: [PATCH] test(channels): cover applySyncFilter + DataTable fixedLayout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - applySyncFilter: the fully/needsFull/all predicate the top chips drive (moved off the retired Sync column) — guards which channels show. - DataTable fixedLayout: asserts the colgroup + table-fixed appear only when opted in, with widths on fixed columns and none on fluid ones. --- .../DataTable.fixedLayout.dom.test.tsx | 46 +++++++++++++++ .../components/channelColumns.dom.test.tsx | 56 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 frontend/src/components/DataTable.fixedLayout.dom.test.tsx create mode 100644 frontend/src/components/channelColumns.dom.test.tsx diff --git a/frontend/src/components/DataTable.fixedLayout.dom.test.tsx b/frontend/src/components/DataTable.fixedLayout.dom.test.tsx new file mode 100644 index 00000000..cb89d0a9 --- /dev/null +++ b/frontend/src/components/DataTable.fixedLayout.dom.test.tsx @@ -0,0 +1,46 @@ +// @vitest-environment jsdom +// +// The R14 zoom-fit foundation: DataTable's opt-in `fixedLayout` renders a and puts the +// table into `table-layout: fixed`, so columns with a width keep it and the rest (fluid text columns) +// split the remainder + truncate instead of pushing the table wider than its container. Off by +// default so existing content-sized tables are untouched. +import { describe, it, expect, afterEach, vi } from "vitest"; +import { render, cleanup } from "@testing-library/react"; +import DataTable, { type Column } from "./DataTable"; + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; +}); + +afterEach(cleanup); + +type Row = { a: string; b: string }; +const columns: Column[] = [ + { key: "a", header: "A", width: "5rem", render: (r) => r.a }, + { key: "b", header: "B", render: (r) => r.b }, // fluid — no width +]; +const rows: Row[] = [{ a: "1", b: "hello" }]; + +describe("DataTable fixedLayout", () => { + it("renders a width-bearing colgroup and table-fixed only when fixedLayout is set", () => { + const { container } = render( + r.a} fixedLayout />, + ); + const table = container.querySelector("table"); + expect(table?.className).toContain("table-fixed"); + + const cols = container.querySelectorAll("colgroup col"); + expect(cols).toHaveLength(2); + expect((cols[0] as HTMLElement).style.width).toBe("5rem"); // fixed column keeps its width + expect((cols[1] as HTMLElement).style.width).toBe(""); // fluid column: no width, splits remainder + }); + + it("omits the colgroup and stays auto-layout by default", () => { + const { container } = render( + r.a} />, + ); + expect(container.querySelector("colgroup")).toBeNull(); + expect(container.querySelector("table")?.className).not.toContain("table-fixed"); + }); +}); diff --git a/frontend/src/components/channelColumns.dom.test.tsx b/frontend/src/components/channelColumns.dom.test.tsx new file mode 100644 index 00000000..f9272f59 --- /dev/null +++ b/frontend/src/components/channelColumns.dom.test.tsx @@ -0,0 +1,56 @@ +// @vitest-environment jsdom +// +// applySyncFilter is the sync-status pre-filter the top chips drive (R14 retired the Sync column and +// folded its status 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 } from "./channelColumns"; +import { type ManagedChannel } from "../lib/api"; + +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, + total_duration_seconds: 0, + count_normal: 0, + count_short: 0, + count_live: 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"]); + }); +});