feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts / offline wide-probe fallback) and home-screen library shortcuts - add ArtistLinks component and shared navigation/genreDiversity utils - player/playback-mode refinements across Rust and frontend
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { selectDiverseGenres, sampleAcross } from "./genreDiversity";
|
||||
|
||||
const g = (...names: string[]) => names.map(name => ({ name }));
|
||||
|
||||
describe("sampleAcross", () => {
|
||||
it("returns input unchanged when at or under the count", () => {
|
||||
expect(sampleAcross([1, 2, 3], 5)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("spreads the sample across the whole list rather than taking a prefix", () => {
|
||||
// 26 letters, want ~7: a prefix would be A–G; striding spans A→Z.
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
const picked = sampleAcross(letters, 7);
|
||||
expect(picked[0]).toBe("A");
|
||||
// Reaches deep into the alphabet, not stuck near the front.
|
||||
expect(picked[picked.length - 1] >= "S").toBe(true);
|
||||
expect(picked.length).toBeLessThanOrEqual(7);
|
||||
});
|
||||
|
||||
it("never exceeds the requested count", () => {
|
||||
const items = Array.from({ length: 100 }, (_, i) => i);
|
||||
expect(sampleAcross(items, 8).length).toBeLessThanOrEqual(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectDiverseGenres", () => {
|
||||
it("returns input unchanged when at or under the limit", () => {
|
||||
const input = g("Rock", "Jazz");
|
||||
expect(selectDiverseGenres(input, 5)).toEqual(input);
|
||||
});
|
||||
|
||||
it("seeds with the most populous genre (first in input)", () => {
|
||||
const out = selectDiverseGenres(g("Rock", "Jazz", "Hip Hop"), 1);
|
||||
expect(out.map(x => x.name)).toEqual(["Rock"]);
|
||||
});
|
||||
|
||||
it("spreads the family instead of stacking near-synonyms", () => {
|
||||
// Count ranking puts all the Rock variants first; a naive top-N would
|
||||
// return four flavours of Rock. Diversity should pull in distinct genres.
|
||||
const input = g(
|
||||
"Rock",
|
||||
"Hard Rock",
|
||||
"Classic Rock",
|
||||
"Pop Rock",
|
||||
"Jazz",
|
||||
"Hip Hop",
|
||||
"Classical"
|
||||
);
|
||||
const names = selectDiverseGenres(input, 4).map(x => x.name);
|
||||
|
||||
expect(names[0]).toBe("Rock"); // seeded by count
|
||||
expect(names).toContain("Jazz");
|
||||
expect(names).toContain("Hip Hop");
|
||||
expect(names).toContain("Classical");
|
||||
// Only the seed represents the Rock cluster.
|
||||
expect(names.filter(n => n.includes("Rock"))).toEqual(["Rock"]);
|
||||
});
|
||||
|
||||
it("preserves count order as the tie-breaker among equally-distinct genres", () => {
|
||||
// All four are mutually distinct (no shared tokens), so every pick after
|
||||
// the seed is a distance tie and should follow input (count) order.
|
||||
const input = g("Rock", "Jazz", "Blues", "Folk");
|
||||
expect(selectDiverseGenres(input, 3).map(x => x.name)).toEqual([
|
||||
"Rock",
|
||||
"Jazz",
|
||||
"Blues",
|
||||
]);
|
||||
});
|
||||
|
||||
it("is case- and separator-insensitive when comparing", () => {
|
||||
const input = g("Hip Hop", "hip-hop", "Reggae");
|
||||
// "Hip Hop" and "hip-hop" share all tokens → the second is redundant.
|
||||
expect(selectDiverseGenres(input, 2).map(x => x.name)).toEqual([
|
||||
"Hip Hop",
|
||||
"Reggae",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user