Stable twin of the same fix on main. The dropdown rendered as `value="0"` and adminPhotos.js:1001 skips '0', so no category condition was applied and the whole event came back. The branch that does the work sits four lines below, keyed on the literal 'uncategorized' that nothing was sending. Silent by nature — a full list reads as 'nothing to narrow' rather than 'the filter did not run' — which is why it survived this long. The reporter in #1209 is on 3.46.4, so this is the branch that reaches them. Tests both ends of the contract, since the bug was the pairing rather than either half. Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
c685a3e931
commit
15c844db06
@@ -63,7 +63,12 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
|
||||
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-accent-dark"
|
||||
>
|
||||
<option value="">{t('gallery.allCategories', 'All Categories')}</option>
|
||||
<option value="0">{t('gallery.uncategorized', 'Uncategorized')}</option>
|
||||
{/* The literal the backend understands, not 0 (#1211). It skips
|
||||
'0' outright — `category_id !== '0'` — so this filter used to
|
||||
apply no condition at all and quietly returned the whole event.
|
||||
The onChange below passes non-numeric values through unchanged,
|
||||
so the string arrives intact. */}
|
||||
<option value="uncategorized">{t('gallery.uncategorized', 'Uncategorized')}</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* The category filter's wire values (#1211).
|
||||
*
|
||||
* "Uncategorized" was rendered as `value="0"`, and the backend skips `'0'`
|
||||
* outright (`adminPhotos.js`: `category_id !== '0'`), so the filter applied no
|
||||
* condition and returned the whole event. The value it does understand is the
|
||||
* literal `uncategorized`, four lines below that guard, which nothing sent.
|
||||
*
|
||||
* The failure was silent — a full list reads as "nothing to narrow" rather
|
||||
* than "the filter did not run" — so this pins the wire value rather than the
|
||||
* rendered label. Reported in #1209 by someone trying to isolate a few
|
||||
* thousand uncategorised imports to re-assign them.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PhotoFilters } from '../PhotoFilters';
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (_key: string, fallback?: any) => (typeof fallback === 'string' ? fallback : _key),
|
||||
i18n: { language: 'en' }
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const categories = [
|
||||
{ id: 3, name: 'Ceremony' },
|
||||
{ id: 4, name: 'Reception' },
|
||||
];
|
||||
|
||||
const renderFilters = (selectedCategory: number | string | null = null) => {
|
||||
const onCategoryChange = vi.fn();
|
||||
render(
|
||||
<PhotoFilters
|
||||
selectedCategory={selectedCategory}
|
||||
categories={categories as any}
|
||||
onCategoryChange={onCategoryChange}
|
||||
searchTerm=""
|
||||
onSearchChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
return { onCategoryChange };
|
||||
};
|
||||
|
||||
const categorySelect = () => screen.getAllByRole('combobox')[0];
|
||||
|
||||
describe('category filter wire values (#1211)', () => {
|
||||
it('sends the literal the backend understands for Uncategorized', async () => {
|
||||
const { onCategoryChange } = renderFilters();
|
||||
|
||||
await userEvent.selectOptions(categorySelect(), 'uncategorized');
|
||||
|
||||
// Not 0 — the backend drops that and returns everything.
|
||||
expect(onCategoryChange).toHaveBeenCalledWith('uncategorized');
|
||||
});
|
||||
|
||||
it('still sends a numeric id for a real category', async () => {
|
||||
const { onCategoryChange } = renderFilters();
|
||||
|
||||
await userEvent.selectOptions(categorySelect(), '3');
|
||||
|
||||
expect(onCategoryChange).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it('clears back to null for All Categories', async () => {
|
||||
const { onCategoryChange } = renderFilters('uncategorized');
|
||||
|
||||
await userEvent.selectOptions(categorySelect(), '');
|
||||
|
||||
expect(onCategoryChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('keeps Uncategorized selected once it is chosen', () => {
|
||||
renderFilters('uncategorized');
|
||||
// The select is controlled; if the option value and the state value ever
|
||||
// drift apart the control silently falls back to the first option.
|
||||
expect((categorySelect() as HTMLSelectElement).value).toBe('uncategorized');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user