/** * Playwright E2E: Anime Settings page * * Verifies the new flow after the rename from "NFO Diagnostics" to * "Anime Settings": * 1. Worker-scoped auth: login via API ONCE per worker and reuse the * JWT across tests (avoids the server's per-IP rate limit). * 2. Navigate to / * 3. Right-click on first .series-card * 4. Click "Anime Settings" in the context menu * 5. Verify navigation to /anime/settings?key=... * 6. Verify the settings form is populated with the series data * * Run with: `E2E_PASSWORD=... npx playwright test anime_settings_page.spec.js` */ import { test as base, expect } from '@playwright/test'; const BASE_URL = process.env.E2E_BASE_URL || 'http://127.0.0.1:8000'; const TEST_PASSWORD = process.env.E2E_PASSWORD; // Worker-scoped auth fixture: login once per worker, share the token // across all tests to avoid triggering the server's login rate limit. const test = base.extend({ authedPage: async ({ page, context }, use) => { test.skip(!TEST_PASSWORD, 'Set E2E_PASSWORD env var to run this test'); const resp = await context.request.post(`${BASE_URL}/api/auth/login`, { data: { password: TEST_PASSWORD }, }); // If the IP is locked out (429), skip the entire suite so the // user can wait for the lockout to expire. test.skip( resp.status() === 429, 'Server login rate-limited (429). Wait ~5 minutes.', ); expect(resp.status(), 'auth/login should succeed').toBe(200); const body = await resp.json(); const token = body.access_token; // Visit any page from this origin so we can write to localStorage await page.goto(`${BASE_URL}/login`); await page.evaluate((t) => { localStorage.setItem('access_token', t); }, token); await use(page); }, }); test.describe('Anime Settings page (right-click flow)', () => { test('right-click series card opens Anime Settings page', async ({ authedPage: page }) => { await page.goto(BASE_URL); // Wait for at least one series card to render await page.waitForSelector('.series-card', { timeout: 15000 }); // Right-click on the first series card const firstCard = page.locator('.series-card').first(); const key = await firstCard.getAttribute('data-key'); expect(key, 'series card must have data-key').toBeTruthy(); await firstCard.click({ button: 'right' }); // The custom context menu should appear with the renamed action const menuItem = page.locator('[data-action="anime-settings"]'); await expect(menuItem).toBeVisible({ timeout: 5000 }); // Click the menu item — should navigate to /anime/settings?key=... await menuItem.click(); await page.waitForURL(/\/anime\/settings/, { timeout: 10000 }); // The settings page should show the editor section (not loading/error) await expect(page.locator('#settings-section')).toBeVisible({ timeout: 10000, }); // The form input for name should be populated (i.e. not empty) const nameInput = page.locator('#field-name'); await expect(nameInput).toBeVisible(); const nameValue = await nameInput.inputValue(); expect(nameValue.length).toBeGreaterThan(0); // The URL should carry the key param const url = new URL(page.url()); expect(url.pathname).toBe('/anime/settings'); expect(url.searchParams.get('key')).toBe(key); }); test('direct navigation to /anime/settings?key=... works', async ({ authedPage: page }) => { await page.goto(BASE_URL); await page.waitForSelector('.series-card', { timeout: 15000 }); const firstCard = page.locator('.series-card').first(); const key = await firstCard.getAttribute('data-key'); expect(key).toBeTruthy(); await page.goto(`${BASE_URL}/anime/settings?key=${encodeURIComponent(key)}`); await expect(page.locator('#settings-section')).toBeVisible({ timeout: 10000, }); // Overview should show the key await expect(page.locator('#overview-key')).toContainText(key); }); test('legacy /settings/nfo URL redirects to /anime/settings', async ({ authedPage: page }) => { const resp = await page.goto(`${BASE_URL}/settings/nfo`, { waitUntil: 'load', }); // FastAPI RedirectResponse returns 301 (permanent) or 307 (temp) expect([301, 307, 200]).toContain(resp.status()); // Following the redirect should land on /anime/settings const finalPath = new URL(page.url()).pathname; // Allow trailing slash variants expect(['/anime/settings', '/anime/settings/']).toContain(finalPath); }); test('context menu no longer shows NFO Diagnostics', async ({ authedPage: page }) => { await page.goto(BASE_URL); await page.waitForSelector('.series-card', { timeout: 15000 }); const firstCard = page.locator('.series-card').first(); await firstCard.click({ button: 'right' }); // The legacy action should be gone const legacy = page.locator('[data-action="nfo-diagnostics"]'); await expect(legacy).toHaveCount(0); }); test('context menu shows Anime Settings action', async ({ authedPage: page }) => { await page.goto(BASE_URL); await page.waitForSelector('.series-card', { timeout: 15000 }); const firstCard = page.locator('.series-card').first(); await firstCard.click({ button: 'right' }); const menuItem = page.locator('[data-action="anime-settings"]'); await expect(menuItem).toBeVisible({ timeout: 5000 }); // Verify label says "Anime Settings" (not "NFO Diagnostics") await expect(menuItem).toContainText(/Anime Settings/); }); });