92 lines
5.6 KiB
JavaScript
92 lines
5.6 KiB
JavaScript
// @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/);
|
|
});
|
|
|
|
test('consistent image generation can be cancelled from its progress panel', async ({ page }) => {
|
|
let cancelled = false;
|
|
let cancelRequests = 0;
|
|
await page.route('**/api/characters*', route => route.fulfill({ json: { characters: [] } }));
|
|
await page.route('**/api/voices*', route => route.fulfill({ json: [] }));
|
|
await page.route('**/api/settings*', route => route.fulfill({ json: {} }));
|
|
await page.route('**/api/character-generate-consistent-images', route => route.fulfill({ json: { job_id: 'job-cancel' } }));
|
|
await page.route('**/api/character-generate-consistent-images/cancel/job-cancel', async route => {
|
|
cancelRequests += 1;
|
|
cancelled = true;
|
|
await route.fulfill({ json: { ok: true, cancelled: true } });
|
|
});
|
|
await page.route('**/api/character-generate-consistent-images/status/job-cancel', route => route.fulfill({
|
|
json: cancelled
|
|
? { stage: 'cancelled', detail: 'Cancelled', elapsed: 1, stale_seconds: 0, done: true, cancelled: true, error: 'Generation cancelled', result: null }
|
|
: { stage: 'identity', detail: 'Generating shared identity portrait…', elapsed: 0, stale_seconds: 0, done: false, error: null, result: null },
|
|
}));
|
|
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => typeof window._charAutoGenerateConsistentImages === 'function', null, { timeout: 15_000 });
|
|
await page.evaluate(() => {
|
|
window.__cancelRun = window._charAutoGenerateConsistentImages({
|
|
id: 'cancel-character', name: 'Cancel Character', book: 'Smoke Book',
|
|
sheet: { image_prompt: 'portrait', concept_art_prompt: 'reference' },
|
|
}, 'cinematic').catch(() => null);
|
|
});
|
|
await expect(page.locator('#cij-progress-panel')).toBeVisible({ timeout: 10_000 });
|
|
await page.locator('#cij-progress-panel .cij-progress-cancel').click();
|
|
await expect.poll(() => cancelRequests).toBe(1);
|
|
await expect(page.locator('#cij-progress-panel')).toHaveClass(/cij-progress-err/, { timeout: 5_000 });
|
|
});
|