feat: fruit search by name/synonym and type filter (story #06)
Backend: List repo query gains name (ILIKE with wildcard escaping on
name + synonym LEFT JOIN, SELECT DISTINCT) and types (ANY cast) params;
handler parses ?name= and ?type=, resolves combined-type aliases from
domain.FruitTypeAliases, validates plain types against validFruitTypes
to prevent Postgres enum cast errors. ORDER BY f.name, f.id for stable
pagination.
Frontend: listFruits gains optional {name, type} params; fruitStore adds
searchName/searchType state and setSearch action (resets offset, refetches);
FruitList.vue wires text input (debounced 300 ms + Enter) and flat type
dropdown (17 enum values + 4 aliases per spec §5 order); debounce timer
cleared on unmount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,18 +19,17 @@ type MockResponse = {
|
||||
const fetchMock = vi.fn((url: string, init?: RequestInit): Promise<MockResponse> => {
|
||||
const method = init?.method ?? 'GET'
|
||||
|
||||
if (url === '/api/v1/fruits?limit=50&offset=0' && method === 'GET') {
|
||||
if (url.startsWith('/api/v1/fruits?') && method === 'GET') {
|
||||
const params = new URLSearchParams(url.split('?')[1])
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ items: [], total: 0, limit: 50, offset: 0 }),
|
||||
})
|
||||
}
|
||||
if (url === '/api/v1/fruits?limit=10&offset=20' && method === 'GET') {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ items: [], total: 0, limit: 10, offset: 20 }),
|
||||
json: async () => ({
|
||||
items: [],
|
||||
total: 0,
|
||||
limit: Number(params.get('limit') ?? 50),
|
||||
offset: Number(params.get('offset') ?? 0),
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (url === '/api/v1/fruits/1' && method === 'GET') {
|
||||
@@ -99,9 +98,19 @@ describe('listFruits', () => {
|
||||
})
|
||||
|
||||
it('passes custom limit and offset', async () => {
|
||||
const result = await listFruits(10, 20)
|
||||
const result = await listFruits({ limit: 10, offset: 20 })
|
||||
expect(result.offset).toBe(20)
|
||||
})
|
||||
|
||||
it('appends name param when provided', async () => {
|
||||
await listFruits({ name: 'Boskop' })
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(expect.stringContaining('name=Boskop'))
|
||||
})
|
||||
|
||||
it('appends type param when provided', async () => {
|
||||
await listFruits({ type: 'Apfelsorten' })
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(expect.stringContaining('type=Apfelsorten'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFruit', () => {
|
||||
|
||||
Reference in New Issue
Block a user