import { test } from "node:test"; import assert from "node:assert/strict"; import { MISSING_STATES, VIDEO_STATES, type VideoState, } from "../lib/availability"; import { buildShareSearchParams, hasShareV1, parseShareV1, type ShareSelection, } from "./shareUrl"; import { emptySnapshot, snapshotStates, snapshotsEqual, statesFromLegacy, writeSnapshotStates, } from "./exportFilterStorage"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test common/components/shareUrl.test.ts // // The share schema is the one part of the filter state that outlives the tab it // was created in, so back-compat is a correctness property, not politeness: a // link somebody pasted into chat last year still has to resolve to what its // author selected. function selection(states: VideoState[]): ShareSelection { return { selectedChannels: new Set(["Chan A"]), videos: true, livestreams: true, allAges: true, restricted: true, states: new Set(states), tracks: new Set(), }; } function roundTrip(states: VideoState[]): Set { const params = buildShareSearchParams(selection(states), { q: "", mode: "transcripts", regex: false, }); const parsed = parseShareV1(`?${params.toString()}`, ["Chan A"]); return new Set(parsed.states); } test("build writes the v2 sentinel, and both versions are accepted", () => { const params = buildShareSearchParams(selection([...VIDEO_STATES]), { q: "", mode: "transcripts", regex: false, }); assert.equal(params.get("fv"), "2"); assert.ok(hasShareV1(`?${params.toString()}`)); assert.ok(hasShareV1("?fv=1&fav=a")); assert.ok(!hasShareV1("?fv=3")); assert.ok(!hasShareV1("?q=coffee")); }); test("every state round-trips on its own", () => { for (const state of VIDEO_STATES) { assert.deepEqual([...roundTrip([state])], [state], `state ${state}`); } }); test("the full set round-trips", () => { const out = roundTrip([...VIDEO_STATES]); assert.equal(out.size, VIDEO_STATES.length); for (const state of VIDEO_STATES) assert.ok(out.has(state)); }); test("an empty selection round-trips as empty, not as keep-all", () => { // The distinction the fv bump exists to preserve: "the author deselected // everything" must not decay into "the author selected nothing explicit". assert.equal(roundTrip([]).size, 0); }); test("a mixed selection round-trips exactly", () => { const picked: VideoState[] = ["available", "maybe_missing", "deleted"]; assert.deepEqual([...roundTrip(picked)].sort(), [...picked].sort()); }); // ─── v1 back-compat ─── test("a legacy fv=1 link that kept 'available' also keeps unconfirmed", () => { const sel = parseShareV1("?fv=1&fav=a", []); assert.deepEqual([...sel.states].sort(), ["available", "maybe_missing"]); }); test("a legacy fv=1 all-buckets link yields every state", () => { const sel = parseShareV1("?fv=1&fav=a&fav=u&fav=d", []); assert.equal(sel.states.size, VIDEO_STATES.length); for (const state of VIDEO_STATES) assert.ok(sel.states.has(state)); }); test("a legacy fv=1 'deleted' link covers the states v1 could not express", () => { const sel = parseShareV1("?fv=1&fav=d", []); assert.deepEqual( [...sel.states].sort(), ["deleted", "members_only", "private"], ); }); test("a v2 link with the same tokens as a v1 link means something different", () => { // fav=a&fav=u&fav=d is byte-identical across the two schemas, and the two // readings genuinely differ — which is the whole reason for the sentinel. const v1 = parseShareV1("?fv=1&fav=a&fav=u&fav=d", []); const v2 = parseShareV1("?fv=2&fav=a&fav=u&fav=d", []); assert.equal(v1.states.size, 6); assert.deepEqual([...v2.states].sort(), ["available", "deleted", "unlisted"]); }); // ─── stored snapshots ─── test("a snapshot with no availability keys keeps everything", () => { assert.equal(snapshotStates(null).size, VIDEO_STATES.length); assert.equal(snapshotStates(emptySnapshot()).size, VIDEO_STATES.length); }); test("legacy nav/nd/nu profiles migrate instead of resetting", () => { // nav dropped "available" back when unconfirmed videos counted as available, // so it has to drop maybe_missing with it. assert.deepEqual( [...statesFromLegacy({ nav: true })].sort(), ["deleted", "members_only", "private", "unlisted"], ); assert.ok(!statesFromLegacy({ nd: true }).has("deleted")); assert.ok(!statesFromLegacy({ nu: true }).has("unlisted")); assert.deepEqual( [...snapshotStates({ ...emptySnapshot(), nd: true, nu: true })].sort(), ["available", "maybe_missing", "members_only", "private"], ); }); test("av wins over the legacy booleans on the same snapshot", () => { const snap = { ...emptySnapshot(), av: ["deleted" as VideoState], nd: true }; assert.deepEqual([...snapshotStates(snap)], ["deleted"]); }); test("writeSnapshotStates omits av when everything is kept", () => { const snap = emptySnapshot(); writeSnapshotStates(snap, new Set(VIDEO_STATES)); assert.equal(snap.av, undefined); writeSnapshotStates(snap, new Set(MISSING_STATES)); assert.deepEqual(snap.av, [...MISSING_STATES].sort((a, b) => VIDEO_STATES.indexOf(a) - VIDEO_STATES.indexOf(b), )); }); test("snapshotsEqual compares through the resolved kept-set", () => { // A profile saved as legacy booleans and the same profile saved as `av` are // the same filter; treating them as different would show a phantom "unsaved // changes" state on load. const legacy = { ...emptySnapshot(), nd: true }; const modern = { ...emptySnapshot(), av: [...snapshotStates(legacy)] }; assert.ok(snapshotsEqual(legacy, modern)); assert.ok(!snapshotsEqual(legacy, emptySnapshot())); }); test("nop and nu survive a parse round-trip", () => { // Both were written by committedSnapshot but never read back, so they were // silently dropped on every reload. const snap = { ...emptySnapshot(), nop: true, nu: true }; assert.ok(!snapshotsEqual(snap, { ...emptySnapshot(), nu: true })); assert.ok(!snapshotsEqual(snap, { ...emptySnapshot(), nop: true })); });