tts-voice-creator-clone-and.../tests/rehearser.spec.js
2026-10-06 11:37:16 +02:00

93 lines
4.4 KiB
JavaScript

// @ts-check
// Functional safety net for the Script Rehearser — guards the parser/cast pipeline
// while rehearser.js is split into smaller modules (#8).
const { test, expect } = require('./_guard');
const SCRIPT = `FADE IN
EXT. HARBOR - NIGHT
A ship rocks in the dark.
JACK
We need to find the compass.
GIBBS
Aye, Captain. But the tide is against us.
JACK
Then we sail against the tide.`;
async function navTo(page, section) {
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.evaluate((s) => window.navTo(s), section);
}
test('parses a screenplay and builds the cast', async ({ page }) => {
await page.route('**/api/voices*', route => route.fulfill({
json: [{ id: 'smoke_voice', name: 'Smoke voice', enabled: true }],
}));
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await navTo(page, 's-rehearser');
await page.waitForFunction(() => typeof window.navRehearserPhase === 'function', null, { timeout: 15_000 });
await expect.poll(() => page.evaluate(() => Boolean(
window.TTVC?.rehearser?.parse?.parseScript &&
window.TTVC?.rehearser?.stage?.renderCastStrip &&
window.TTVC?.rehearser?.voicePicker?.castOnlinePanelHtml &&
window.TTVC?.rehearser?.voicePicker?.searchOnline &&
window.TTVC?.rehearser?.cast?.renderCastList
))).toBe(true);
await page.evaluate(() => window.navRehearserPhase(1));
// Go to the Cast tab area: paste a script and parse
await page.waitForSelector('#reh-script-text', { state: 'attached', timeout: 10_000 });
await page.evaluate(() => window.navRehearserPhase(1));
await expect(page.locator('#reh-script-text')).toBeVisible({ timeout: 10_000 });
await page.fill('#reh-script-text', SCRIPT);
await page.click('#reh-parse-btn');
// Cast cards should appear: Narrator + JACK + GIBBS (title page / FADE IN filtered out)
const cards = page.locator('#reh-cast-list .reh-cast-card');
await expect(cards).toHaveCount(3, { timeout: 10_000 });
await expect(page.locator('#reh-cast-list')).toContainText('JACK');
await expect(page.locator('#reh-cast-list')).toContainText('GIBBS');
// Scene heading / FADE IN must NOT have become characters
await expect(page.locator('#reh-cast-list')).not.toContainText('FADE IN');
await expect(page.locator('#reh-cast-list')).not.toContainText('HARBOR');
// A "Hear a line" sample button appears once a character has a voice assigned
await expect(page.locator('.reh-cc-sample-btn')).toHaveCount(0);
const realVoiceId = await page.evaluate(async () => {
const response = await fetch('/api/voices');
const voices = await response.json();
return voices.find(v => v && v.id && v.enabled !== false)?.id || voices.find(v => v && v.id)?.id;
});
expect(realVoiceId).toBeTruthy();
await page.evaluate((voiceId) => {
rehState.cast['JACK'].voice = voiceId;
renderCastList();
}, realVoiceId);
await expect(page.locator('.reh-cc-sample-btn').first()).toContainText('Hear a line');
await expect(page.locator('.reh-cc-sample-btn').first()).toContainText('Hear a line');
});
test('ACT and SCENE headings accept digits, roman numerals and number words, but not plain prose', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await navTo(page, 's-rehearser');
await expect.poll(() => page.evaluate(() => Boolean(window.TTVC?.rehearser?.parse?.parseScript))).toBe(true);
const types = await page.evaluate(() => {
const kinds = (t) => window.TTVC.rehearser.parse.parseScript(t).map(l => l.type);
return {
act2: kinds('ACT 2\nIt is night.'), scene12: kinds('SCENE 12\nIt is night.'), act1title: kinds('ACT 1 - The Harbour\nIt is night.'),
roman: kinds('ACT II\nIt is night.'), sceneRoman: kinds('SCENE III\nIt is night.'), word: kinds('ACT ONE\nIt is night.'),
prose: kinds('ACT quickly now.\nIt is night.'), sceneProse: kinds('SCENE changes now.\nIt is night.'),
};
});
for (const k of ['act2', 'act1title', 'roman', 'word']) expect(types[k], k).toContain('act');
for (const k of ['scene12', 'sceneRoman']) expect(types[k], k).toContain('scene');
expect(types.prose).not.toContain('act'); // regression: the empty numeral used to make this a heading (Sonar S5842)
expect(types.sceneProse).not.toContain('scene');
});