// @ts-check // Lightweight constrained-device regression: the browser's 4 GB profile must // reduce shared batch concurrency before multiple audio/image operations run. const { test, expect } = require('./_guard'); test('4 GB browser profile caps shared batch concurrency at two', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.runPool === 'function', null, { timeout: 15_000 }); const maxActive = await page.evaluate(async () => { Object.defineProperty(navigator, 'deviceMemory', { configurable: true, value: 4 }); let active = 0; let max = 0; await window.runPool(Array.from({ length: 8 }, (_, i) => i), async () => { active += 1; max = Math.max(max, active); await new Promise(resolve => setTimeout(resolve, 15)); active -= 1; }, 4); return max; }); expect(maxActive).toBeLessThanOrEqual(2); }); test('large character productions keep the rendered DOM paginated', async ({ page }) => { const characters = Array.from({ length: 250 }, (_, index) => ({ id: 'large-' + index, book: 'Large Smoke Book', name: 'Character ' + index, tags: '', voice: null, sheet: { gender: index % 2 ? 'F' : 'M' }, })); 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.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(50); await page.locator('#lib-chars-list .lib-chars-load-more').click(); await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(100); }); test('desktop navigation remains usable under Chrome CPU throttling', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'desktop', 'Chromium CDP throttling is a desktop measurement'); const cdp = await page.context().newCDPSession(page); await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); try { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 20_000 }); await page.evaluate(() => window.navTo('s-performance')); await expect(page.locator('#background-jobs-card')).toBeVisible({ timeout: 15_000 }); const metrics = await page.evaluate(() => ({ domNodes: document.getElementsByTagName('*').length, audioElements: document.querySelectorAll('audio').length, jsHeapBytes: performance.memory?.usedJSHeapSize || null, })); expect(metrics.domNodes).toBeLessThan(20_000); expect(metrics.audioElements).toBeLessThan(64); } finally { await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 }).catch(() => {}); await cdp.detach().catch(() => {}); } }); test('large character workload stays bounded under CPU throttling', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'desktop', 'Chromium CDP throttling is a desktop measurement'); const characters = Array.from({ length: 500 }, (_, index) => ({ id: 'workload-' + index, book: 'Bounded Workload', name: 'Workload Character ' + index, tags: '', voice: null, image: '/profile-' + index + '.png', sheet: { gender: index % 2 ? 'F' : 'M', physical: 'A character with a bounded test profile.' }, })); 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.addInitScript(() => { localStorage.setItem('ttsvc_section', 's-library'); localStorage.setItem('ttsvc_settings_seen', '1'); }); const cdp = await page.context().newCDPSession(page); await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 }); await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(50); const metrics = await page.evaluate(() => ({ domNodes: document.getElementsByTagName('*').length, audioElements: document.querySelectorAll('audio').length, renderedImages: document.querySelectorAll('#lib-chars-list img').length, imageCacheEntries: window.TTVC?.imageCache?.size?.() ?? null, jsHeapBytes: performance.memory?.usedJSHeapSize || null, })); expect(metrics.renderedImages).toBeLessThanOrEqual(64); expect(metrics.imageCacheEntries).toBeLessThanOrEqual(64); expect(metrics.audioElements).toBeLessThanOrEqual(32); expect(metrics.domNodes).toBeLessThan(5000); }); test('voice library cache reuses fresh data and honors explicit refresh', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.fetchVoiceLibrary === 'function', null, { timeout: 15_000 }); const result = await page.evaluate(async () => { sessionStorage.removeItem('ttsvc_voice_library_cache_v2'); let calls = 0; const originalFetch = window.fetch; window.fetch = async (url, options) => { if (String(url).startsWith('/api/voices')) { calls += 1; return new Response(JSON.stringify([{ id: 'cached-voice' }]), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return originalFetch(url, options); }; try { await window.fetchVoiceLibrary({ refresh: true }); await window.fetchVoiceLibrary(); await window.fetchVoiceLibrary({ refresh: true }); return calls; } finally { window.fetch = originalFetch; } }); expect(result).toBe(2); }); test('Rehearser audio cache stays bounded with LRU eviction', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); await page.evaluate(() => window.navTo('s-rehearser')); await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 }); const size = await page.evaluate(() => { const cache = window.rehState.synthCache; for (let index = 0; index < 180; index += 1) cache.set(index, new Blob(['audio-' + index])); cache.get(179); return cache.size; }); expect(size).toBeLessThanOrEqual(128); }); test('library image cache evicts decoded image sources but preserves restoration data', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => window.TTVC?.imageCache, null, { timeout: 15_000 }); const result = await page.evaluate(() => { const holder = document.createElement('div'); document.body.appendChild(holder); for (let index = 0; index < 90; index += 1) { const img = document.createElement('img'); img.src = '/api/voice/picture/cache-' + index; holder.appendChild(img); } window.TTVC.imageCache.observe(holder); const images = [...holder.querySelectorAll('img')]; const evicted = images.filter(img => !img.getAttribute('src')); const restorable = evicted.every(img => img.dataset.ttvcSrc); holder.remove(); return { size: window.TTVC.imageCache.size(), evicted: evicted.length, restorable }; }); expect(result.size).toBeLessThanOrEqual(64); expect(result.evicted).toBeGreaterThan(0); expect(result.restorable).toBeTruthy(); });