feat(files): 文件管理四项——侧栏拖拽移动、草稿跨窗口认领、未命名默认存根目录、打开所在位置(docs/25)
This commit is contained in:
@@ -99,3 +99,95 @@ describe('文件树', () => {
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
|
||||
describe('文件树拖拽移动', () => {
|
||||
const fireDrag = async (el: Element | undefined | null, type: string) => {
|
||||
if (!el) throw new Error(`元素不存在,无法派发 ${type}`)
|
||||
const e = new Event(type, { bubbles: true, cancelable: true }) as Event & { dataTransfer: Record<string, unknown> }
|
||||
e.dataTransfer = { effectAllowed: '', dropEffect: '', setData: () => undefined, getData: () => '' }
|
||||
await act(async () => { el.dispatchEvent(e) })
|
||||
}
|
||||
|
||||
it('把文件拖到文件夹上触发移动回调', async () => {
|
||||
const moves: { path: string; dest: string }[] = []
|
||||
const { container, root } = await renderSidebar({
|
||||
onMoveEntry: (entry: { path: string }, dest: string) => moves.push({ path: entry.path, dest }),
|
||||
})
|
||||
const file = findButton(container, 'a.md')
|
||||
const folder = findButton(container, 'sub')
|
||||
await fireDrag(file, 'dragstart')
|
||||
await fireDrag(folder, 'dragover')
|
||||
expect(folder?.className).toContain('drop-target')
|
||||
await fireDrag(folder, 'drop')
|
||||
expect(moves).toEqual([{ path: 'C:/ws/a.md', dest: 'C:/ws/sub' }])
|
||||
await act(async () => { root.unmount() })
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('拖回原文件夹(根目录)不触发移动', async () => {
|
||||
const moves: unknown[] = []
|
||||
const { container, root } = await renderSidebar({
|
||||
onMoveEntry: (...args: unknown[]) => moves.push(args),
|
||||
})
|
||||
const file = findButton(container, 'a.md')
|
||||
const head = container.querySelector('.filetree-head')
|
||||
await fireDrag(file, 'dragstart')
|
||||
await fireDrag(head, 'dragover')
|
||||
await fireDrag(head, 'drop')
|
||||
expect(moves).toEqual([])
|
||||
await act(async () => { root.unmount() })
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('子文件夹里的文件可以拖回根目录', async () => {
|
||||
const moves: { path: string; dest: string }[] = []
|
||||
const { container, root } = await renderSidebar({
|
||||
onMoveEntry: (entry: { path: string }, dest: string) => moves.push({ path: entry.path, dest }),
|
||||
})
|
||||
await act(async () => {
|
||||
findButton(container, 'sub')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
await act(async () => { await sleep(30) })
|
||||
const inner = findButton(container, 'b.md')
|
||||
const head = container.querySelector('.filetree-head')
|
||||
await fireDrag(inner, 'dragstart')
|
||||
await fireDrag(head, 'dragover')
|
||||
await fireDrag(head, 'drop')
|
||||
expect(moves).toEqual([{ path: 'C:/ws/sub/b.md', dest: 'C:/ws' }])
|
||||
await act(async () => { root.unmount() })
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('文件夹拖到自己身上不触发移动', async () => {
|
||||
const moves: unknown[] = []
|
||||
const { container, root } = await renderSidebar({
|
||||
onMoveEntry: (...args: unknown[]) => moves.push(args),
|
||||
})
|
||||
const folder = findButton(container, 'sub')
|
||||
await fireDrag(folder, 'dragstart')
|
||||
await fireDrag(folder, 'dragover')
|
||||
await fireDrag(folder, 'drop')
|
||||
expect(moves).toEqual([])
|
||||
await act(async () => { root.unmount() })
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('右键菜单含「打开所在位置」并回传条目', async () => {
|
||||
const revealed: string[] = []
|
||||
const { container, root } = await renderSidebar({
|
||||
onRevealEntry: (entry: { path: string }) => revealed.push(entry.path),
|
||||
})
|
||||
await act(async () => {
|
||||
findButton(container, 'a.md')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true }))
|
||||
})
|
||||
await act(async () => { await sleep(10) })
|
||||
const item = findButton(container, '打开所在位置')
|
||||
expect(item).toBeTruthy()
|
||||
await act(async () => {
|
||||
item?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
expect(revealed).toEqual(['C:/ws/a.md'])
|
||||
await act(async () => { root.unmount() })
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,3 +37,22 @@ describe('路径工具', () => {
|
||||
expect(isMarkdownPath('a.txt')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('路径助手(拖放/改名校验)', () => {
|
||||
it('分隔符混用也能判等与取父目录', async () => {
|
||||
const { pathsEqual, parentDirOf, joinPathFs } = await import('../src/utils/text')
|
||||
expect(pathsEqual('C:\\ws\\sub', 'C:/ws/sub')).toBe(true)
|
||||
expect(pathsEqual('C:/ws/a.md', 'C:/ws/b.md')).toBe(false)
|
||||
expect(parentDirOf('C:/ws/sub/a.md')).toBe('C:/ws/sub')
|
||||
expect(parentDirOf('C:\\ws\\a.md')).toBe('C:\\ws')
|
||||
expect(joinPathFs('C:\\ws', 'a.md')).toBe('C:\\ws\\a.md')
|
||||
expect(joinPathFs('C:/ws/', 'a.md')).toBe('C:/ws/a.md')
|
||||
})
|
||||
it('isPathInside 只认真正的子路径', async () => {
|
||||
const { isPathInside } = await import('../src/utils/text')
|
||||
expect(isPathInside('C:/ws/sub/deep', 'C:/ws/sub')).toBe(true)
|
||||
expect(isPathInside('C:/ws/sub', 'C:/ws/sub')).toBe(false)
|
||||
expect(isPathInside('C:/ws/sub2', 'C:/ws/sub')).toBe(false)
|
||||
expect(isPathInside('C:\\ws\\sub\\deep', 'C:/ws/sub')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
36
tests/window-claims.test.ts
Normal file
36
tests/window-claims.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// 跨窗口草稿认领:别的窗口打开着的文档不该在本窗口再弹恢复提示;
|
||||
// 窗口注销或认领过期后,草稿重新可被提示(真正的崩溃恢复不被吞)。
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createWindowClaims, CLAIM_STALE_MS } from '../src/services/windowClaims'
|
||||
|
||||
describe('跨窗口文档认领', () => {
|
||||
beforeEach(() => { localStorage.clear() })
|
||||
|
||||
it('其他窗口认领的文档键会被列出,本窗口自己的不算', () => {
|
||||
const a = createWindowClaims('win-a')
|
||||
const b = createWindowClaims('win-b')
|
||||
a.update(['path:C:/docs/a.md'])
|
||||
b.update(['path:C:/docs/b.md'])
|
||||
expect([...b.liveClaimedKeys()]).toEqual(['path:C:/docs/a.md'])
|
||||
expect([...a.liveClaimedKeys()]).toEqual(['path:C:/docs/b.md'])
|
||||
})
|
||||
|
||||
it('窗口注销后其认领立即失效', () => {
|
||||
const a = createWindowClaims('win-a')
|
||||
const b = createWindowClaims('win-b')
|
||||
a.update(['path:C:/docs/a.md'])
|
||||
a.dispose()
|
||||
expect(b.liveClaimedKeys().size).toBe(0)
|
||||
})
|
||||
|
||||
it('认领过期(窗口崩溃未注销)后自动失效,不吞崩溃草稿', () => {
|
||||
let t = 1_000_000
|
||||
const now = () => t
|
||||
const a = createWindowClaims('win-a', localStorage, now)
|
||||
const b = createWindowClaims('win-b', localStorage, now)
|
||||
a.update(['path:C:/docs/a.md'])
|
||||
expect(b.liveClaimedKeys().size).toBe(1)
|
||||
t += CLAIM_STALE_MS + 1
|
||||
expect(b.liveClaimedKeys().size).toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user