feat(search): context-scoped search with filter chips and group order
Add a search scope (all/music/shows/movies) resolved from the entry route and adjustable via filter chips, threaded through the library store's search() into includeItemTypes. Results group by type in a user-configurable order, editable from settings. TRACES: UR-049 | DR-063, DR-064, DR-065; UR-050 | DR-066, DR-067
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Persisted search result group order.
|
||||
*
|
||||
* TRACES: UR-050 | DR-066 | UT-*
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, beforeAll, afterAll, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { DEFAULT_GROUP_ORDER } from "$lib/utils/searchScope";
|
||||
|
||||
const STORAGE_KEY = "jellytau-search-group-order";
|
||||
|
||||
// This jsdom setup doesn't expose localStorage, so stand in a minimal
|
||||
// implementation — the store only uses getItem/setItem.
|
||||
const store = new Map<string, string>();
|
||||
const localStorage = {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
clear: () => store.clear(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("searchGroupOrder", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("starts at the shipped default with nothing stored", async () => {
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
expect(get(searchGroupOrder)).toEqual([...DEFAULT_GROUP_ORDER]);
|
||||
});
|
||||
|
||||
it("loads a stored order", async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify(["tvShows", "movies", "songs", "albums", "artists"])
|
||||
);
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
expect(get(searchGroupOrder)).toEqual([
|
||||
"tvShows",
|
||||
"movies",
|
||||
"songs",
|
||||
"albums",
|
||||
"artists",
|
||||
]);
|
||||
});
|
||||
|
||||
it("appends groups a partial stored order does not mention", async () => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(["movies"]));
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
expect(get(searchGroupOrder)).toEqual([
|
||||
"movies",
|
||||
"songs",
|
||||
"albums",
|
||||
"artists",
|
||||
"tvShows",
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the default on corrupt stored JSON", async () => {
|
||||
localStorage.setItem(STORAGE_KEY, "{not json");
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
expect(get(searchGroupOrder)).toEqual([...DEFAULT_GROUP_ORDER]);
|
||||
});
|
||||
|
||||
it("persists a move so the order survives a restart", async () => {
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
searchGroupOrder.move("movies", -1);
|
||||
|
||||
expect(get(searchGroupOrder)).toEqual([
|
||||
"songs",
|
||||
"albums",
|
||||
"movies",
|
||||
"artists",
|
||||
"tvShows",
|
||||
]);
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)!)).toEqual([
|
||||
"songs",
|
||||
"albums",
|
||||
"movies",
|
||||
"artists",
|
||||
"tvShows",
|
||||
]);
|
||||
|
||||
// Simulate a fresh app start reading the same storage.
|
||||
vi.resetModules();
|
||||
const reloaded = await import("./searchGroupOrder");
|
||||
expect(get(reloaded.searchGroupOrder)).toEqual([
|
||||
"songs",
|
||||
"albums",
|
||||
"movies",
|
||||
"artists",
|
||||
"tvShows",
|
||||
]);
|
||||
});
|
||||
|
||||
it("persists a drag reorder", async () => {
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
searchGroupOrder.reorder(4, 0);
|
||||
expect(get(searchGroupOrder)).toEqual([
|
||||
"tvShows",
|
||||
"songs",
|
||||
"albums",
|
||||
"artists",
|
||||
"movies",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets to the shipped default", async () => {
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
searchGroupOrder.move("tvShows", -1);
|
||||
searchGroupOrder.reset();
|
||||
expect(get(searchGroupOrder)).toEqual([...DEFAULT_GROUP_ORDER]);
|
||||
});
|
||||
|
||||
it("normalizes an explicitly set order", async () => {
|
||||
const { searchGroupOrder } = await import("./searchGroupOrder");
|
||||
searchGroupOrder.set(["movies", "podcasts"] as never);
|
||||
expect(get(searchGroupOrder)).toEqual([
|
||||
"movies",
|
||||
"songs",
|
||||
"albums",
|
||||
"artists",
|
||||
"tvShows",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user