Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/client/EditorHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,13 @@ export function EditorHost(props: {
{saveLabel !== '' && (
<span className={clsx(css.editorStatus, toolbar?.saveState === 'failed' && css.editorStatusError)}>{saveLabel}</span>
)}
{toolbar !== null && (
{/* Manual refresh is NOT toolbar-gated: viewers without a hoisted
toolbar (pdf / image / binary-download) see it too, so an on-disk
change to e.g. a PDF is one click away (issue #167 extends to every
file tab). The 'loading' pass unmounts the current viewer and the
fresh 'ready' pass remounts it, so even viewers that re-fetch by
path (PdfView) pick up the new bytes. */}
{!showEmpty && (
<button
type="button"
className={css.iconButton}
Expand Down
51 changes: 39 additions & 12 deletions tests/editor-refresh.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* re-runs the load (A), returning from edit to preview reloads unless the
* draft is dirty or the save failed (B), and a preview-mode save reloads on
* the 'saved' edge (C). A mock viewer reports a hoisted toolbar so the host
* chrome renders exactly like the real text editor.
* chrome renders exactly like the real text editor — and a toolbar-less
* viewer (pdf / image / binary-download) must see the same refresh button
* (D), since those previewers re-fetch on remount.
*/
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -32,31 +34,35 @@ function reads(): number {
interface MockViewerProps {
initialMode?: 'preview' | 'edit'
dirty?: boolean
/** Report a hoisted toolbar (text-editor-like); false = pdf/image-like. */
reportToolbar?: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onToolbarState?: (state: any) => void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onToolbarControls?: (controls: any) => void
}

/** A viewer that hoists a real toolbar (mode toggle + save state). */
function MockTextViewer({ initialMode = 'preview', dirty = false, onToolbarState, onToolbarControls }: MockViewerProps) {
function MockTextViewer({ initialMode = 'preview', dirty = false, reportToolbar = true, onToolbarState, onToolbarControls }: MockViewerProps) {
const [mode, setMode] = useState(initialMode)
const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved' | 'failed'>('idle')
useEffect(() => {
onToolbarState?.({ modes: true, mode, dirty, editable: true, saveState })
}, [mode, saveState, dirty, onToolbarState])
if (reportToolbar) onToolbarState?.({ modes: true, mode, dirty, editable: true, saveState })
}, [mode, saveState, dirty, onToolbarState, reportToolbar])
useEffect(() => {
onToolbarControls?.({
setMode,
// A synchronous save (saving then saved in one batch) drives the edge.
save: () => { setSaveState('saving'); setSaveState('saved') },
})
return () => { onToolbarControls?.(null) }
}, [onToolbarControls])
if (reportToolbar) {
onToolbarControls?.({
setMode,
// A synchronous save (saving then saved in one batch) drives the edge.
save: () => { setSaveState('saving'); setSaveState('saved') },
})
return () => { onToolbarControls?.(null) }
}
}, [onToolbarControls, reportToolbar])
return createElement('div', null, `mock-${mode}`)
}

function setup(initialMode: 'preview' | 'edit' = 'preview', dirty = false): {
function setup(initialMode: 'preview' | 'edit' = 'preview', dirty = false, reportToolbar = true): {
ctx: Context
fileTab: () => SidebarTab
} {
Expand All @@ -71,6 +77,7 @@ function setup(initialMode: 'preview' | 'edit' = 'preview', dirty = false): {
component: (props: FileViewerProps) => createElement(MockTextViewer, {
initialMode,
dirty,
reportToolbar,
onToolbarState: props.onToolbarState,
onToolbarControls: props.onToolbarControls,
}),
Expand Down Expand Up @@ -186,4 +193,24 @@ describe('EditorHost refresh (issue #167)', () => {
unmount()
}
})

it('D: the refresh button renders for a viewer without a toolbar (pdf/image-like) and re-runs the load', async () => {
const { ctx, fileTab } = setup('preview', false, false)
const { container, unmount } = mount(ctx, fileTab)
try {
await act(async () => { await Promise.resolve() })
expect(reads()).toBe(1)
const refresh = refreshButton(container)
expect(refresh).not.toBeNull()
// No toolbar -> the mode toggle / save controls stay absent, only the
// refresh (and the tree toggle) remain in the header.
expect(Array.from(container.querySelectorAll('button'))
.some(button => button.textContent === 'Preview')).toBe(false)
click(refresh!)
await act(async () => { await Promise.resolve() })
expect(reads()).toBe(2)
} finally {
unmount()
}
})
})