fix: Prevent Novel Genre Rendering Crashes

This commit is contained in:
Rajarshee Chatterjee
2026-07-29 15:43:00 +05:30
parent 9612bf6f89
commit eb12bdfd62
5 changed files with 96 additions and 27 deletions
+12 -8
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
FlatList,
Pressable,
@@ -19,6 +19,7 @@ import { NovelInfo } from '@database/types';
import { NovelStatus } from '@plugins/types';
import { translateNovelStatus } from '@utils/translateEnum';
import { showToast } from '@utils/showToast';
import { parseGenres } from '../utils/genres';
interface EditInfoModalProps {
theme: ThemeColors;
@@ -55,12 +56,15 @@ const EditInfoModalContent = ({
const [saving, setSaving] = useState(false);
const [newGenre, setNewGenre] = useState('');
const genres = useMemo(
() => parseGenres(novelInfo.genres),
[novelInfo.genres],
);
const removeTag = (t: string) => {
setNovelInfo(current => ({
...current,
genres: current.genres
?.split(',')
genres: parseGenres(current.genres)
.filter(item => item !== t)
.join(','),
}));
@@ -199,9 +203,9 @@ const EditInfoModalContent = ({
setNovelInfo(prevVal => ({
...prevVal,
genres: prevVal.genres
? `${prevVal.genres},` + newGenreTrimmed
: newGenreTrimmed,
genres: [...parseGenres(prevVal.genres), newGenreTrimmed].join(
',',
),
}));
setNewGenre('');
}}
@@ -210,11 +214,11 @@ const EditInfoModalContent = ({
style={styles.inputWrapper}
/>
{novelInfo.genres !== undefined && novelInfo.genres !== '' ? (
{genres.length > 0 ? (
<FlatList
style={getGenreListStyle()}
horizontal
data={novelInfo.genres?.split(',')}
data={genres}
keyExtractor={(_, index) => 'novelTag' + index}
renderItem={({ item }) => (
<GenreChip theme={theme} onPress={() => removeTag(item)}>
@@ -9,6 +9,7 @@ import { isMissingNovelCover } from '../../../../components/NovelCoverImage';
import { ThemeColors } from '@theme/types';
import { getString } from '@i18n/translations';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { parseGenres } from '../../utils/genres';
interface CoverImageProps {
children: React.ReactNode;
@@ -269,26 +270,29 @@ const TrackerButton = ({
const genreKeyExtractor = (_item: string, index: number) => 'genre' + index;
const NovelGenres = memo(
({ theme, genres }: { theme: ThemeColors; genres: string }) => {
const data = useMemo(() => genres.split(/,\s*/), [genres]);
const renderGenre = useCallback(
({ item }: { item: string }) => <Chip label={item} theme={theme} />,
[theme],
);
interface NovelGenresProps {
theme: ThemeColors;
genres?: string | null;
}
return (
<FlatList
contentContainerStyle={styles.genreContainer}
horizontal
data={data}
keyExtractor={genreKeyExtractor}
renderItem={renderGenre}
showsHorizontalScrollIndicator={false}
/>
);
},
);
const NovelGenres = memo(({ theme, genres }: NovelGenresProps) => {
const data = useMemo(() => parseGenres(genres), [genres]);
const renderGenre = useCallback(
({ item }: { item: string }) => <Chip label={item} theme={theme} />,
[theme],
);
return (
<FlatList
contentContainerStyle={styles.genreContainer}
horizontal
data={data}
keyExtractor={genreKeyExtractor}
renderItem={renderGenre}
showsHorizontalScrollIndicator={false}
/>
);
});
export {
NovelInfoContainer,
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react-native';
import type { ThemeColors } from '@theme/types';
import { NovelGenres } from '../NovelInfoComponents';
jest.mock('../../../../../components', () => {
const { Text } = require('react-native');
return {
Chip: ({ label }: { label: string }) => <Text>{label}</Text>,
NovelCoverImage: () => null,
};
});
const theme = {} as ThemeColors;
describe('NovelGenres', () => {
it('renders normalized genres', () => {
render(
<NovelGenres theme={theme} genres="Fantasy, Adventure, , Romance " />,
);
expect(screen.getByText('Fantasy')).toBeTruthy();
expect(screen.getByText('Adventure')).toBeTruthy();
expect(screen.getByText('Romance')).toBeTruthy();
});
it.each([
['missing', undefined],
['null', null],
['malformed', ['Fantasy'] as unknown as string],
])('does not crash for %s genres', (_case, genres) => {
expect(() =>
render(<NovelGenres theme={theme} genres={genres} />),
).not.toThrow();
});
});
@@ -0,0 +1,18 @@
import { parseGenres } from '../genres';
describe('parseGenres', () => {
it('normalizes comma-separated genres', () => {
expect(parseGenres('Fantasy, Adventure, , Romance ')).toEqual([
'Fantasy',
'Adventure',
'Romance',
]);
});
it.each([undefined, null, ['Fantasy'], { genre: 'Fantasy' }])(
'returns an empty list for malformed input: %p',
genres => {
expect(parseGenres(genres)).toEqual([]);
},
);
});
+7
View File
@@ -0,0 +1,7 @@
export const parseGenres = (genres: unknown): string[] =>
typeof genres === 'string'
? genres
.split(',')
.map(genre => genre.trim())
.filter(Boolean)
: [];