update
This commit is contained in:
144
tests/unit/renderer/components/folder-row-interactions.test.tsx
Normal file
144
tests/unit/renderer/components/folder-row-interactions.test.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
// tests/unit/renderer/components/folder-row-interactions.test.tsx
|
||||
//
|
||||
// 守 r174「文件夹行是死的」契约 —— FolderRowOption 此前是 Phase 1 stub:
|
||||
// 声明了 onContextMenu / onContextMenuAt / onHover 三个 prop 却一个都没用,
|
||||
// 并且 draggable={false} 让浏览器根本不发起原生 drag。后果:
|
||||
// - 右键文件夹 → 菜单永远不弹(NotesList / LiteratureList 里那套
|
||||
// folderMenuItems:剪切 / 粘贴 / 移到根目录 / 删除 全都够不着);
|
||||
// - hover 文件夹 → activeId 不跟手(SR 的 aria-activedescendant 落在
|
||||
// 别的行上);
|
||||
// - 拖文件夹 → r173 接好的 folders-move-dir 链路永远收不到载荷。
|
||||
//
|
||||
// 本测是**行为级** guard:不锁实现细节,只锁「用户做的动作真的到达了
|
||||
// 父级回调」。父级接线(坐标 vs 事件)另由 NotesList / LiteratureList 侧
|
||||
// 负责。
|
||||
//
|
||||
// r176(树状改造)追加:a11y 语义升格 —— 侧栏从 listbox/option 变成
|
||||
// tree/treeitem,aria-level / aria-expanded 必须在行上,否则读屏器
|
||||
// 报「树里没有层级」;缩进也必须真的按 depth 走(层级感是树的核心
|
||||
// 视觉线索)。另 `onOpen` 改名 `onActivate`(语义从「钻入」变成
|
||||
// 「设为落点 + 切换展开」)。
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { render, fireEvent, cleanup } from '@testing-library/react';
|
||||
import { afterEach } from 'vitest';
|
||||
import { FolderRowOption } from '../../../../src/renderer/src/components/common/FolderChrome';
|
||||
import type { DiskFolderNode } from '../../../../src/shared/types';
|
||||
|
||||
// i18n:组件内 FolderNameInput / aria 走 useTranslation —— 测试里只关心
|
||||
// 交互,不关心文案,直接 mock 成 key 原样返回。
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (k: string) => k }),
|
||||
}));
|
||||
|
||||
const folder: DiskFolderNode = { relPath: 'a/b', name: 'b', parentRelPath: 'a' };
|
||||
|
||||
function renderRow(overrides: Partial<React.ComponentProps<typeof FolderRowOption>> = {}) {
|
||||
const props = {
|
||||
folder,
|
||||
domId: 'note-row-folder-a/b',
|
||||
isActive: false,
|
||||
isCut: false,
|
||||
isDropHot: false,
|
||||
isRenaming: false,
|
||||
depth: 1,
|
||||
expanded: false,
|
||||
hasChildren: true,
|
||||
isTarget: false,
|
||||
posInSet: 1,
|
||||
setSize: 3,
|
||||
onActivate: vi.fn(),
|
||||
onContextMenu: vi.fn(),
|
||||
onContextMenuAt: vi.fn(),
|
||||
onHover: vi.fn(),
|
||||
onDragStart: vi.fn(),
|
||||
onDragOver: vi.fn(),
|
||||
onDragLeave: vi.fn(),
|
||||
onDrop: vi.fn(),
|
||||
onCommitRename: vi.fn().mockResolvedValue(true),
|
||||
onCancelRename: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
const utils = render(<FolderRowOption {...props} />);
|
||||
const row = utils.container.querySelector('[data-folder-relpath]') as HTMLElement;
|
||||
return { ...utils, row, props };
|
||||
}
|
||||
|
||||
describe('FolderRowOption 交互接线(r174)', () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('右键文件夹 → 父级 onContextMenu 收到 (folder, event)', () => {
|
||||
const { row, props } = renderRow();
|
||||
fireEvent.contextMenu(row, { clientX: 12, clientY: 34 });
|
||||
expect(props.onContextMenu).toHaveBeenCalledTimes(1);
|
||||
const [f, ev] = (props.onContextMenu as ReturnType<typeof vi.fn>).mock.calls[0]!;
|
||||
expect(f).toBe(folder);
|
||||
expect((ev as MouseEvent).clientX).toBe(12);
|
||||
});
|
||||
|
||||
it('hover 文件夹 → 父级 onHover 收到 folder', () => {
|
||||
const { row, props } = renderRow();
|
||||
fireEvent.mouseEnter(row);
|
||||
expect(props.onHover).toHaveBeenCalledWith(folder);
|
||||
});
|
||||
|
||||
it('文件夹行 draggable=true,dragstart 真的到达父级', () => {
|
||||
const { row, props } = renderRow();
|
||||
expect(row.getAttribute('draggable')).toBe('true');
|
||||
fireEvent.dragStart(row);
|
||||
expect(props.onDragStart).toHaveBeenCalledTimes(1);
|
||||
expect((props.onDragStart as ReturnType<typeof vi.fn>).mock.calls[0]![0]).toBe(folder);
|
||||
});
|
||||
|
||||
it('renaming 态 draggable=false(别拖 input)', () => {
|
||||
const { row } = renderRow({ isRenaming: true });
|
||||
expect(row.getAttribute('draggable')).toBe('false');
|
||||
});
|
||||
|
||||
it('Shift+F10 → 父级 onContextMenuAt 收到坐标', () => {
|
||||
const { row, props } = renderRow();
|
||||
fireEvent.keyDown(row, { key: 'F10', shiftKey: true });
|
||||
expect(props.onContextMenuAt).toHaveBeenCalledTimes(1);
|
||||
const [f, x, y] = (props.onContextMenuAt as ReturnType<typeof vi.fn>).mock.calls[0]!;
|
||||
expect(f).toBe(folder);
|
||||
expect(typeof x).toBe('number');
|
||||
expect(typeof y).toBe('number');
|
||||
});
|
||||
|
||||
it('Enter → 父级 onActivate 收到 folder', () => {
|
||||
const { row, props } = renderRow();
|
||||
fireEvent.keyDown(row, { key: 'Enter' });
|
||||
expect(props.onActivate).toHaveBeenCalledWith(folder);
|
||||
});
|
||||
|
||||
// === r176:树语义 ===
|
||||
|
||||
it('行是 treeitem(不是 option)—— aria-level / aria-expanded 只在 treeitem 上有效', () => {
|
||||
const { row } = renderRow({ depth: 2, expanded: true, hasChildren: true });
|
||||
expect(row.getAttribute('role')).toBe('treeitem');
|
||||
expect(row.getAttribute('aria-level')).toBe('3'); // depth + 1
|
||||
expect(row.getAttribute('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
it('叶子文件夹(无子节点)不带 aria-expanded —— 免得读屏 announce 一个不存在的展开态', () => {
|
||||
const { row } = renderRow({ hasChildren: false, expanded: false });
|
||||
expect(row.hasAttribute('aria-expanded')).toBe(false);
|
||||
});
|
||||
|
||||
it('缩进按 depth 递增(层级感是树的核心视觉线索)', () => {
|
||||
const { row: d0 } = renderRow({ depth: 0 });
|
||||
const { row: d2 } = renderRow({ depth: 2 });
|
||||
expect(d0.style.paddingLeft).toBe('12px');
|
||||
expect(d2.style.paddingLeft).toBe('36px'); // 12 + 2*12
|
||||
});
|
||||
|
||||
it('落点文件夹(isTarget)带 list-row-tree-target 类(与主选中态区分的视觉)', () => {
|
||||
const { row } = renderRow({ isTarget: true });
|
||||
expect(row.className).toContain('list-row-tree-target');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user