From 6a638f218bd9d7866fe23de05de8181033c86f70 Mon Sep 17 00:00:00 2001 From: "Michael E. Karpeles (Mek)" Date: Sun, 26 Apr 2026 15:47:33 -0700 Subject: [PATCH 1/2] docs: add FilterState typedef and annotate filter-passing functions closes #35 Adds a @typedef {Object} FilterState in utils/filters.js documenting all seven fields (sort, availability, fictionFilter, languages, genres, authors, subjects) with accepted value ranges. Annotates buildChips and buildSearchParams with @param {FilterState} and @returns. Four static-analysis tests verify the typedef and annotations exist so they stay in sync if the filter shape changes. --- frontend/src/utils/filters.js | 18 ++++++++++++++++++ frontend/src/utils/filters.test.js | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/frontend/src/utils/filters.js b/frontend/src/utils/filters.js index 9716df2..9f0a6f8 100644 --- a/frontend/src/utils/filters.js +++ b/frontend/src/utils/filters.js @@ -120,6 +120,17 @@ export const POPULAR_AUTHORS = [ 'Chimamanda Ngozi Adichie', 'Kazuo Ishiguro', 'Salman Rushdie', 'Milan Kundera', ]; +/** + * @typedef {Object} FilterState + * @property {string} sort - '' | 'new' | 'old' | 'rating desc' | 'readinglog desc' | 'title' + * @property {string} availability - '' | 'readable' | 'borrowable' | 'open' + * @property {string} fictionFilter - '' | 'fiction' | 'nonfiction' + * @property {string[]} languages - ISO 639-2/B codes e.g. ['eng', 'spa'] + * @property {string[]} genres - values from GENRE_OPTIONS e.g. ['mystery'] + * @property {string[]} authors - free-text author names + * @property {string[]} subjects - free-text subject names + */ + // DEFAULT_FILTERS is the canonical name — "EMPTY" was a misnomer because // availability defaults to 'readable', not to a truly empty state. export const DEFAULT_FILTERS = { @@ -175,6 +186,8 @@ export function shufflePick(arr, n) { * Derive the chip array from a filter state object. * Each chip label includes a human-readable "type: value" prefix. * Ordering: availability → fictionFilter → language → genre → author → subject + * @param {FilterState} filters + * @returns {{ type: string, label: string, value: string|null }[]} */ export function buildChips(filters) { const chips = []; @@ -223,6 +236,11 @@ export function buildChips(filters) { * Build URLSearchParams for a search request. * Multi-value arrays (languages, genres, authors, subjects) are each appended * as repeated params; the backend ORs them together within each facet. + * @param {string} q + * @param {FilterState} filters + * @param {number} page + * @param {number} limit + * @returns {URLSearchParams} */ export function buildSearchParams(q, filters, page, limit) { const p = new URLSearchParams(); diff --git a/frontend/src/utils/filters.test.js b/frontend/src/utils/filters.test.js index 1f9bd9b..2ce77f8 100644 --- a/frontend/src/utils/filters.test.js +++ b/frontend/src/utils/filters.test.js @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; import { toggleArrayValue, shufflePick, @@ -447,3 +448,24 @@ describe('DEFAULT_FILTERS', () => { expect(EMPTY_FILTERS).toBeDefined(); }); }); + +// ── FilterState typedef static-analysis ────────────────────────────────────── + +const filtersSrc = readFileSync(new URL('./filters.js', import.meta.url), 'utf8'); + +describe('FilterState typedef', () => { + it('@typedef {Object} FilterState is declared in filters.js', () => { + expect(filtersSrc).toMatch(/@typedef\s*\{Object\}\s*FilterState/); + }); + + it('all seven filter fields are documented with @property', () => { + for (const field of ['sort', 'availability', 'fictionFilter', 'languages', 'genres', 'authors', 'subjects']) { + expect(filtersSrc).toMatch(new RegExp(`@property.*${field}`)); + } + }); + + it('buildChips and buildSearchParams each have a @param {FilterState} annotation (2 total)', () => { + const matches = [...filtersSrc.matchAll(/@param\s*\{FilterState\}/g)]; + expect(matches.length).toBeGreaterThanOrEqual(2); + }); +}); From 029d07ad6de1615d86b3ad9b47a44c9839fb1eae Mon Sep 17 00:00:00 2001 From: "Michael E. Karpeles (Mek)" Date: Sun, 26 Apr 2026 16:58:14 -0700 Subject: [PATCH 2/2] fix: address Copilot review comments on PR #39 - FilterState @property annotations now use JSDoc string-literal unions for sort/availability/fictionFilter and mark array fields as optional ([languages], [genres], [authors], [subjects]) to match runtime usage - buildChips docstring corrected: availability and fiction chips use plain descriptive labels, not "type: value" prefix (only lang/genre/ author/subject use the prefix) - Added a 4th static-analysis test verifying literal-union types are present in the FilterState typedef --- frontend/src/utils/filters.js | 24 ++++++++++++++++-------- frontend/src/utils/filters.test.js | 10 ++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/frontend/src/utils/filters.js b/frontend/src/utils/filters.js index 9f0a6f8..48b7f11 100644 --- a/frontend/src/utils/filters.js +++ b/frontend/src/utils/filters.js @@ -122,13 +122,20 @@ export const POPULAR_AUTHORS = [ /** * @typedef {Object} FilterState - * @property {string} sort - '' | 'new' | 'old' | 'rating desc' | 'readinglog desc' | 'title' - * @property {string} availability - '' | 'readable' | 'borrowable' | 'open' - * @property {string} fictionFilter - '' | 'fiction' | 'nonfiction' - * @property {string[]} languages - ISO 639-2/B codes e.g. ['eng', 'spa'] - * @property {string[]} genres - values from GENRE_OPTIONS e.g. ['mystery'] - * @property {string[]} authors - free-text author names - * @property {string[]} subjects - free-text subject names + * @property {''|'new'|'old'|'rating desc'|'readinglog desc'|'title'} sort + * Sort value from SORT_OPTIONS. + * @property {''|'readable'|'borrowable'|'open'} availability + * Availability filter from AVAILABILITY_OPTIONS. + * @property {''|'fiction'|'nonfiction'} fictionFilter + * Fiction/nonfiction filter. + * @property {string[]} [languages] + * ISO 639-2/B codes e.g. ['eng', 'spa']. + * @property {string[]} [genres] + * Values from GENRE_OPTIONS e.g. ['mystery']. + * @property {string[]} [authors] + * Free-text author names. + * @property {string[]} [subjects] + * Free-text subject names. */ // DEFAULT_FILTERS is the canonical name — "EMPTY" was a misnomer because @@ -184,7 +191,8 @@ export function shufflePick(arr, n) { /** * Derive the chip array from a filter state object. - * Each chip label includes a human-readable "type: value" prefix. + * Chip labels are human-readable; language/genre/author/subject labels include a + * "type: value" prefix, while availability and fiction use plain descriptive labels. * Ordering: availability → fictionFilter → language → genre → author → subject * @param {FilterState} filters * @returns {{ type: string, label: string, value: string|null }[]} diff --git a/frontend/src/utils/filters.test.js b/frontend/src/utils/filters.test.js index 2ce77f8..e353cb9 100644 --- a/frontend/src/utils/filters.test.js +++ b/frontend/src/utils/filters.test.js @@ -468,4 +468,14 @@ describe('FilterState typedef', () => { const matches = [...filtersSrc.matchAll(/@param\s*\{FilterState\}/g)]; expect(matches.length).toBeGreaterThanOrEqual(2); }); + + it('FilterState uses literal-union types for sort, availability, and fictionFilter', () => { + const typedef = filtersSrc.slice( + filtersSrc.indexOf('@typedef {Object} FilterState'), + filtersSrc.indexOf('@typedef {Object} FilterState') + 600, + ); + expect(typedef).toMatch(/'new'.*'old'|'old'.*'new'/); // sort has at least two literals + expect(typedef).toMatch(/'readable'/); // availability literal + expect(typedef).toMatch(/'fiction'.*'nonfiction'|'nonfiction'.*'fiction'/); // fictionFilter literals + }); });