fix(library): restore back button from series/author folders (#4437) (#4629)

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:
Huang Xin
2026-06-17 13:50:14 +08:00
committed by GitHub
parent 5861aead4b
commit b31699d052
2 changed files with 76 additions and 1 deletions
@@ -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());
};