Inside a Series/Author library folder, the back arrow was a no-op after a
cold start. `GroupHeader.handleBack` deleted the `group` query param, leaving
an empty search string; `router.replace('/library')` with an empty search
silently no-ops under the Next.js 16.2 static export (every non-web build).
This is the same root cause as #3782, which was fixed for the breadcrumb
"All" button in #3832 — but the series/author back button never got the
workaround.
It only reproduces after a cold start, when `groupBy` comes from settings
(not the URL) and sort/order/view are at defaults, so `group` is the only
query param; that is why it could not be reproduced within a session.
Fix: set `group=''` instead of deleting it (mirroring
`handleLibraryNavigation`). The resulting `/library?group=` commits, and the
existing cleanup effect in page.tsx strips the trailing empty `group=`.
Verified on-device (Android, WebView 148, static export): tapping back inside
an author folder now returns to the main list.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import GroupHeader from '@/app/library/components/GroupHeader';
|
||||
import { LibraryGroupByType } from '@/types/settings';
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({
|
||||
useTranslation: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useResponsiveSize', () => ({
|
||||
useResponsiveSize: (n: number) => n,
|
||||
}));
|
||||
|
||||
const routerStub = { push: vi.fn(), replace: vi.fn(), back: vi.fn() };
|
||||
let currentSearch = '';
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => routerStub,
|
||||
useSearchParams: () => new URLSearchParams(currentSearch),
|
||||
}));
|
||||
|
||||
const navigateToLibraryMock = vi.fn();
|
||||
vi.mock('@/utils/nav', () => ({
|
||||
navigateToLibrary: (...args: unknown[]) => navigateToLibraryMock(...args),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
navigateToLibraryMock.mockReset();
|
||||
});
|
||||
|
||||
describe('GroupHeader back button', () => {
|
||||
// Regression for #4437: inside a series/author folder after a cold start, the
|
||||
// URL is just `?group=X` (groupBy comes from settings, not the URL). Deleting
|
||||
// `group` would leave an empty query, and `router.replace('/library')` with an
|
||||
// empty search silently no-ops under the Next.js 16.2 static export (same root
|
||||
// cause as #3782, fixed for the breadcrumb "All" button in #3832). The back
|
||||
// button must keep the query non-empty via the `group=` workaround so the
|
||||
// navigation actually commits.
|
||||
it('keeps a non-empty query when group is the only param', () => {
|
||||
currentSearch = 'group=abc123';
|
||||
render(<GroupHeader groupBy={LibraryGroupByType.Series} groupName='My Series' />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back to library' }));
|
||||
|
||||
expect(navigateToLibraryMock).toHaveBeenCalledTimes(1);
|
||||
const query = navigateToLibraryMock.mock.calls[0]![1] as string;
|
||||
expect(query).not.toBe('');
|
||||
const params = new URLSearchParams(query);
|
||||
expect(params.has('group')).toBe(true);
|
||||
expect(params.get('group')).toBe('');
|
||||
});
|
||||
|
||||
it('preserves other params while clearing the group', () => {
|
||||
currentSearch = 'groupBy=author&sort=title&group=abc123';
|
||||
render(<GroupHeader groupBy={LibraryGroupByType.Author} groupName='Jane Doe' />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back to library' }));
|
||||
|
||||
const query = navigateToLibraryMock.mock.calls[0]![1] as string;
|
||||
const params = new URLSearchParams(query);
|
||||
expect(params.get('groupBy')).toBe('author');
|
||||
expect(params.get('sort')).toBe('title');
|
||||
expect(params.get('group')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,16 @@ const GroupHeader: React.FC<GroupHeaderProps> = ({ groupBy, groupName }) => {
|
||||
|
||||
const handleBack = () => {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.delete('group');
|
||||
// Set `group` to an empty string instead of deleting it. After a cold start
|
||||
// the URL inside a series/author folder is just `?group=X` (groupBy comes
|
||||
// from settings, not the URL), so deleting `group` would leave an empty
|
||||
// search string — and `router.replace('/library')` with an empty search
|
||||
// silently no-ops under the Next.js 16.2 static export, leaving the back
|
||||
// button dead (#4437). This mirrors the workaround in
|
||||
// `handleLibraryNavigation` (see page.tsx, originally #3782/#3832): the
|
||||
// resulting `/library?group=` does commit, and the trailing empty `group=`
|
||||
// is stripped cosmetically by the cleanup effect in page.tsx.
|
||||
params.set('group', '');
|
||||
navigateToLibrary(router, params.toString());
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user