Files
siftlode/frontend/src/lib/channelsUrlState.dom.test.ts
T
peter 4aaaaa7860 chore(comments): drop epic, sprint and review-round ids from the code
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").

Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.

Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).

One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
2026-10-05 00:19:39 +02:00

109 lines
4.1 KiB
TypeScript

// @vitest-environment jsdom
//
// The Channels-manager content state (active tab + column sort + the column-filter map) round-trips
// through namespaced URL keys (ctab/csort/cf) so a manager view is shareable, the plexUrlState
// analogue. `cf` carries the mixed scalar/array FilterMap as a JSON blob.
import { describe, expect, it } from "vitest";
import {
channelsParamsToState,
channelsParamUpdates,
hasChannelsParams,
type ChannelsUrlState,
} from "./channelsUrlState";
import type { FilterMap } from "./tableColumns";
// A concrete non-default filter map exercising both wire shapes: scalars (the sync chip, a select) and
// an array (a multi column).
const RICH_FILTERS: FilterMap = {
sync: "needsFull",
priority: "3",
tags: ["12", "34"],
};
const BASE: ChannelsUrlState = { view: "subscribed", sort: null, filters: {} };
// Apply an updates delta onto a base URLSearchParams the way urlWithParams would (null deletes).
function applyUpdates(updates: Record<string, string | null>): URLSearchParams {
const p = new URLSearchParams();
for (const [k, v] of Object.entries(updates)) {
if (v != null) p.set(k, v);
}
return p;
}
describe("channelsParamUpdates", () => {
it("emits nothing for the all-default state", () => {
expect(channelsParamUpdates(BASE)).toEqual({ ctab: null, csort: null, cf: null });
});
it("clears every key when passed null (off-manager page)", () => {
expect(channelsParamUpdates(null)).toEqual({ ctab: null, csort: null, cf: null });
});
it("emits only non-default keys", () => {
const u = channelsParamUpdates({
view: "hidden",
sort: { key: "subscribers", dir: "desc" },
filters: { sync: "fully" },
});
expect(u.ctab).toBe("hidden");
expect(u.csort).toBe("subscribers:desc");
expect(u.cf).toBe(JSON.stringify({ sync: "fully" }));
});
it("drops empty filter entries so a cleared funnel emits no cf", () => {
expect(
channelsParamUpdates({ view: "subscribed", sort: null, filters: { tags: [], sync: "" } }).cf,
).toBeNull();
});
});
describe("round-trip", () => {
it("restores a rich state (scalar + array filters) from the emitted URL", () => {
const state: ChannelsUrlState = {
view: "blocked",
sort: { key: "priority", dir: "asc" },
filters: RICH_FILTERS,
};
const params = applyUpdates(channelsParamUpdates(state));
const back = channelsParamsToState(params, BASE);
expect(back.view).toBe("blocked");
expect(back.sort).toEqual({ key: "priority", dir: "asc" });
expect(back.filters).toEqual(RICH_FILTERS);
// the array shape survives the JSON round-trip, not collapsed to a scalar
expect(Array.isArray(back.filters.tags)).toBe(true);
});
it("keeps base values for absent keys (partial link)", () => {
const params = new URLSearchParams("csort=title:asc");
const back = channelsParamsToState(params, {
view: "discovery",
sort: null,
filters: { sync: "needsFull" },
});
expect(back.view).toBe("discovery"); // from base, no ctab
expect(back.filters).toEqual({ sync: "needsFull" }); // from base, no cf
expect(back.sort).toEqual({ key: "title", dir: "asc" });
});
it("ignores an out-of-vocabulary tab / bad sort / corrupt cf and falls back to base", () => {
const params = new URLSearchParams("ctab=bogus&csort=key:sideways&cf=notjson");
const back = channelsParamsToState(params, { view: "subscribed", sort: null, filters: {} });
expect(back.view).toBe("subscribed"); // bogus tab → base
expect(back.sort).toBeNull(); // bad dir → parseSortState null → base (null)
expect(back.filters).toEqual({}); // corrupt cf → decodeFilters heals to empty
});
});
describe("hasChannelsParams", () => {
it("detects any namespaced key", () => {
expect(hasChannelsParams(new URLSearchParams("ctab=hidden"))).toBe(true);
expect(hasChannelsParams(new URLSearchParams("cf=%7B%22sync%22%3A%22fully%22%7D"))).toBe(true);
});
it("ignores the feed's and plex's keys", () => {
expect(hasChannelsParams(new URLSearchParams("scope=all&pscope=movie&sort=oldest"))).toBe(
false,
);
});
});