tts-voice-creator-clone-and.../tests/lazy-loading.spec.js
mARTin-B78 e8c0df027e test: fail any spec that would write to the live API
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 21:00:25 +02:00

118 lines
6.8 KiB
JavaScript

// @ts-check
// Section-specific feature code should not be fetched until its section is opened.
const { test, expect } = require('./_guard');
test('heavy feature modules load on demand', async ({ page }) => {
const requests = [];
page.on('request', request => {
if (request.url().includes('/static/js/')) requests.push(request.url());
});
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
expect(requests.some(url => url.includes('/rehearser.js'))).toBeFalsy();
expect(requests.some(url => url.includes('/library-characters.js'))).toBeFalsy();
await page.evaluate(() => window.navTo('s-rehearser'));
await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 });
expect(requests.some(url => url.includes('/rehearser.js'))).toBeTruthy();
const countBeforeRepeat = requests.filter(url => url.includes('/rehearser.js')).length;
await page.evaluate(() => window.navTo('s-rehearser'));
await page.waitForTimeout(100);
expect(requests.filter(url => url.includes('/rehearser.js')).length).toBe(countBeforeRepeat);
});
test('Audiobook lazy modules publish their namespace APIs after loading', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.evaluate(() => window.navTo('s-reader'));
await page.waitForFunction(() => typeof window.TTVC?.audiobook?.core?.dialogueKey === 'function', null, { timeout: 15_000 });
await expect.poll(() => page.evaluate(() => ({
castView: typeof window.TTVC?.audiobook?.cast?.castView,
stream: typeof window.TTVC?.audiobook?.attributeStream,
startCast: typeof window.TTVC?.audiobook?.startCast,
pipelineCast: typeof window.TTVC?.audiobook?.pipeline?.cast,
core: typeof window.TTVC?.audiobook?.core?.dialogueKey,
preview: typeof window.TTVC?.audiobook?.preview?.show,
exportAudio: typeof window.TTVC?.audiobook?.exporting?.export,
}))).toEqual({ castView: 'function', stream: 'function', startCast: 'function', pipelineCast: 'function', core: 'function', preview: 'function', exportAudio: 'function' });
});
test('Audiobook recast merge preserves narration and merges only known dialogue pairs', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.evaluate(() => window.navTo('s-reader'));
await page.waitForFunction(() => typeof window.TTVC?.audiobook?.cast?.mergeAdjacentDialogue === 'function', null, { timeout: 15_000 });
await expect.poll(() => page.evaluate(() => {
const merge = window.TTVC.audiobook.cast.mergeAdjacentDialogue;
return merge([
{ type: 'narration', speaker: 'Narrator', text: 'A.' },
{ type: 'narration', speaker: 'Narrator', text: 'B.' },
{ type: 'dialogue', speaker: 'Mira', text: 'C.' },
{ type: 'dialogue', speaker: 'Mira', text: 'D.' },
{ type: 'dialogue', speaker: 'Unknown', text: 'E.' },
{ type: 'dialogue', speaker: 'Unknown', text: 'F.' },
]);
})).toEqual({
segments: [
{ type: 'narration', speaker: 'Narrator', text: 'A.' },
{ type: 'narration', speaker: 'Narrator', text: 'B.' },
{ type: 'dialogue', speaker: 'Mira', text: 'C.\n\nD.' },
{ type: 'dialogue', speaker: 'Unknown', text: 'E.' },
{ type: 'dialogue', speaker: 'Unknown', text: 'F.' },
],
merged: 1,
});
});
test('Rehearser playback module publishes transport APIs after loading', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.evaluate(() => window.navTo('s-rehearser'));
await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 });
await expect.poll(() => page.evaluate(() => ({
start: typeof window.TTVC?.rehearser?.playback?.start,
stop: typeof window.TTVC?.rehearser?.playback?.stop,
synthAll: typeof window.TTVC?.rehearser?.playback?.synthAll,
loadRecord: typeof window.TTVC?.rehearser?.io?.loadRecord,
exportSession: typeof window.TTVC?.rehearser?.io?.exportSession,
recording: typeof window.TTVC?.rehearser?.recording?.start,
training: typeof window.TTVC?.rehearser?.training?.enter,
core: typeof window.TTVC?.rehearser?.core?.buildScriptPage,
state: !!window.TTVC?.rehearser?.state,
}))).toEqual({ start: 'function', stop: 'function', synthAll: 'function', loadRecord: 'function', exportSession: 'function', recording: 'function', training: 'function', core: 'function', state: true });
});
test('pure leaf modules keep private declarations behind compatibility exports', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.evaluate(() => window.navTo('s-reader'));
await page.waitForFunction(() => typeof window.audiobookSplitByQuotes === 'function', null, { timeout: 15_000 });
await expect.poll(() => page.evaluate(() => ({
parse: typeof window.TTVC?.rehearser?.parse?.parseScript,
compare: typeof window.TTVC?.rehearser?.train?.compare,
dialogue: typeof window.TTVC?.audiobook?.parse?.audiobookHasDialogue,
split: typeof window.TTVC?.audiobook?.segmentation?.splitByQuotes,
}))).toEqual({ parse: 'function', compare: 'function', dialogue: 'function', split: 'function' });
});
test('initial cast normalization merges narration without fusing dialogue turns', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.evaluate(() => window.navTo('s-reader'));
await page.waitForFunction(() => typeof window.TTVC?.audiobook?.cast?.mergeAdjacentNarration === 'function', null, { timeout: 15_000 });
const result = await page.evaluate(() => window.TTVC.audiobook.cast.mergeAdjacentNarration([
{ type: 'narration', speaker: 'Narrator', page: 1, text: 'A.' },
{ type: 'narration', speaker: 'Narrator', page: 1, text: 'B.' },
{ type: 'dialogue', speaker: 'Mira', page: 1, text: 'C.' },
{ type: 'dialogue', speaker: 'Mira', page: 1, text: 'D.' },
]));
expect(result.map(s => s.text)).toEqual(['A.\n\nB.', 'C.', 'D.']);
});