mirror of
https://github.com/FluxaMedia/fluxa-desktop.git
synced 2026-08-19 21:51:32 +00:00
Fix Discover catalog dropdown going empty on Movie/Series switch
discover.catalogs held whatever type the last fetch returned, so switching content type raced the async refetch: the auto-select effect could latch onto a stale catalog key from the previous type, which then matched nothing once the real catalogs arrived. Filter catalogs by the current contentType at the source so a mismatched entry can never be selected. Adds a Vitest + RTL harness (none existed) to drive the real DiscoverScreen through the exact race condition.
This commit is contained in:
parent
353f82de26
commit
18c99a66e2
6 changed files with 1354 additions and 5 deletions
1227
package-lock.json
generated
1227
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"test": "vitest run",
|
||||
"typecheck": "node scripts/gen-core-methods.mjs && tsc --noEmit",
|
||||
"build": "node scripts/gen-core-methods.mjs && tsc && vite build",
|
||||
"check": "npm run build && cd src-tauri && cargo check",
|
||||
|
|
@ -29,11 +30,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"esbuild": "^0.28.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.1"
|
||||
"vite": "^8.1.1",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
102
src/screens/DiscoverScreen.race.test.tsx
Normal file
102
src/screens/DiscoverScreen.race.test.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import React, { useState } from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DiscoverScreen } from './DiscoverScreen';
|
||||
import type { AppState, DiscoverCatalog } from '../core/types';
|
||||
|
||||
const MOVIE_CATALOGS: DiscoverCatalog[] = [
|
||||
{ key: 'cinemeta-movie-top', label: 'Cinemeta: Popular', type: 'movie', extras: [] },
|
||||
];
|
||||
|
||||
const SERIES_CATALOGS: DiscoverCatalog[] = [
|
||||
{ key: 'cinemeta-series-top', label: 'Cinemeta: Popular', type: 'series', extras: [] },
|
||||
];
|
||||
|
||||
function baseState(): AppState {
|
||||
return {
|
||||
navigation: { route: 'discover' },
|
||||
home: {} as AppState['home'],
|
||||
detail: {} as AppState['detail'],
|
||||
search: {} as AppState['search'],
|
||||
player: {} as AppState['player'],
|
||||
library: {} as AppState['library'],
|
||||
discover: { catalogs: MOVIE_CATALOGS, results: [], isLoading: false, catalogsLoading: false },
|
||||
addons: {
|
||||
installed: [{
|
||||
manifest: { catalogs: [{ type: 'movie' }, { type: 'series' }] },
|
||||
}] as unknown as AppState['addons']['installed'],
|
||||
},
|
||||
settings: {},
|
||||
} as unknown as AppState;
|
||||
}
|
||||
|
||||
function Harness({ catalogFetchDelayMs }: { catalogFetchDelayMs: number }) {
|
||||
const [state, setState] = useState<AppState>(baseState());
|
||||
|
||||
const onDispatch = (actionJson: string) => {
|
||||
const action = JSON.parse(actionJson) as { type: string; contentType?: string };
|
||||
if (action.type === 'discoverCatalogFiltersRequested') {
|
||||
const forType = action.contentType;
|
||||
setTimeout(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
discover: {
|
||||
...prev.discover,
|
||||
catalogs: forType === 'series' ? SERIES_CATALOGS : MOVIE_CATALOGS,
|
||||
},
|
||||
}));
|
||||
}, catalogFetchDelayMs);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DiscoverScreen
|
||||
state={state}
|
||||
onDispatch={onDispatch}
|
||||
onNavigateDetail={() => {}}
|
||||
onBack={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function openPopoverMenu() {
|
||||
return document.body.querySelector('.ui-popover') as HTMLElement;
|
||||
}
|
||||
|
||||
async function switchToSeries(user: ReturnType<typeof userEvent.setup>) {
|
||||
const typeDropdown = screen.getByText('Movie').closest('button')!;
|
||||
await user.click(typeDropdown);
|
||||
const menu = openPopoverMenu();
|
||||
await user.click(within(menu).getByText('Series'));
|
||||
}
|
||||
|
||||
describe('DiscoverScreen: switching content type', () => {
|
||||
it('shows the series catalog once the async catalog fetch resolves, never leaving the dropdown stuck empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness catalogFetchDelayMs={50} />);
|
||||
|
||||
await switchToSeries(user);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Cinemeta: Popular')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const catalogDropdown = screen.getByText('Cinemeta: Popular').closest('button')!;
|
||||
await user.click(catalogDropdown);
|
||||
const menu = openPopoverMenu();
|
||||
expect(within(menu).getByText('Cinemeta: Popular')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('never lets a stale movie catalog key survive the switch to Series (the actual bug)', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness catalogFetchDelayMs={10} />);
|
||||
|
||||
await switchToSeries(user);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('No content found')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Cinemeta: Popular')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -44,7 +44,7 @@ function DiscoverScreenInner({ state, onDispatch, onNavigateDetail, initialGenre
|
|||
const isGridScrollingRef = useRef(false);
|
||||
const scrollIdleTimerRef = useRef<number | null>(null);
|
||||
const hoveredMetaRef = useRef<Meta | null>(null);
|
||||
const catalogs = (discover.catalogs ?? []) as DiscoverCatalog[];
|
||||
const catalogs = ((discover.catalogs ?? []) as DiscoverCatalog[]).filter((catalog) => catalog.type === contentType);
|
||||
const selectedCatalog = catalogs.find((catalog) => catalog.key === selectedCatalogKey) ?? null;
|
||||
const selectedExtra = selectedCatalog?.extras?.[0] ?? null;
|
||||
const key = cacheKey(selectedCatalog?.key ?? null, selectedExtra?.name ?? null, extraValue);
|
||||
|
|
|
|||
9
src/test/setup.ts
Normal file
9
src/test/setup.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
window.ResizeObserver = ResizeObserverStub;
|
||||
11
vitest.config.ts
Normal file
11
vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue