tts-voice-creator-clone-and.../tests/voice-provenance.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

57 lines
3.6 KiB
JavaScript

// @ts-check
// Source & consent panel in the voice inspector (all /api routes mocked; no live writes).
const { test, expect } = require('./_guard');
const VOICE = { id: 'EN_F_Test', path: '/voices/EN_F_Test.wav', file_type: 'wav', duration: 8, lang: 'EN', gender: 'F', enabled: true, has_ref: true, transcript: 'hi' };
test('unknown badge, validation error text and save payload', async ({ page }) => {
/** @type {any[]} */ const puts = [];
await page.route('**/api/**', route => route.request().method() === 'GET' ? route.fulfill({ json: {} }) : route.fulfill({ json: { ok: true } }));
await page.route('**/api/voices?*', route => route.fulfill({ json: [VOICE] }));
await page.route('**/api/voices', route => route.fulfill({ json: [VOICE] }));
await page.route('**/api/voices/EN_F_Test/provenance', route => {
puts.push(route.request().postDataJSON());
return puts.length === 1
? route.fulfill({ status: 400, json: { detail: 'source URL must be http(s) without credentials' } })
: route.fulfill({ json: { ok: true, provenance: { ...puts[1], set_at: 'now' } } });
});
await page.route('**/static/dist/main.min.js*', r => r.abort());
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-voices'));
const row = page.locator('[data-id="EN_F_Test"]').first();
await expect(row.locator('.vl-prov-badge')).toHaveCount(0); // not in the list rows: crowded the play buttons
await row.locator('.vl-name').click();
await expect(page.locator('.vl-prov-badge').first()).toBeVisible(); // still shown in the inspector
const panel = page.locator('.insp-prov-panel');
await expect(panel.getByRole('heading', { name: 'Source & consent' })).toBeVisible();
await panel.getByLabel('Basis').selectOption('licensed');
await panel.getByLabel(/^Source/).fill('ftp://bad');
const save = panel.getByRole('button', { name: /Save source/ });
const box = await save.boundingBox();
expect(box && box.height).toBeGreaterThanOrEqual(44);
await save.click();
await expect(panel.getByRole('status')).toContainText('Save failed: source URL must be http(s)');
await panel.getByLabel(/^Source/).fill('https://example.org/v');
await save.click();
await expect(panel.getByRole('status')).toHaveText('Saved.');
expect(puts[1]).toMatchObject({ basis: 'licensed', source: 'https://example.org/v' });
});
test('record button explains missing microphone permission', async ({ page }) => {
await page.route('**/api/**', route => route.request().method() === 'GET' ? route.fulfill({ json: {} }) : route.fulfill({ json: { ok: true } }));
await page.route('**/api/voices*', route => route.fulfill({ json: [VOICE] }));
await page.route('**/static/dist/main.min.js*', r => r.abort());
await page.addInitScript(() => {
navigator.mediaDevices.getUserMedia = () => Promise.reject(Object.assign(new Error('x'), { name: 'NotAllowedError' }));
});
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-voices'));
await page.locator('[data-id="EN_F_Test"]').first().locator('.vl-name').click();
await page.getByRole('button', { name: 'Record spoken consent' }).click();
await expect(page.locator('.insp-prov-panel').getByRole('status')).toContainText('Microphone permission was denied');
});