fix: the Latest feed's filter dropdowns opened empty
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m2s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m20s
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m2s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m20s
Operator: "the filters in the latest feed, drop down but don't have values". Two separate causes: - Platform: PostsFilterBar built its items from `platformsStore.platforms`. The platforms store has never had that property; it exposes `list` and `byKey`. The read returned undefined, `|| []` turned that into an empty list, and nothing failed. ArtistsView had copied the same read, so the Browse → Artists platform filter was empty too. Both now read `list` and show platform names rather than raw keys. - Artist: the autocomplete searched the server only after something was typed (autocomplete returns [] for an empty query by design, which its tests pin). Opening the dropdown therefore showed an empty menu. PostsFilterBar now loads every artist once from a new lightweight `GET /api/artists/names` (id, name, slug; alphabetical; no joins) and filters client-side, so the list is there on open. A deep-linked artist_id now also shows the artist's real name instead of "Artist #id". Guard: frontend/test/storeUsage.spec.js scans src for `platformsStore.<name>` and fails on any name the store doesn't define, since the frontend CI has no type-checker to catch this. A positive control shows the shipped `platformsStore.platforms` read is flagged, and a vacuity check confirms the scan really walks the tree. tests/test_api_artists_create.py covers /names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// Reads of a store property that does not exist.
|
||||
//
|
||||
// JavaScript returns `undefined` for a missing property instead of failing, and
|
||||
// the frontend CI has no type-checker to catch it. `platformsStore.platforms`
|
||||
// was read for months by the Latest feed's filter bar and the Artists view. The
|
||||
// store exposes `list` and `byKey`, so both platform dropdowns opened empty and
|
||||
// only someone clicking them noticed.
|
||||
//
|
||||
// This scans the source for `platformsStore.<name>` and checks each name
|
||||
// against what the store really defines.
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { usePlatformsStore } from '../src/stores/platforms.js'
|
||||
|
||||
const SRC = fileURLToPath(new URL('../src', import.meta.url))
|
||||
const READ = /\bplatformsStore\.([A-Za-z_$][\w$]*)/g
|
||||
|
||||
function sourceFiles (dir) {
|
||||
return readdirSync(dir).flatMap((name) => {
|
||||
const path = join(dir, name)
|
||||
if (statSync(path).isDirectory()) return sourceFiles(path)
|
||||
return /\.(vue|js)$/.test(name) ? [path] : []
|
||||
})
|
||||
}
|
||||
|
||||
function unknownReads (text, known) {
|
||||
return [...text.matchAll(READ)].map(m => m[1]).filter(name => !known.has(name))
|
||||
}
|
||||
|
||||
function storeKeys () {
|
||||
setActivePinia(createPinia())
|
||||
return new Set(Object.keys(usePlatformsStore()))
|
||||
}
|
||||
|
||||
describe('platforms store usage', () => {
|
||||
it('every platformsStore.<name> read in src names something the store defines', () => {
|
||||
const known = storeKeys()
|
||||
const offenders = sourceFiles(SRC).flatMap((file) =>
|
||||
unknownReads(readFileSync(file, 'utf8'), known)
|
||||
.map(name => `${file.slice(SRC.length + 1)}: platformsStore.${name}`),
|
||||
)
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
|
||||
it('the scan finds the read that shipped broken (positive control)', () => {
|
||||
const known = storeKeys()
|
||||
expect(known.has('list')).toBe(true)
|
||||
expect(unknownReads('(platformsStore.platforms || []).map(p => p)', known))
|
||||
.toEqual(['platforms'])
|
||||
})
|
||||
|
||||
it('the scan actually walks the source tree', () => {
|
||||
const hasRead = /\bplatformsStore\./
|
||||
const withReads = sourceFiles(SRC).filter(f => hasRead.test(readFileSync(f, 'utf8')))
|
||||
expect(withReads.length).toBeGreaterThan(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user