import { test, expect, Page } from '@playwright/test'; import { switchTab, waitForHtmx, collectJsErrors } from './helpers'; /** * Download Flow E2E Tests * * These tests cover the complete user journey for discovering and downloading * anime/series content, including mocked provider flows and real file downloads. */ // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function getAuthToken(page: Page): Promise { return page.evaluate(() => localStorage.getItem('auth_token')); } async function createDownloadViaApi(page: Page, url: string): Promise { const token = await getAuthToken(page); if (!token) throw new Error('No auth token found'); const response = await page.request.post(`/api/anime/download?url=${encodeURIComponent(url)}`, { headers: { Authorization: `Bearer ${token}` }, }); expect(response.status()).toBeLessThan(400); const body = await response.json(); return body.task_id as string; } async function deleteDownloadViaApi(page: Page, taskId: string): Promise { const token = await getAuthToken(page); if (!token) return; await page.request.delete(`/api/downloads/${taskId}`, { headers: { Authorization: `Bearer ${token}` }, }); } // --------------------------------------------------------------------------- // Test: Episode picker + download toast (fully mocked) // --------------------------------------------------------------------------- test.describe('Download Flow E2E', () => { test('should choose episodes from search result and trigger download toast', async ({ page }) => { const jsErrors = collectJsErrors(page); // 1. Mock search results with a full card including dropdown await page.route('/api/anime/search?**', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: `
Frieren

Frieren: Beyond Journey's End

`, }); }); // 2. Mock episode list pointing to local static file for real download const testFileUrl = 'http://localhost:3000/static/test_download/test_episode_01.mp4'; await page.route('/api/anime/episodes?**', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: `

Frieren

1 épisodes disponibles
EP 1
Le départ
`, }); }); await page.goto('/web'); await switchTab(page, 'Anime'); await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 }); // Trigger search await page.fill('#animeSearchInput', 'Frieren'); await page.click('#tab-anime button[type="submit"]'); // Wait for search results await page.locator('#animeSearchResults .sr-card').first().waitFor({ state: 'visible', timeout: 10000 }); await expect(page.locator('#animeSearchResults')).toContainText("Frieren: Beyond Journey's End"); // Open dropdown await page.locator('#animeSearchResults .sr-card').first().locator('.sr-btn-dl').click(); await page.locator('.sr-dropdown-item:has-text("Choisir des episodes")').waitFor({ state: 'visible', timeout: 5000 }); // Click "Choisir des épisodes" await page.locator('.sr-dropdown-item:has-text("Choisir des episodes")').click(); // Wait for episode list await page.locator('#player-container .episode-item').first().waitFor({ state: 'visible', timeout: 10000 }); await expect(page.locator('#player-container')).toContainText('EP 1'); // Click download on first episode and wait for the real server response const [response] = await Promise.all([ page.waitForResponse( (resp) => resp.url().includes('/api/anime/download') && resp.request().method() === 'POST' ), page.locator('#player-container .episode-item').first() .locator('button[title="Télécharger cet épisode"]').click(), ]); expect(response.status()).toBeLessThan(400); // Wait for toast triggered by HX-Trigger header await page.locator('#toast-container .toast-success') .filter({ hasText: /Téléchargement lancé/i }) .waitFor({ state: 'visible', timeout: 8000 }); // Cleanup the created download task via API const body = await response.json(); if (body.task_id) { await deleteDownloadViaApi(page, body.task_id as string); } expect(jsErrors).toHaveLength(0); }); // --------------------------------------------------------------------------- // Test: Real file download via static fixture // --------------------------------------------------------------------------- test('should download a real file and show it in downloads list', async ({ page }) => { test.setTimeout(60000); const jsErrors = collectJsErrors(page); // Navigate first so localStorage is available on the correct origin await page.goto('/web'); // Use the static test file served by the app itself const testFileUrl = 'http://localhost:3000/static/test_download/test_episode_01.mp4'; // 1. Create download via API const taskId = await createDownloadViaApi(page, testFileUrl); // 2. Navigate to downloads tab await switchTab(page, 'Téléchargements'); await page.locator('#tab-downloads').waitFor({ state: 'visible', timeout: 5000 }); // 3. Wait for the task to appear in the list await page.locator('#downloads-container-inner .download-item').first().waitFor({ state: 'visible', timeout: 10000 }); // 4. Wait for completion (poll until status is completed) await expect(page.locator('#downloads-container-inner .download-item.status-completed')).toBeVisible({ timeout: 30000 }); // 5. Verify progress is 100% const progressText = await page.locator('#downloads-container-inner .download-item.status-completed .download-meta span').first().textContent(); expect(progressText).toContain('100'); // 6. Verify filename is shown await expect(page.locator('#downloads-container-inner .download-item .download-name')).toContainText('test_episode_01.mp4'); // 7. Verify completed actions are present (stream + download links) await expect(page.locator('#downloads-container-inner .download-item.status-completed a[title="Streamer"]')).toBeVisible(); await expect(page.locator('#downloads-container-inner .download-item.status-completed a[download]')).toBeVisible(); // Cleanup await deleteDownloadViaApi(page, taskId); expect(jsErrors).toHaveLength(0); }); // --------------------------------------------------------------------------- // Test: Click a new release on homepage // --------------------------------------------------------------------------- test('should click a new release and switch to anime search', async ({ page }) => { const jsErrors = collectJsErrors(page); // Mock releases with a single anime card await page.route('/api/releases/latest', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: `
Spy x Family
Anime-Sama Spy x Family
`, }); }); // Mock empty recommendations so they don't interfere await page.route('/api/recommendations', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: '

' }); }); await page.goto('/web'); // Wait for releases to load await page.locator('#releasesList .hc').first().waitFor({ state: 'visible', timeout: 10000 }); await expect(page.locator('#releasesList .hc-title')).toContainText('Spy x Family'); // Click the release card await page.locator('#releasesList .hc').first().click(); // Should switch to anime tab and populate search input await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 }); await expect(page.locator('#animeSearchInput')).toHaveValue('Spy x Family'); expect(jsErrors).toHaveLength(0); }); // --------------------------------------------------------------------------- // Test: Series search flow // --------------------------------------------------------------------------- test('should search for series and display results', async ({ page }) => { const jsErrors = collectJsErrors(page); await page.route('/api/series/search?**', async (route) => { await route.fulfill({ status: 200, contentType: 'text/html', body: `

Breaking Bad

A high school chemistry teacher turned methamphetamine producer.

Better Call Saul

The trials and tribulations of criminal lawyer Jimmy McGill.

`, }); }); await page.goto('/web'); await switchTab(page, 'Série'); await page.locator('#tab-series').waitFor({ state: 'visible', timeout: 5000 }); await page.fill('#seriesSearchInput', 'Breaking'); await page.click('#tab-series button[type="submit"]'); await page.locator('#seriesSearchResults .sr-card').first().waitFor({ state: 'visible', timeout: 10000 }); await expect(page.locator('#seriesSearchResults')).toContainText('Breaking Bad'); await expect(page.locator('#seriesSearchResults')).toContainText('Better Call Saul'); expect(jsErrors).toHaveLength(0); }); });