diff --git a/src/core/safe-open.test.ts b/src/core/safe-open.test.ts new file mode 100644 index 0000000..f5cc0de --- /dev/null +++ b/src/core/safe-open.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { safeOpen } from './safe-open.js' + +describe('safeOpen', () => { + it('returns ok true when open succeeds', async () => { + const openTarget = async (_target: string): Promise => {} + await expect(safeOpen(openTarget, 'https://example.com')).resolves.toEqual({ ok: true }) + }) + + it('returns ok false and preserves error message on failure', async () => { + const openTarget = async (_target: string): Promise => { + throw new Error('spawn xdg-open ENOENT') + } + await expect(safeOpen(openTarget, 'https://example.com')).resolves.toEqual({ + ok: false, + error: 'spawn xdg-open ENOENT', + }) + }) + + it('returns fallback message for non-Error throws', async () => { + const openTarget = async (_target: string): Promise => { + throw 'boom' + } + await expect(safeOpen(openTarget, 'https://example.com')).resolves.toEqual({ + ok: false, + error: 'Failed to open browser', + }) + }) +}) diff --git a/src/core/safe-open.ts b/src/core/safe-open.ts new file mode 100644 index 0000000..2a18e68 --- /dev/null +++ b/src/core/safe-open.ts @@ -0,0 +1,18 @@ +export interface SafeOpenResult { + ok: boolean + error?: string +} + +export type OpenTarget = (target: string) => Promise + +export async function safeOpen(openTarget: OpenTarget, target: string): Promise { + try { + await openTarget(target) + return { ok: true } + } catch (error: unknown) { + if (error instanceof Error && error.message) { + return { ok: false, error: error.message } + } + return { ok: false, error: 'Failed to open browser' } + } +} diff --git a/src/screens/GalleryDetail.tsx b/src/screens/GalleryDetail.tsx index 5953137..1630b1c 100644 --- a/src/screens/GalleryDetail.tsx +++ b/src/screens/GalleryDetail.tsx @@ -5,6 +5,7 @@ import open from 'open' import type { PixelmuseClient } from '../core/client.js' import type { Generation } from '../core/types.js' import { imageToBuffer, autoSave } from '../core/image.js' +import { safeOpen } from '../core/safe-open.js' import ImagePreview from '../components/ImagePreview.js' interface Props { @@ -49,7 +50,11 @@ export default function GalleryDetail({ client, generationId, back }: Props) { if (confirming) return if (input === 'd') setConfirming(true) if (input === 'o' && generation) { - open(`https://www.pixelmuse.studio/g/${generation.id}`) + void safeOpen(open, `https://www.pixelmuse.studio/g/${generation.id}`).then((result) => { + if (!result.ok) { + setError(result.error ?? 'Failed to open browser') + } + }) } })