// @ts-check // Section-specific feature code should not be fetched until its section is opened. const { test, expect } = require('@playwright/test'); 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' }); });