// @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/); });