fix: bound code-block clipboard copies
This commit is contained in:
66
src/utils/clipboardText.test.ts
Normal file
66
src/utils/clipboardText.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { writeClipboardText } from './clipboardText'
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: vi.fn(),
|
||||
}))
|
||||
|
||||
const originalClipboard = navigator.clipboard
|
||||
const mockInvoke = vi.mocked(invoke)
|
||||
|
||||
function setClipboard(writeText: ReturnType<typeof vi.fn> | undefined) {
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: writeText ? { writeText } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
describe('writeClipboardText', () => {
|
||||
beforeEach(() => {
|
||||
mockInvoke.mockReset()
|
||||
;(globalThis as { isTauri?: boolean }).isTauri = false
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: originalClipboard,
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the Web Clipboard API when it is available', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
setClipboard(writeText)
|
||||
|
||||
await writeClipboardText('copy me')
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('copy me')
|
||||
expect(mockInvoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prefers the Web Clipboard API inside Tauri', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
setClipboard(writeText)
|
||||
;(globalThis as { isTauri?: boolean }).isTauri = true
|
||||
|
||||
await writeClipboardText('copy me')
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('copy me')
|
||||
expect(mockInvoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the native command when Web Clipboard copy fails inside Tauri', async () => {
|
||||
const writeText = vi.fn().mockRejectedValue(new Error('permission denied'))
|
||||
setClipboard(writeText)
|
||||
;(globalThis as { isTauri?: boolean }).isTauri = true
|
||||
mockInvoke.mockResolvedValue(undefined)
|
||||
|
||||
await writeClipboardText('copy me')
|
||||
|
||||
expect(mockInvoke).toHaveBeenCalledWith('copy_text_to_clipboard', { text: 'copy me' })
|
||||
})
|
||||
|
||||
it('reports unavailable clipboard support outside Tauri', async () => {
|
||||
setClipboard(undefined)
|
||||
|
||||
await expect(writeClipboardText('copy me')).rejects.toThrow('Clipboard API is unavailable')
|
||||
})
|
||||
})
|
||||
38
src/utils/clipboardText.ts
Normal file
38
src/utils/clipboardText.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
|
||||
const NATIVE_COPY_TEXT_COMMAND = 'copy_text_to_clipboard'
|
||||
|
||||
type WebClipboardResult =
|
||||
| { status: 'copied' }
|
||||
| { status: 'failed'; error: unknown }
|
||||
| { status: 'unavailable' }
|
||||
|
||||
function hasWebClipboard(): boolean {
|
||||
return typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'
|
||||
}
|
||||
|
||||
async function writeWebClipboardText(text: string): Promise<WebClipboardResult> {
|
||||
if (!hasWebClipboard()) return { status: 'unavailable' }
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return { status: 'copied' }
|
||||
} catch (error) {
|
||||
return { status: 'failed', error }
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeClipboardText(text: string): Promise<void> {
|
||||
const webResult = await writeWebClipboardText(text)
|
||||
if (webResult.status === 'copied') return
|
||||
|
||||
if (isTauri()) {
|
||||
await invoke(NATIVE_COPY_TEXT_COMMAND, { text })
|
||||
return
|
||||
}
|
||||
|
||||
if (webResult.status === 'failed') throw webResult.error
|
||||
|
||||
throw new Error('Clipboard API is unavailable')
|
||||
}
|
||||
Reference in New Issue
Block a user