import { act, cleanup, render } from '@testing-library/react' import { afterEach, describe, it, expect, vi } from 'vitest' import { makeAppForTest, provisionMarkdownFile, provisionBinaryFile, TestView, } from '../../plugin/__tests__/test-helpers' import { MainAreaViewContainer, LeafMount } from '../MainAreaViewContainer' import { MainAreaContext, type MainAreaContextValue } from '../MainAreaContext' import type { App } from '../../plugin/app' import type { WorkspaceLeaf } from '../../plugin/workspace' import type { View } from '../../plugin/views/view' import { ItemView, type ReactItemView } from '../../plugin/views/item-view' import { createElement, type ReactNode } from 'react' // Stub provider value adequate for component-mount assertions. The full // MarkdownLeafBinding consumes useDocumentSync; for component tests that only // assert the dispatcher's behavior, a no-op handlers value suffices. function makeStubContextValue(app: App): MainAreaContextValue { return { wikilinkHandlers: { onNavigateWikilink: () => {}, resolveLink: () => true }, editorMode: 'live-preview', formattingActionManager: app.reamsFormattingActions, vault: app.vault, pluginApp: app, } } describe('MainAreaViewContainer', () => { afterEach(async () => { // Drain binding teardown updates before Vitest disposes the jsdom window. await act(async () => { cleanup() }) }) it('renders the React component wrapper for a ReactItemView leaf', async () => { const ctx = await makeAppForTest() const file = await provisionMarkdownFile(ctx, 'note.md', '# hi') const leaf = ctx.workspace.getLeaf(true) await leaf.openFile(file) // Silence any console.error from CM6 initialization in jsdom; the dispatcher // assertion below targets the wrapper element only. const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) try { const { container } = render( , ) expect(container.querySelector('[data-reams-view="markdown"]')).not.toBeNull() } finally { errSpy.mockRestore() } }) it('mounts containerEl for a DOM-mounted ItemView leaf', async () => { const ctx = await makeAppForTest() ctx.workspace.registerViewType('demo-view', (l) => { const v = new TestView(l, 'demo-view') v.containerEl.dataset.testview = 'demo' return v }) ctx.workspace.registerViewTypeForContentType('public.data', 'demo-view') const file = await provisionBinaryFile( ctx, 'blob.unknownext', 'application/octet-stream', new Uint8Array([0]), ) const leaf = ctx.workspace.getLeaf(true) await leaf.openFile(file) const ready = vi.fn(() => { expect(document.querySelector('[data-testview="demo"]')).not.toBeNull() }) const { container } = render( , ) expect(container.querySelector('[data-testview="demo"]')).not.toBeNull() expect(container.querySelector('[data-reams-view="demo-view"]')).not.toBeNull() expect(ready).toHaveBeenCalledWith(leaf) }) it('renders the empty state when no leaves exist', async () => { const ctx = await makeAppForTest() const { getByText } = render() expect(getByText(/Open a document to start editing/i)).toBeInTheDocument() }) // Rules-of-Hooks regression (R4). Toggle `leaf.view` between a real view // and null while the same `LeafMount` instance stays mounted. React throws // "Rendered fewer hooks than expected" when an early return shrinks the // hook count between renders; this test catches a regression where the // pre-fix LeafMount placed `if (!view) return null` between useRef and // useMemo/useEffect. it('tolerates leaf.view toggling between non-null and null without unmounting', async () => { const ctx = await makeAppForTest() ctx.workspace.registerViewType('demo-toggle', (l) => { const v = new TestView(l, 'demo-toggle') v.containerEl.dataset.toggleTestview = 'demo-toggle' return v }) ctx.workspace.registerViewTypeForContentType('public.data', 'demo-toggle') const file = await provisionBinaryFile( ctx, 'toggle.unknownext', 'application/octet-stream', new Uint8Array([0]), ) const leaf = ctx.workspace.getLeaf(true) as WorkspaceLeaf await leaf.openFile(file) const initialView = leaf.view expect(initialView).not.toBeNull() const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) try { const { rerender, container } = render() expect(container.querySelector('[data-reams-view="demo-toggle"]')).not.toBeNull() // Clear leaf.view (mimics the unregisterViewType cascade in // workspace.ts:2028) and re-render. The hook count must not change // between the two renders. ;(leaf as { view: View | null }).view = null rerender() expect(container.querySelector('[data-reams-view="demo-toggle"]')).toBeNull() // Restore and re-render again; no hook order errors should be logged. ;(leaf as { view: View | null }).view = initialView rerender() expect(container.querySelector('[data-reams-view="demo-toggle"]')).not.toBeNull() const hookErrors = errSpy.mock.calls .map((args) => String(args[0] ?? '')) .filter((msg) => /Rendered fewer hooks|hook order/i.test(msg)) expect(hookErrors).toEqual([]) } finally { errSpy.mockRestore() } }) it('returns from an editor tab to a React query tab with fresh view ownership', async () => { const ctx = await makeAppForTest() class QueryView extends ItemView implements ReactItemView { getViewType(): string { return 'query-view' } getDisplayText(): string { return 'Exercise' } getReactComponent(): ReactNode { return createElement('div', { 'data-testid': 'query-surface' }, 'Exercise query') } } const queryLeaf = ctx.workspace.getLeaf(true) queryLeaf.view = new QueryView(queryLeaf) const editorLeaf = ctx.workspace.getLeaf(true) ctx.workspace.setActiveLeaf(editorLeaf) ctx.workspace.registerViewType('query-view', leaf => new QueryView(leaf)) const { container } = render() expect(container.querySelector('.app-shell-empty')).not.toBeNull() act(() => ctx.workspace.setActiveLeaf(queryLeaf)) expect(container.querySelector('[data-testid="query-surface"]')).toHaveTextContent('Exercise query') act(() => ctx.workspace.setActiveLeaf(editorLeaf)) expect(container.querySelector('.app-shell-empty')).not.toBeNull() act(() => ctx.workspace.setActiveLeaf(queryLeaf)) expect(container.querySelector('[data-testid="query-surface"]')).toHaveTextContent('Exercise query') }) it('refreshes a React view element when its active leaf commits a file', async () => { const ctx = await makeAppForTest() const file = await provisionMarkdownFile(ctx, 'dates.md', 'Dates') function FilePanel({ leaf }: { leaf: WorkspaceLeaf }) { return
{leaf.file?.path ?? 'Loading file'}
} class FileView extends ItemView implements ReactItemView { getViewType(): string { return 'file-handshake' } getDisplayText(): string { return 'Dates' } getReactComponent(): ReactNode { return createElement(FilePanel, { leaf: this.leaf }) } } const leaf = ctx.workspace.getLeaf(true) leaf.view = new FileView(leaf) ctx.workspace.setActiveLeaf(leaf) const rendered = render() expect(rendered.getByTestId('file-handshake')).toHaveTextContent('Loading file') act(() => { leaf.file = file; ctx.workspace.trigger('active-leaf-change', leaf) }) expect(rendered.getByTestId('file-handshake')).toHaveTextContent('dates.md') }) })