Red while the mic is hot (live or recording), blue when off; label toggles Stop/Start microphone (DE+EN); pulse only without reduced motion; reset to blue when microphone access fails. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
55 lines
3.1 KiB
JavaScript
55 lines
3.1 KiB
JavaScript
// @ts-check
|
|
// Conversation mic button: red while hot, blue when off, aria toggles (all /api routes mocked).
|
|
const { test, expect } = require('./_guard');
|
|
|
|
async function boot(page, { denied = false } = {}) {
|
|
await page.route('**/api/**', route => route.request().method() === 'GET' ? route.fulfill({ json: {} }) : route.fulfill({ json: { ok: true } }));
|
|
await page.route('**/static/dist/main.min.js*', r => r.abort());
|
|
await page.addInitScript(d => {
|
|
const gum = d ? () => Promise.reject(new Error('denied')) : () => Promise.resolve({ getTracks: () => [{ stop() {} }] });
|
|
Object.defineProperty(navigator, 'mediaDevices', { value: { getUserMedia: gum }, configurable: true });
|
|
window.MediaRecorder = class { constructor() { this.state = 'inactive'; this.mimeType = 'audio/webm'; }
|
|
start() { this.state = 'recording'; } stop() { this.state = 'inactive'; setTimeout(() => this.onstop && this.onstop(), 20); } };
|
|
window.MediaRecorder.isTypeSupported = () => true;
|
|
}, denied);
|
|
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-conversation'));
|
|
}
|
|
const bg = async (/** @type {import('@playwright/test').Page} */ page) => { await page.mouse.move(0, 0); return page.locator('#conv-mic-btn').evaluate(e => getComputedStyle(e).backgroundColor); };
|
|
|
|
test('mic is blue and unpressed when off, red and pressed when hot, back to blue after stop', async ({ page }) => {
|
|
await boot(page);
|
|
const btn = page.locator('#conv-mic-btn');
|
|
await expect(btn).toHaveAttribute('aria-pressed', 'false');
|
|
await expect(btn).toHaveAttribute('aria-label', 'Start microphone');
|
|
const blue = await bg(page);
|
|
await btn.click();
|
|
await expect(btn).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(btn).toHaveAttribute('aria-label', 'Stop microphone');
|
|
await expect(page.locator('#conv-mic-status')).toHaveText('Listening…');
|
|
await expect.poll(() => bg(page)).not.toBe(blue);
|
|
await expect.poll(async () => { const [r, g, b] = (await bg(page)).match(/\d+/g).map(Number); return r > 200 && g < 90 && b < 90; }).toBe(true); // --red
|
|
await btn.click();
|
|
await expect(btn).toHaveAttribute('aria-pressed', 'false');
|
|
await expect(page.locator('#conv-mic-status')).toHaveText('Mic off');
|
|
await expect.poll(() => bg(page)).toBe(blue);
|
|
});
|
|
|
|
test('mic never stays red when microphone access is denied', async ({ page }) => {
|
|
await boot(page, { denied: true });
|
|
const btn = page.locator('#conv-mic-btn');
|
|
const blue = await bg(page);
|
|
await btn.click();
|
|
await expect(btn).toHaveAttribute('aria-pressed', 'false');
|
|
await expect(page.locator('#conv-mic-status')).toHaveText('Mic off');
|
|
await expect.poll(() => bg(page)).toBe(blue);
|
|
});
|
|
|
|
test('German mic labels', async ({ page }) => {
|
|
await page.addInitScript(() => { try { localStorage.setItem('app-lang', 'de'); } catch (e) {} });
|
|
await boot(page);
|
|
await expect(page.locator('#conv-mic-btn')).toHaveAttribute('aria-label', 'Mikrofon starten');
|
|
});
|