Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187import { 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( <MainAreaContext.Provider value={makeStubContextValue(ctx.app)}> <MainAreaViewContainer app={ctx.app} /> </MainAreaContext.Provider>, ) 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( <MainAreaContext.Provider value={{ ...makeStubContextValue(ctx.app), onInitialViewReady: ready }}> <MainAreaViewContainer app={ctx.app} /> </MainAreaContext.Provider>, ) 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(<MainAreaViewContainer app={ctx.app} />) 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(<LeafMount leaf={leaf} />) 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(<LeafMount leaf={leaf} />) 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(<LeafMount leaf={leaf} />) 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(<MainAreaViewContainer app={ctx.app} />) 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 <div data-testid="file-handshake">{leaf.file?.path ?? 'Loading file'}</div> } 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(<MainAreaViewContainer app={ctx.app} />) 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') })})