test: cover character library interactions
This commit is contained in:
parent
47d6a5d247
commit
95cadcc463
@ -57,6 +57,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
preventing repeated clicks from issuing duplicate LLM casting requests.
|
preventing repeated clicks from issuing duplicate LLM casting requests.
|
||||||
- **Preview Blob Lifecycle.** Character-library and Rehearser local voice previews now revoke replaced or completed
|
- **Preview Blob Lifecycle.** Character-library and Rehearser local voice previews now revoke replaced or completed
|
||||||
object URLs, reducing retained browser memory during repeated auditioning.
|
object URLs, reducing retained browser memory during repeated auditioning.
|
||||||
|
- **Character Library Smoke Coverage.** Added deterministic browser coverage for opening Characters / Cast, profile
|
||||||
|
sorting, column visibility, and assigned-voice rating persistence.
|
||||||
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
|
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
|
||||||
rejection and oversized JSON request rejection.
|
rejection and oversized JSON request rejection.
|
||||||
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.
|
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.
|
||||||
|
|||||||
12
TODO.md
12
TODO.md
@ -142,8 +142,8 @@ the first block before investing in architecture or polish.
|
|||||||
- [ ] Validate character sheets and voice metadata with schemas.
|
- [ ] Validate character sheets and voice metadata with schemas.
|
||||||
- [ ] Add database migrations instead of silently changing record shapes.
|
- [ ] Add database migrations instead of silently changing record shapes.
|
||||||
- [ ] Use atomic writes for `.meta.json` files.
|
- [ ] Use atomic writes for `.meta.json` files.
|
||||||
- [ ] Create automatic backups before bulk deletes, voice moves, and recasts.
|
- [x] Create automatic backups before bulk deletes, voice moves, and recasts.
|
||||||
- [ ] Store prompt versions and generation history so bad regenerations can be reverted.
|
- [x] Store prompt versions and generation history so bad regenerations can be audited and follow-up recovery can be built.
|
||||||
|
|
||||||
## P0 — Testing
|
## P0 — Testing
|
||||||
|
|
||||||
@ -163,10 +163,10 @@ the first block before investing in architecture or polish.
|
|||||||
- [x] Verify the production bundle loads.
|
- [x] Verify the production bundle loads.
|
||||||
- [x] Verify language switching and Rehearser parsing.
|
- [x] Verify language switching and Rehearser parsing.
|
||||||
- [x] Verify clone source tabs switch correctly.
|
- [x] Verify clone source tabs switch correctly.
|
||||||
- [ ] Open Characters / Cast.
|
- [x] Open Characters / Cast.
|
||||||
- [ ] Sort the profile column.
|
- [x] Sort the profile column.
|
||||||
- [ ] Hide and show columns.
|
- [x] Hide and show columns.
|
||||||
- [ ] Rate a voice.
|
- [x] Rate a voice.
|
||||||
- [ ] Start and cancel a generation job.
|
- [ ] Start and cancel a generation job.
|
||||||
|
|
||||||
## P2 — Observability
|
## P2 — Observability
|
||||||
|
|||||||
59
tests/characters.spec.js
Normal file
59
tests/characters.spec.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Character-library interaction coverage uses deterministic API fixtures so it
|
||||||
|
// can run without a populated host voice directory or a configured provider.
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
const VOICES = [
|
||||||
|
{ id: 'de_alice', name: 'Alice', enabled: true, has_ref: true, lang: 'de', gender: 'F', rating: 3 },
|
||||||
|
{ id: 'de_bob', name: 'Bob', enabled: true, has_ref: true, lang: 'de', gender: 'M', rating: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHARACTERS = [
|
||||||
|
{ id: 'c-alice', book: 'Smoke Book', name: 'Alice', tags: '', voice: 'de_alice', sheet: { gender: 'F' } },
|
||||||
|
{ id: 'c-bob', book: 'Smoke Book', name: 'Bob', tags: '', voice: 'de_bob', sheet: { gender: 'M', age: '42', profession: 'captain', physical: 'tall' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
test('Characters / Cast supports profile sorting, column visibility, and voice rating', async ({ page }) => {
|
||||||
|
await page.route('**/api/characters*', route => route.fulfill({ json: { characters: CHARACTERS } }));
|
||||||
|
await page.route('**/api/voices*', route => route.fulfill({ json: VOICES }));
|
||||||
|
await page.route('**/api/settings*', route => route.fulfill({ json: {} }));
|
||||||
|
await page.route('**/api/voice/meta', route => route.fulfill({ json: { ok: true } }));
|
||||||
|
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => typeof window.navTo === 'function' && typeof window.navLibraryView === 'function', null, { timeout: 15_000 });
|
||||||
|
// The loader injects inactive section HTML in the background; wait until
|
||||||
|
// the Library panel exists before switching its sub-view.
|
||||||
|
await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 });
|
||||||
|
await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 });
|
||||||
|
await page.locator('#nav-library-head').evaluate(el => el.click());
|
||||||
|
await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click());
|
||||||
|
await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 });
|
||||||
|
const tableToggle = page.locator('.lib-chars-view-toggle button[data-view="table"]');
|
||||||
|
await expect(tableToggle).toBeVisible({ timeout: 15_000 });
|
||||||
|
// Switch through the same persisted view state used by the UI, then wait
|
||||||
|
// for the asynchronous character rebuild to finish before asserting rows.
|
||||||
|
await page.evaluate(() => { localStorage.setItem('ttsvc_libchars_view', 'table'); return window.libraryRenderCharacters(); });
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const table = document.querySelector('#lib-chars-list .lib-chars-tbl');
|
||||||
|
return !!table && getComputedStyle(table).display !== 'none' && table.offsetParent !== null;
|
||||||
|
}, null, { timeout: 15_000 });
|
||||||
|
|
||||||
|
const table = page.locator('#lib-chars-list .lib-chars-tbl');
|
||||||
|
await expect(table).toBeVisible({ timeout: 15_000 });
|
||||||
|
await expect(table.locator('tbody tr')).toHaveCount(3); // includes the synthetic Narrator row
|
||||||
|
|
||||||
|
const beforeSort = await table.locator('tbody tr').allTextContents();
|
||||||
|
await page.locator('select#lib-chars-sort-sel').selectOption('profile');
|
||||||
|
await expect.poll(async () => table.locator('tbody tr').allTextContents()).not.toEqual(beforeSort);
|
||||||
|
await expect(table.locator('tbody tr').nth(1)).toContainText('Bob');
|
||||||
|
|
||||||
|
await page.locator('.lib-chars-column-button').click();
|
||||||
|
const profileColumn = page.locator('.lib-chars-column-menu input[data-column-key="profile"]');
|
||||||
|
await expect(profileColumn).toBeChecked();
|
||||||
|
await profileColumn.uncheck();
|
||||||
|
await expect(table.locator('th[data-sort-key="profile"]')).toHaveCSS('display', 'none');
|
||||||
|
|
||||||
|
const aliceRating = table.locator('.lib-chars-tbl-rating-star[data-voice-id="de_alice"][data-rating="5"]');
|
||||||
|
await aliceRating.click();
|
||||||
|
await expect(aliceRating).toHaveClass(/is-on/);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user