From b31699d0523e005072a666cc56bb435bdf18cb08 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 17 Jun 2026 13:50:14 +0800 Subject: [PATCH] fix(library): restore back button from series/author folders (#4437) (#4629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../app/library/group-header.test.tsx | 66 +++++++++++++++++++ .../app/library/components/GroupHeader.tsx | 11 +++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/__tests__/app/library/group-header.test.tsx diff --git a/apps/readest-app/src/__tests__/app/library/group-header.test.tsx b/apps/readest-app/src/__tests__/app/library/group-header.test.tsx new file mode 100644 index 00000000..7eded996 --- /dev/null +++ b/apps/readest-app/src/__tests__/app/library/group-header.test.tsx @@ -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(); + + 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(); + + 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(''); + }); +}); diff --git a/apps/readest-app/src/app/library/components/GroupHeader.tsx b/apps/readest-app/src/app/library/components/GroupHeader.tsx index 77930cfc..87f0509d 100644 --- a/apps/readest-app/src/app/library/components/GroupHeader.tsx +++ b/apps/readest-app/src/app/library/components/GroupHeader.tsx @@ -23,7 +23,16 @@ const GroupHeader: React.FC = ({ 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()); };