feat(anime): rename NFO Diagnostics to Anime Settings + add edit endpoints
Replaces the read-only 'NFO Diagnostics' page with a full per-anime
Settings page reached from the right-click context menu on series cards.
Users can now view and edit key, name, folder, tmdb_id, tvdb_id and site
for each anime; changes are persisted to the DB and optionally written
back to the NFO file or used to regenerate it.
Backend
- Rename NfoDiagnosticsResponse -> NfoSettingsResponse,
NfoSeriesDiagnostics -> NfoSeriesSettings
- Rename get_nfo_diagnostics -> get_nfo_settings,
repair_nfo -> repair_nfo_settings
- Fix nfo.py bug: repair was calling non-existent
update_series_nfo_status(); now uses update_nfo_status() and an
explicit AnimeSeriesService.update(nfo_path=...)
- New endpoints on /api/anime/{key}:
GET /settings -> AnimeSettingsResponse
PUT /settings -> AnimeSettingsResponse
(body: name/folder/tmdb_id/tvdb_id/site,
options: apply_to_nfo, rename_disk)
POST /regenerate-nfo -> AnimeSettingsRegenerateNfoResponse
- New Pydantic models: AnimeSettingsResponse,
AnimeSettingsUpdateRequest, AnimeSettingsRegenerateNfoResponse
- /anime/settings page route; /settings/nfo now 301-redirects to it
Frontend
- New AniWorld.AnimeSettingsManager JS module (single-page form,
no tabs) with public API init/loadSeries/saveSettings/regenerateNfo/
validateField/populateForm/showSaveSuccess/showError
- New anime-settings.html template + anime-settings.css
- Right-click menu: data-action 'nfo-diagnostics' replaced by
'anime-settings' (label 'Anime Settings'), navigates to
/anime/settings?key=...
- Library 'Open NFO Diagnostics' link renamed to 'Open Anime Settings'
Bug fix
- context-menu click handler was calling hide() BEFORE building the
navigation URL, which cleared currentSeriesKey to null and produced
/anime/settings?key=null. Captures the key into a local const first.
Regression-locked by tests/frontend/unit/context_menu.test.js.
Tests
- 21 new pytest tests in tests/api/test_anime_settings_endpoints.py
(GET/PUT/regenerate-nfo, auth, validation, nfo-repair bug regression)
- tests/api/test_nfo_endpoints.py trimmed to 6 focused tests
- 31 new Vitest unit tests for AnimeSettingsManager
- 5 new Vitest unit tests for ContextMenu (incl. source-invariant
regression guard for the hide()-before-key bug)
- 5 new Playwright E2E tests covering right-click, direct nav,
legacy /settings/nfo redirect, and context-menu labels
- New vitest.config.js (environment: happy-dom)
Docs
- Docs/API.md: new section 'Anime Settings Endpoints'
- Docs/CHANGELOG.md: documents the rename and the context-menu bug fix
Verified
- pytest: 27/27 (21 new + 6 trimmed nfo)
- vitest: 36/36 (31 anime-settings + 5 context-menu)
- playwright e2e: 5/5
This commit is contained in:
138
tests/frontend/e2e/anime_settings_page.spec.js
Normal file
138
tests/frontend/e2e/anime_settings_page.spec.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user