屏蔽 WebView 自带右键菜单(输入框除外);新增 useDismiss 统一修复菜单 点别处不消失(标签菜单实测中招、文件树菜单一并加固);侧栏页签行加收起 按钮、收起后编辑区左上角有展开按钮;编辑区右下角加源码/即时渲染切换按钮; 标签与文件树右键补保存/另存为/新窗口打开/导出/打印(针对目标文件, 未打开先打开再执行)。vitest 97 通过 + 2 跳过。
114 lines
5.0 KiB
TypeScript
114 lines
5.0 KiB
TypeScript
// 右键菜单回归(docs/21):标签菜单与文件树菜单的文件类动作、点别处关闭。
|
||
import { describe, it, expect, vi } from 'vitest'
|
||
import { createRoot } from 'react-dom/client'
|
||
import { act } from 'react-dom/test-utils'
|
||
|
||
vi.mock('../src/services/platform', async (importOriginal) => {
|
||
const orig = await importOriginal<typeof import('../src/services/platform')>()
|
||
return {
|
||
...orig,
|
||
listDir: async (dir: string) =>
|
||
dir === 'C:/ws' ? [{ name: 'a.md', path: 'C:/ws/a.md', isDir: false }] : [],
|
||
}
|
||
})
|
||
|
||
import TabBar from '../src/components/TabBar'
|
||
import Sidebar from '../src/components/Sidebar'
|
||
import { createTab } from '../src/stores/tabs'
|
||
|
||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||
|
||
function rightClick(el: Element) {
|
||
el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 10, clientY: 10 }))
|
||
}
|
||
function mouseDownOutside() {
|
||
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
||
}
|
||
function menuTexts(container: HTMLElement, selector: string) {
|
||
const menu = container.querySelector(selector)
|
||
return menu ? [...menu.querySelectorAll('button')].map((b) => b.textContent) : null
|
||
}
|
||
|
||
describe('标签右键菜单', () => {
|
||
it('含文件类动作;动作回调带标签 id;点别处关闭', async () => {
|
||
const container = document.createElement('div')
|
||
document.body.appendChild(container)
|
||
const root = createRoot(container)
|
||
const tab = createTab({ title: 'a', content: 'x', path: 'C:/a.md' })
|
||
const onSaveAsTab = vi.fn()
|
||
await act(async () => {
|
||
root.render(
|
||
<TabBar
|
||
tabs={[tab]} activeId={tab.id}
|
||
onActivate={() => undefined} onClose={() => undefined}
|
||
onCloseOthers={() => undefined} onCloseRight={() => undefined}
|
||
onNew={() => undefined} onMove={() => undefined}
|
||
onSaveAsTab={onSaveAsTab}
|
||
onOpenInNewWindowTab={() => undefined}
|
||
/>,
|
||
)
|
||
})
|
||
const tabEl = container.querySelector('.tab') as Element
|
||
await act(async () => { rightClick(tabEl) })
|
||
const texts = menuTexts(container, '.context-menu') ?? []
|
||
for (const label of ['另存为…', '在新窗口打开', '导出…', '打印 / 导出 PDF…', '关闭']) {
|
||
expect(texts.some((t) => t === label)).toBe(true)
|
||
}
|
||
// 点菜单外(文档任意处按下)→ 关闭
|
||
await act(async () => { mouseDownOutside() })
|
||
expect(container.querySelector('.context-menu')).toBeNull()
|
||
// 重新打开并点「另存为…」→ 回调触发且菜单关闭
|
||
await act(async () => { rightClick(tabEl) })
|
||
const saveAsBtn = [...container.querySelectorAll('.context-menu button')].find((b) => b.textContent === '另存为…')
|
||
await act(async () => { saveAsBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
||
expect(onSaveAsTab).toHaveBeenCalledWith(tab.id)
|
||
expect(container.querySelector('.context-menu')).toBeNull()
|
||
await act(async () => { root.unmount() })
|
||
container.remove()
|
||
})
|
||
})
|
||
|
||
describe('文件树右键菜单', () => {
|
||
it('文件条目含打开/新窗口/另存为/导出/打印;点别处关闭', async () => {
|
||
const container = document.createElement('div')
|
||
document.body.appendChild(container)
|
||
const root = createRoot(container)
|
||
const onExportEntry = vi.fn()
|
||
await act(async () => {
|
||
root.render(
|
||
<Sidebar
|
||
mode="files" onModeChange={() => undefined}
|
||
workspaceRoot="C:/ws" activePath={null}
|
||
onOpenFile={() => undefined} onOpenFolder={() => undefined}
|
||
outline={[]} onJumpHeading={() => undefined} tauriMode
|
||
onRenameEntry={() => undefined}
|
||
onOpenInNewWindowEntry={() => undefined}
|
||
onSaveAsEntry={() => undefined}
|
||
onExportEntry={onExportEntry}
|
||
onPrintEntry={() => undefined}
|
||
/>,
|
||
)
|
||
})
|
||
await act(async () => { await sleep(30) })
|
||
const item = [...container.querySelectorAll('.tree-item')].find((el) => el.textContent?.includes('a.md')) as Element
|
||
expect(item).toBeTruthy()
|
||
await act(async () => { rightClick(item) })
|
||
const texts = menuTexts(container, '.tree-context-menu') ?? []
|
||
for (const label of ['打开', '在新窗口打开', '另存为…', '导出…', '打印 / 导出 PDF…', '重命名…']) {
|
||
expect(texts.some((t) => t === label)).toBe(true)
|
||
}
|
||
const exportBtn = [...container.querySelectorAll('.tree-context-menu button')].find((b) => b.textContent === '导出…')
|
||
await act(async () => { exportBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
||
expect(onExportEntry).toHaveBeenCalledTimes(1)
|
||
expect(onExportEntry.mock.calls[0][0].path).toBe('C:/ws/a.md')
|
||
expect(container.querySelector('.tree-context-menu')).toBeNull()
|
||
// 再次打开后点别处关闭
|
||
await act(async () => { rightClick(item) })
|
||
expect(container.querySelector('.tree-context-menu')).not.toBeNull()
|
||
await act(async () => { mouseDownOutside() })
|
||
expect(container.querySelector('.tree-context-menu')).toBeNull()
|
||
await act(async () => { root.unmount() })
|
||
container.remove()
|
||
})
|
||
})
|