The feedSortOptions test compared each option's default direction against SORT_DEFAULT_DIR — the implementation's own table, so a wrong entry passed — and that import was the export's only user outside its module, which kept knip from seeing it had become dead after F3. The test now states the directions, the table is private again.
134 lines
5.4 KiB
TypeScript
134 lines
5.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { feedSortOptions, sortYtResults, YT_SORT_KEYS, SORT_KEYS } from "./feedSort";
|
|
import type { Video } from "./api";
|
|
|
|
// The live-YouTube results view shipped with no sort control at all: the provider's order was the
|
|
// only order, "már 1 lapnyi találatnál is lenne értelme". These pin what the added control means —
|
|
// it reorders the page already on screen, and it only offers keys a search result actually carries.
|
|
const v = (over: Partial<Video>): Video => ({ id: over.title ?? "?", ...over }) as Video;
|
|
const titles = (list: Video[]) => list.map((x) => x.title);
|
|
|
|
const RESULTS: Video[] = [
|
|
v({
|
|
title: "b-old-short",
|
|
published_at: "2019-01-01T00:00:00Z",
|
|
view_count: 10,
|
|
duration_seconds: 60,
|
|
}),
|
|
v({
|
|
title: "a-new-long",
|
|
published_at: "2026-01-01T00:00:00Z",
|
|
view_count: 5,
|
|
duration_seconds: 6000,
|
|
}),
|
|
v({
|
|
title: "c-mid-mid",
|
|
published_at: "2022-01-01T00:00:00Z",
|
|
view_count: 99,
|
|
duration_seconds: 600,
|
|
}),
|
|
];
|
|
|
|
describe("YT_SORT_KEYS", () => {
|
|
it("offers only keys a live search result can be ordered by", () => {
|
|
// `subscribers`/`priority` describe a SUBSCRIPTION, which a search hit is not, and `shuffle` on a
|
|
// ranked result list is noise. An option that cannot mean anything here is worse than no option.
|
|
expect(YT_SORT_KEYS).not.toContain("subscribers");
|
|
expect(YT_SORT_KEYS).not.toContain("priority");
|
|
expect(YT_SORT_KEYS).not.toContain("shuffle");
|
|
// …and every key it does offer is part of the feed's one vocabulary, not a private spelling.
|
|
for (const k of YT_SORT_KEYS) {
|
|
if (k === "relevance") continue; // the provider's ranking; not in the wire-sort map
|
|
expect(SORT_KEYS).toContain(k);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("sortYtResults", () => {
|
|
it("leaves the provider's ranking alone on `relevance`", () => {
|
|
expect(titles(sortYtResults(RESULTS, "relevance", "desc"))).toEqual([
|
|
"b-old-short",
|
|
"a-new-long",
|
|
"c-mid-mid",
|
|
]);
|
|
});
|
|
|
|
it("orders by upload date in both directions", () => {
|
|
expect(titles(sortYtResults(RESULTS, "date", "desc"))).toEqual([
|
|
"a-new-long",
|
|
"c-mid-mid",
|
|
"b-old-short",
|
|
]);
|
|
expect(titles(sortYtResults(RESULTS, "date", "asc"))).toEqual([
|
|
"b-old-short",
|
|
"c-mid-mid",
|
|
"a-new-long",
|
|
]);
|
|
});
|
|
|
|
it("orders by views and by length", () => {
|
|
expect(titles(sortYtResults(RESULTS, "popular", "desc"))[0]).toBe("c-mid-mid");
|
|
expect(titles(sortYtResults(RESULTS, "duration", "desc"))[0]).toBe("a-new-long");
|
|
expect(titles(sortYtResults(RESULTS, "duration", "asc"))[0]).toBe("b-old-short");
|
|
});
|
|
|
|
it("orders by title alphabetically, not by code point", () => {
|
|
const accented = [v({ title: "Zebra" }), v({ title: "Ábel" }), v({ title: "alma" })];
|
|
expect(titles(sortYtResults(accented, "title", "asc"))).toEqual(["Ábel", "alma", "Zebra"]);
|
|
});
|
|
|
|
it("sinks results with no value to the END in both directions", () => {
|
|
// YouTube does not always report a duration; "unknown" is not "the shortest".
|
|
const mixed = [
|
|
v({ title: "unknown", duration_seconds: null }),
|
|
v({ title: "long", duration_seconds: 900 }),
|
|
v({ title: "short", duration_seconds: 60 }),
|
|
];
|
|
expect(titles(sortYtResults(mixed, "duration", "desc"))).toEqual(["long", "short", "unknown"]);
|
|
expect(titles(sortYtResults(mixed, "duration", "asc"))).toEqual(["short", "long", "unknown"]);
|
|
});
|
|
|
|
it("keeps the provider's ranking inside a tie, in both directions", () => {
|
|
// Flipping the direction must reverse the RANKING, not shuffle equally-ranked hits around.
|
|
const tied = [
|
|
v({ title: "first", view_count: 100 }),
|
|
v({ title: "second", view_count: 100 }),
|
|
v({ title: "third", view_count: 5 }),
|
|
];
|
|
expect(titles(sortYtResults(tied, "popular", "desc"))).toEqual(["first", "second", "third"]);
|
|
expect(titles(sortYtResults(tied, "popular", "asc"))).toEqual(["third", "first", "second"]);
|
|
});
|
|
|
|
it("does not reorder anything for a key it cannot honour", () => {
|
|
// A pref stored before this view offered a control can name `shuffle` or `priority`.
|
|
expect(titles(sortYtResults(RESULTS, "priority", "desc"))).toEqual(titles(RESULTS));
|
|
expect(titles(sortYtResults(RESULTS, "shuffle", "desc"))).toEqual(titles(RESULTS));
|
|
});
|
|
|
|
it("does not mutate the array it was given", () => {
|
|
const input = [...RESULTS];
|
|
sortYtResults(input, "date", "asc");
|
|
expect(titles(input)).toEqual(titles(RESULTS));
|
|
});
|
|
});
|
|
|
|
describe("feedSortOptions", () => {
|
|
// The library feed and the live results used to build their options from two copies of this
|
|
// mapping; one helper means a key cannot get a different default direction in one of the modes.
|
|
it("gives directional keys their default direction and marks shuffle/relevance directionless", () => {
|
|
// Literal directions, not the implementation's own table: comparing against that table let a
|
|
// wrong entry in it pass. Name is the one key that starts ascending.
|
|
const opts = feedSortOptions(
|
|
["relevance", "date", "duration", "title", "shuffle"],
|
|
(k) => `label:${k}`,
|
|
);
|
|
expect(opts.map((o) => [o.key, o.label, !!o.directionless, o.defaultDir])).toEqual([
|
|
["relevance", "label:relevance", true, undefined],
|
|
["date", "label:date", false, "desc"],
|
|
["duration", "label:duration", false, "desc"],
|
|
["title", "label:title", false, "asc"],
|
|
["shuffle", "label:shuffle", true, undefined],
|
|
]);
|
|
});
|
|
});
|