diff --git a/src/client/EditorHost.tsx b/src/client/EditorHost.tsx
index 9db60cf9d..d1157bb6d 100644
--- a/src/client/EditorHost.tsx
+++ b/src/client/EditorHost.tsx
@@ -436,7 +436,13 @@ export function EditorHost(props: {
{saveLabel !== '' && (
{saveLabel}
)}
- {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 && (
void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -39,24 +43,26 @@ interface MockViewerProps {
}
/** 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
} {
@@ -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,
}),
@@ -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()
+ }
+ })
})