feat(desktop): binary refusal page offers the system app first
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { MarkdownPreview } from './preview-file'
|
||||
const { openFileWithSystemApp } = vi.hoisted(() => ({ openFileWithSystemApp: vi.fn() }))
|
||||
|
||||
vi.mock('@/lib/office-files', () => ({ openFileWithSystemApp }))
|
||||
|
||||
import { LocalFilePreview, MarkdownPreview } from './preview-file'
|
||||
|
||||
// Behavior tests for the .md file preview renderer: input markdown goes
|
||||
// through normalizeFilePreviewMath -> Streamdown (+ KaTeX math plugin) and must
|
||||
@@ -51,3 +55,50 @@ describe('MarkdownPreview', () => {
|
||||
expect(anchor?.getAttribute('rel')).toBe('noopener noreferrer')
|
||||
})
|
||||
})
|
||||
|
||||
const binaryTarget = {
|
||||
binary: true,
|
||||
byteSize: 8_000,
|
||||
kind: 'file' as const,
|
||||
label: 'report.docx',
|
||||
language: 'text',
|
||||
large: false,
|
||||
path: '/w/report.docx',
|
||||
previewKind: 'binary' as const,
|
||||
source: '/w/report.docx',
|
||||
url: 'file:///w/report.docx'
|
||||
}
|
||||
|
||||
describe('LocalFilePreview binary refusal', () => {
|
||||
beforeEach(() => {
|
||||
openFileWithSystemApp.mockReset().mockResolvedValue(true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('makes the system app the primary action for binary files', () => {
|
||||
render(<LocalFilePreview reloadKey={0} target={binaryTarget} />)
|
||||
|
||||
const primary = screen.getByRole('button', { name: 'Open with system app' })
|
||||
|
||||
expect(primary).not.toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Preview anyway' })).not.toBeNull()
|
||||
|
||||
fireEvent.click(primary)
|
||||
expect(openFileWithSystemApp).toHaveBeenCalledWith('/w/report.docx')
|
||||
})
|
||||
|
||||
it('keeps Preview anyway primary for merely large text files', () => {
|
||||
render(
|
||||
<LocalFilePreview
|
||||
reloadKey={0}
|
||||
target={{ ...binaryTarget, binary: false, large: true, label: 'huge.log', path: '/w/huge.log' }}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Preview anyway' })).not.toBeNull()
|
||||
expect(screen.queryByRole('button', { name: 'Open with system app' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@ import { createMemoizedMathPlugin } from '@/lib/katex-memo'
|
||||
import { isComposerChord } from '@/lib/keybinds/chords'
|
||||
import { shikiLanguageForFilename } from '@/lib/markdown-code'
|
||||
import { normalizeFilePreviewMath } from '@/lib/markdown-preprocess'
|
||||
import { openFileWithSystemApp } from '@/lib/office-files'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { PreviewTarget } from '@/store/preview'
|
||||
import { setPreviewDirty } from '@/store/preview-edit'
|
||||
@@ -1034,11 +1035,19 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
|
||||
) {
|
||||
const binary = target.binary || state.binary
|
||||
const size = target.byteSize || state.byteSize
|
||||
const previewAnyway = () => setForcePreview(true)
|
||||
|
||||
return (
|
||||
<PreviewEmptyState
|
||||
body={binary ? t.preview.binaryBody(target.label) : t.preview.largeBody(target.label, formatBytes(size))}
|
||||
primaryAction={{ label: t.preview.previewAnyway, onClick: () => setForcePreview(true) }}
|
||||
// Binary files can't render as text — the system default app is the
|
||||
// useful way out; "Preview anyway" stays as the secondary escape hatch.
|
||||
primaryAction={
|
||||
binary
|
||||
? { label: t.preview.openWithSystemApp, onClick: () => void openFileWithSystemApp(filePath) }
|
||||
: { label: t.preview.previewAnyway, onClick: previewAnyway }
|
||||
}
|
||||
secondaryAction={binary ? { label: t.preview.previewAnyway, onClick: previewAnyway } : undefined}
|
||||
title={binary ? t.preview.binaryTitle : t.preview.largeTitle}
|
||||
tone="warning"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user