import { test, expect, type Page } from './fixtures'; // A partial upload failure (manga created, a chapter fails) must NOT create a // duplicate manga when the user retries — the second submit reuses the created // manga and only re-sends the failed chapter. const userFixture = { id: 'u1', username: 'uploader', created_at: '2026-01-01T00:00:00Z', is_admin: false }; const manga = { id: 'm9', title: 'Retry Saga', status: 'ongoing', description: null, authors: [], alt_titles: [], genres: [], cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }; async function stub(page: Page, counters: { mangaPosts: number; chapterPosts: number }) { await page.route('**/api/v1/auth/config', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ self_register_enabled: true, private_mode: false }) }) ); await page.route('**/api/v1/auth/me', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user: userFixture }) }) ); await page.route('**/api/v1/auth/me/preferences', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: '{}' }) ); await page.route('**/api/v1/genres', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: '[]' })); await page.route('**/api/v1/mangas', (r) => { if (r.request().method() === 'POST') { counters.mangaPosts += 1; return r.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(manga) }); } return r.fallback(); }); // Chapter POST: fail the first attempt, succeed on the retry. await page.route('**/api/v1/mangas/m9/chapters', (r) => { if (r.request().method() === 'POST') { counters.chapterPosts += 1; if (counters.chapterPosts === 1) { return r.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: { code: 'internal_error', message: 'boom' } }) }); } return r.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ id: 'c1', manga_id: 'm9', number: 1, title: null, page_count: 1, created_at: '2026-01-01T00:00:00Z' }) }); } return r.fallback(); }); // Post-success destination + its data. await page.route('**/api/v1/mangas/m9', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(manga) }) ); await page.route('**/api/v1/mangas/m9/chapters?*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 200, offset: 0, total: 0 } }) }) ); await page.route('**/api/v1/me/bookmarks*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); await page.route('**/api/v1/me/read-progress/m9', (r) => r.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: { code: 'not_found', message: 'x' } }) }) ); } test('retrying a partially-failed upload does not create a duplicate manga', async ({ page }) => { const counters = { mangaPosts: 0, chapterPosts: 0 }; await stub(page, counters); await page.goto('/upload'); await page.getByTestId('manga-title').fill('Retry Saga'); await page.getByTestId('add-chapter').click(); const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); await page .getByTestId('staged-chapter-pages-input') .setInputFiles([{ name: 'p.png', mimeType: 'image/png', buffer: png }]); // First submit: manga created, chapter fails → the retry note + relabeled button. await page.getByTestId('manga-submit').click(); await expect(page.getByTestId('manga-success')).toBeVisible(); await expect(page.getByTestId('manga-created-note')).toBeVisible(); await expect(page.getByTestId('manga-submit')).toHaveText('Retry failed chapters'); expect(counters.mangaPosts).toBe(1); // Retry: no second manga POST, chapter re-sent, navigate to the manga. await page.getByTestId('manga-submit').click(); await expect(page).toHaveURL(/\/manga\/m9$/); expect(counters.mangaPosts).toBe(1); expect(counters.chapterPosts).toBe(2); });