// @ts-check // Source & consent panel in the voice inspector (all /api routes mocked; no live writes). const { test, expect } = require('@playwright/test'); 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'); });