diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 7be77d2..f70a05b 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1470,7 +1470,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.50.0" +version = "0.51.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7e95428..e2246ba 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.50.0" +version = "0.51.0" edition = "2021" default-run = "mangalord" diff --git a/frontend/e2e/reader-chapter-select.spec.ts b/frontend/e2e/reader-chapter-select.spec.ts new file mode 100644 index 0000000..0313f63 --- /dev/null +++ b/frontend/e2e/reader-chapter-select.spec.ts @@ -0,0 +1,167 @@ +import { test, expect, type Page } from '@playwright/test'; + +const mangaId = '33333333-3333-3333-3333-333333333333'; +const chapter1Id = 'c1111111-3333-3333-3333-333333333333'; +const chapter2Id = 'c2222222-3333-3333-3333-333333333333'; +const chapter3Id = 'c3333333-3333-3333-3333-333333333333'; + +const mangaFixture = { + id: mangaId, + title: 'Vinland Saga', + author: 'Makoto Yukimura', + description: null, + cover_image_path: null, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z' +}; + +const chaptersFixture = [ + { + id: chapter1Id, + manga_id: mangaId, + number: 1, + title: 'Somewhere, Not Here', + page_count: 1, + created_at: '2026-01-01T00:00:00Z' + }, + { + id: chapter2Id, + manga_id: mangaId, + number: 2, + title: null, + page_count: 1, + created_at: '2026-01-02T00:00:00Z' + }, + { + id: chapter3Id, + manga_id: mangaId, + number: 3, + title: 'Sword Dance', + page_count: 1, + created_at: '2026-01-03T00:00:00Z' + } +]; + +function pageFixture(chapterId: string) { + return [ + { + id: `p1111111-${chapterId.slice(1, 8)}-3333-3333-333333333333`, + chapter_id: chapterId, + page_number: 1, + storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0001.png`, + content_type: 'image/png' + } + ]; +} + +async function mockReaderApis(page: Page) { + // Force public mode so the layout doesn't bounce anonymous visitors + // to /login (the dev backend on this machine runs with + // PRIVATE_MODE=true, which the layout's universal load respects). + await page.route('**/api/v1/auth/config', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ self_register_enabled: true, private_mode: false }) + }) + ); + await page.route('**/api/v1/auth/me', (route) => + route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) + }) + ); + await page.route('**/api/v1/auth/me/preferences', (route) => + route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) + }) + ); + await page.route('**/api/v1/me/bookmarks*', (route) => + route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) + }) + ); + await page.route(`**/api/v1/mangas/${mangaId}`, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(mangaFixture) + }) + ); + await page.route(new RegExp(`/api/v1/mangas/${mangaId}/chapters(\\?.*)?$`), (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + items: chaptersFixture, + page: { limit: 200, offset: 0, total: chaptersFixture.length } + }) + }) + ); + for (const c of chaptersFixture) { + await page.route(`**/api/v1/mangas/${mangaId}/chapters/${c.id}`, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(c) + }) + ); + await page.route( + `**/api/v1/mangas/${mangaId}/chapters/${c.id}/pages`, + (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ pages: pageFixture(c.id) }) + }) + ); + } + const png = Buffer.from( + '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63000100000005000158a3b62a0000000049454e44ae426082', + 'hex' + ); + await page.route('**/api/v1/files/**', (route) => + route.fulfill({ status: 200, contentType: 'image/png', body: png }) + ); +} + +test('reader chapter select lists every chapter with the manga-detail-style label', async ({ + page +}) => { + await mockReaderApis(page); + await page.goto(`/manga/${mangaId}/chapter/${chapter2Id}`); + + const select = page.getByTestId('reader-chapter-select'); + await expect(select).toBeVisible(); + + // The current chapter is preselected. + await expect(select).toHaveValue(chapter2Id); + + // Each chapter rendered as "Ch. N — Title" (or "Ch. N" when title is null), + // in ascending number order — matching the prev/next sort. + const labels = await select.locator('option').allTextContents(); + expect(labels.map((l) => l.trim())).toEqual([ + 'Ch. 1 — Somewhere, Not Here', + 'Ch. 2', + 'Ch. 3 — Sword Dance' + ]); +}); + +test('choosing a chapter from the select navigates to that chapter', async ({ page }) => { + await mockReaderApis(page); + await page.goto(`/manga/${mangaId}/chapter/${chapter1Id}`); + + await expect(page.getByTestId('reader-chapter-select')).toHaveValue(chapter1Id); + + await page.getByTestId('reader-chapter-select').selectOption(chapter3Id); + + await expect(page).toHaveURL( + new RegExp(`/manga/${mangaId}/chapter/${chapter3Id}$`) + ); + await expect(page.getByTestId('reader-chapter-select')).toHaveValue(chapter3Id); +}); diff --git a/frontend/package.json b/frontend/package.json index 36885c0..8a0188f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.50.0", + "version": "0.51.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte index 7681cac..bdcf968 100644 --- a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte +++ b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte @@ -459,6 +459,27 @@