fix(live-log): Fehlermeldung wenn keine Systemkamera vorhanden ist
Prüft videoinput-Geräte beim Öffnen des Foto-Modals und zeigt eine klare Meldung statt leerem Kamera-UI; getUserMedia-Fehler differenziert. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
cameraErrorKeyFromDomException,
|
||||
isCameraApiSupported,
|
||||
probeCameraAvailability
|
||||
} from './cameraAvailability.js'
|
||||
|
||||
describe('cameraAvailability', () => {
|
||||
it('detects missing camera API', () => {
|
||||
const nav = { mediaDevices: undefined }
|
||||
vi.stubGlobal('navigator', nav)
|
||||
expect(isCameraApiSupported()).toBe(false)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('returns none when no videoinput devices', async () => {
|
||||
vi.stubGlobal('navigator', {
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(),
|
||||
enumerateDevices: vi.fn().mockResolvedValue([
|
||||
{ kind: 'audioinput', deviceId: 'a1', label: '', groupId: '' }
|
||||
])
|
||||
}
|
||||
})
|
||||
await expect(probeCameraAvailability()).resolves.toBe('none')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('returns available when a videoinput exists', async () => {
|
||||
vi.stubGlobal('navigator', {
|
||||
mediaDevices: {
|
||||
getUserMedia: vi.fn(),
|
||||
enumerateDevices: vi.fn().mockResolvedValue([
|
||||
{ kind: 'videoinput', deviceId: 'v1', label: '', groupId: '' }
|
||||
])
|
||||
}
|
||||
})
|
||||
await expect(probeCameraAvailability()).resolves.toBe('available')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('maps NotFoundError to no-camera i18n key', () => {
|
||||
expect(cameraErrorKeyFromDomException(new DOMException('', 'NotFoundError'))).toBe(
|
||||
'logs.live_photo_no_camera'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
export type CameraAvailability = 'available' | 'none' | 'unsupported'
|
||||
|
||||
/** Whether the browser exposes camera APIs at all. */
|
||||
export function isCameraApiSupported(): boolean {
|
||||
return typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia
|
||||
}
|
||||
|
||||
/** Best-effort probe for at least one video input device (no permission prompt). */
|
||||
export async function probeCameraAvailability(): Promise<CameraAvailability> {
|
||||
if (!isCameraApiSupported()) return 'unsupported'
|
||||
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||
// Cannot list devices; defer to getUserMedia attempt in the capture UI.
|
||||
return 'available'
|
||||
}
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
if (devices.some((d) => d.kind === 'videoinput')) return 'available'
|
||||
return 'none'
|
||||
} catch {
|
||||
return 'none'
|
||||
}
|
||||
}
|
||||
|
||||
export function cameraErrorKeyFromDomException(err: unknown): string {
|
||||
const name = err instanceof DOMException ? err.name : ''
|
||||
if (name === 'NotFoundError' || name === 'OverconstrainedError') {
|
||||
return 'logs.live_photo_no_camera'
|
||||
}
|
||||
if (name === 'NotAllowedError' || name === 'NotReadableError' || name === 'SecurityError') {
|
||||
return 'logs.live_photo_camera_denied'
|
||||
}
|
||||
return 'logs.live_photo_camera_unavailable'
|
||||
}
|
||||
Reference in New Issue
Block a user