diff --git a/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx index 23531fc95f0..7a9bff8cbc9 100644 --- a/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { cleanup, render } from '@testing-library/react' +import { cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor' import type { FileContent } from './editor-panel-content-types' @@ -97,12 +97,14 @@ function renderEditPath({ content, language = 'markdown', viewMode = 'rich', - mode = 'edit' + mode = 'edit', + onOpenMarkdownPreview }: { content: string language?: 'markdown' | 'typescript' viewMode?: 'source' | 'rich' | 'preview' mode?: 'edit' | 'markdown-preview' + onOpenMarkdownPreview?: () => void }) { const activeFile = openFile(language, mode) const fileContents = { @@ -145,6 +147,7 @@ function renderEditPath({ handleSave={vi.fn()} handleSaveForFile={vi.fn()} reloadContent={vi.fn()} + onOpenMarkdownPreview={onOpenMarkdownPreview} /> ) @@ -211,12 +214,6 @@ describe('inline Markdown render classification', () => { }) it.each([ - { - name: 'source Markdown', - args: { content: '# Source', viewMode: 'source' as const }, - expectedView: 'source', - canExport: false - }, { name: 'Markdown preview', args: { content: '# Preview', mode: 'markdown-preview' as const }, @@ -238,6 +235,15 @@ describe('inline Markdown render classification', () => { expect(classifiers.exceedsSizeLimit).not.toHaveBeenCalled() }) + it('scans source Markdown edit tabs too, so the toggle knows whether rich mode would fall back', () => { + const result = renderEditPath({ content: '# Source', viewMode: 'source' as const }) + + expect(result.view.container.innerHTML).toContain('data-editor-view="source"') + expect(result.model.canExportMarkdownToPdf).toBe(false) + expect(classifiers.getUnsupportedMessage).toHaveBeenCalledTimes(1) + expect(classifiers.exceedsSizeLimit).toHaveBeenCalledTimes(1) + }) + it.each([ { name: 'Changes mode', args: { isChangesMode: true } }, { name: 'content that is still loading', args: { includeFileContent: false } }, @@ -301,4 +307,35 @@ describe('inline Markdown render classification', () => { expect(oversized.view.getByText(/File is larger than the .* rich editing limit/)).toBeTruthy() expect(oversized.view.getByText('Open anyway')).toBeTruthy() }) + + it('offers the Preview toggle once rich mode falls back to source for this content', () => { + const fallback = renderEditPath({ content: '[reference]: https://example.com' }) + expect(fallback.model.availableEditorToggleModes).toEqual([ + 'source', + 'rich', + 'preview', + 'changes' + ]) + + const normal = renderEditPath({ content: '# Ordinary content' }) + expect(normal.model.availableEditorToggleModes).toEqual(['source', 'rich', 'changes']) + }) + + it('opens the preview tab from the fallback banner action', () => { + const onOpenMarkdownPreview = vi.fn() + const { view } = renderEditPath({ + content: '[reference]: https://example.com', + onOpenMarkdownPreview + }) + + fireEvent.click(view.getByText('Open preview')) + + expect(onOpenMarkdownPreview).toHaveBeenCalledTimes(1) + }) + + it('hides the fallback banner preview action when no handler is provided', () => { + const { view } = renderEditPath({ content: '[reference]: https://example.com' }) + + expect(view.queryByText('Open preview')).toBeNull() + }) }) diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index 86527b25f29..2104b97c704 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -61,7 +61,8 @@ export function EditorContent({ handleDirtyStateHint, handleSave, handleSaveForFile, - reloadContent + reloadContent, + onOpenMarkdownPreview }: { activeFile: OpenFile viewStateScopeId: string @@ -90,6 +91,7 @@ export function EditorContent({ handleSave: (content: string) => Promise handleSaveForFile: (file: OpenFile, content: string) => Promise reloadContent: (file: OpenFile) => void + onOpenMarkdownPreview?: () => void }): React.JSX.Element { const editorViewStateKey = viewStateScopeId === activeFile.id @@ -253,6 +255,7 @@ export function EditorContent({ markdownDocuments={markdownDocuments} getConflictNavigation={getConflictNavigation} getMarkdownSourceLineOffset={getMarkdownSourceLineOffset} + onOpenMarkdownPreview={onOpenMarkdownPreview} handleContentChange={handleContentChange} handleDirtyStateHint={handleDirtyStateHint} handleSave={handleSave} diff --git a/src/renderer/src/components/editor/EditorEditFileSurface.tsx b/src/renderer/src/components/editor/EditorEditFileSurface.tsx index a9e3f8ff856..07f421cec3d 100644 --- a/src/renderer/src/components/editor/EditorEditFileSurface.tsx +++ b/src/renderer/src/components/editor/EditorEditFileSurface.tsx @@ -51,6 +51,7 @@ export function EditorEditFileSurface({ markdownDocuments, getConflictNavigation, getMarkdownSourceLineOffset, + onOpenMarkdownPreview, handleContentChange, handleDirtyStateHint, handleSave, @@ -82,6 +83,7 @@ export function EditorEditFileSurface({ markdownDocuments: MarkdownDocumentsController getConflictNavigation: (file: OpenFile, content: string) => EditorConflictNavigation | undefined getMarkdownSourceLineOffset: (frontMatterRaw: string) => number + onOpenMarkdownPreview?: () => void handleContentChange: (content: string) => void handleDirtyStateHint: (dirty: boolean) => void handleSave: (content: string) => Promise @@ -221,6 +223,7 @@ export function EditorEditFileSurface({ markdownAnnotationsEnabled={markdownAnnotationsEnabled} markdownDocuments={markdownDocuments} getMarkdownSourceLineOffset={getMarkdownSourceLineOffset} + onOpenMarkdownPreview={onOpenMarkdownPreview} handleContentChange={handleContentChange} handleDirtyStateHint={handleDirtyStateHint} monacoEditor={monacoEditor} diff --git a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx b/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx index 4db7c9b5045..b34c324ecc6 100644 --- a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx +++ b/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx @@ -25,6 +25,7 @@ export function EditorMarkdownFileSurface({ markdownAnnotationsEnabled, markdownDocuments, getMarkdownSourceLineOffset, + onOpenMarkdownPreview, handleContentChange, handleDirtyStateHint, monacoEditor @@ -41,6 +42,7 @@ export function EditorMarkdownFileSurface({ markdownAnnotationsEnabled: boolean markdownDocuments: MarkdownDocumentsController getMarkdownSourceLineOffset: (frontMatterRaw: string) => number + onOpenMarkdownPreview?: () => void handleContentChange: (content: string) => void handleDirtyStateHint: (dirty: boolean) => void monacoEditor: React.JSX.Element @@ -68,6 +70,17 @@ export function EditorMarkdownFileSurface({
{richFallbackMessage} + {onOpenMarkdownPreview ? ( + + ) : null} {isSizeFallback ? (