diff --git a/jest.config.js b/jest.config.js index 6d5dca613..922df27f1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,5 +26,5 @@ module.exports = { '^monaco-editor$': '/mock/monacoMock.js', '^@test/(.*)$': '/test/$1', }, - setupFiles: ['jest-canvas-mock', './test/setupTests.ts'], + setupFiles: ['jest-canvas-mock', './test/setupTests.tsx'], }; diff --git a/src/components/scrollable/__tests__/scrollable.test.tsx b/src/components/scrollable/__tests__/scrollable.test.tsx index ff63acd9d..19aea6498 100644 --- a/src/components/scrollable/__tests__/scrollable.test.tsx +++ b/src/components/scrollable/__tests__/scrollable.test.tsx @@ -4,6 +4,14 @@ import '@testing-library/jest-dom'; import { IScrollbarProps, Scrollable } from '../index'; +// to make sure the Scrollable component not be mocked by global +jest.mock('mo/components/scrollable', () => { + const originalModule = jest.requireActual('mo/components/scrollable'); + return { + ...originalModule, + }; +}); + function TestScrollable(props: IScrollbarProps) { return (
diff --git a/src/controller/explorer/explorer.tsx b/src/controller/explorer/explorer.tsx index de84287e2..67513ff22 100644 --- a/src/controller/explorer/explorer.tsx +++ b/src/controller/explorer/explorer.tsx @@ -119,11 +119,11 @@ export class ExplorerController const toolbarId = item.id; switch (toolbarId) { case NEW_FILE_COMMAND_ID: { - this.folderTreeController.createTreeNode(FileTypes.File); + this.folderTreeController.createTreeNode?.(FileTypes.File); break; } case NEW_FOLDER_COMMAND_ID: { - this.folderTreeController.createTreeNode(FileTypes.Folder); + this.folderTreeController.createTreeNode?.(FileTypes.Folder); break; } case REMOVE_COMMAND_ID: { diff --git a/src/controller/explorer/folderTree.tsx b/src/controller/explorer/folderTree.tsx index 4f4ee1703..8f3e55b2a 100644 --- a/src/controller/explorer/folderTree.tsx +++ b/src/controller/explorer/folderTree.tsx @@ -23,17 +23,16 @@ export interface LoadEventData extends EventDataNode { } export interface IFolderTreeController { - readonly createTreeNode: (type: FileType) => void; - readonly onClickContextMenu: ( + readonly createTreeNode?: (type: FileType) => void; + readonly onClickContextMenu?: ( contextMenu: IMenuItemProps, - treeNode: ITreeNodeItemProps + treeNode?: ITreeNodeItemProps ) => void; readonly onUpdateFileName?: (file: ITreeNodeItemProps) => void; - readonly onSelectFile: (file: ITreeNodeItemProps) => void; + readonly onSelectFile?: (file: ITreeNodeItemProps) => void; readonly onDropTree?: (treeNode: ITreeNodeItemProps[]) => void; readonly onLoadData?: (treeNode: LoadEventData) => Promise; - - onRightClick(treeNode: ITreeNodeItemProps): IMenuItemProps[]; + readonly onRightClick?: (treeNode: ITreeNodeItemProps) => IMenuItemProps[]; } @singleton() @@ -86,16 +85,17 @@ export class FolderTreeController public readonly onClickContextMenu = ( contextMenu: IMenuItemProps, - treeNode: ITreeNodeItemProps + treeNode?: ITreeNodeItemProps ) => { const menuId = contextMenu.id; - const { id: nodeId } = treeNode; switch (menuId) { case RENAME_COMMAND_ID: { + const { id: nodeId } = treeNode!; this.onRename(nodeId); break; } case DELETE_COMMAND_ID: { + const { id: nodeId } = treeNode!; this.onDelete(nodeId); break; } @@ -108,11 +108,11 @@ export class FolderTreeController break; } case OPEN_TO_SIDE_COMMAND_ID: { - this.onSelectFile(treeNode); + this.onSelectFile(treeNode!); break; } default: { - this.onContextMenuClick(treeNode, contextMenu); + this.onContextMenuClick(contextMenu, treeNode); } } }; @@ -141,10 +141,10 @@ export class FolderTreeController }; private onContextMenuClick = ( - treeNode: ITreeNodeItemProps, - contextMenu: IMenuItemProps + contextMenu: IMenuItemProps, + treeNode?: ITreeNodeItemProps ) => { - this.emit(FolderTreeEvent.onContextMenuClick, treeNode, contextMenu); + this.emit(FolderTreeEvent.onContextMenuClick, contextMenu, treeNode); }; private onRename = (id: number) => { diff --git a/src/services/workbench/explorer/folderTreeService.ts b/src/services/workbench/explorer/folderTreeService.ts index 9c29bb2d5..f59f9759e 100644 --- a/src/services/workbench/explorer/folderTreeService.ts +++ b/src/services/workbench/explorer/folderTreeService.ts @@ -120,8 +120,8 @@ export interface IFolderTreeService extends Component { */ onContextMenu( callback: ( - treeNode: ITreeNodeItemProps, - contextMenu: IMenuItemProps + contextMenu: IMenuItemProps, + treeNode?: ITreeNodeItemProps ) => void ): void; /** @@ -369,8 +369,8 @@ export class FolderTreeService public onContextMenu = ( callback: ( - treeNode: ITreeNodeItemProps, - contextMenu: IMenuItemProps + contextMenu: IMenuItemProps, + treeNode?: ITreeNodeItemProps ) => void ) => { this.subscribe(FolderTreeEvent.onContextMenuClick, callback); diff --git a/src/workbench/activityBar/__tests__/activityBar.test.tsx b/src/workbench/activityBar/__tests__/activityBar.test.tsx index 4f79384e8..46e7fd194 100644 --- a/src/workbench/activityBar/__tests__/activityBar.test.tsx +++ b/src/workbench/activityBar/__tests__/activityBar.test.tsx @@ -27,15 +27,6 @@ const mockData: IActivityBarItem[] = [ }, ]; -// mock Scrollable component -jest.mock('mo/components/scrollable', () => { - const originalModule = jest.requireActual('mo/components/scrollable'); - return { - ...originalModule, - Scrollable: ({ children }) => <>{children}, - }; -}); - describe('The ActivityBar Component', () => { test('match the snapshot', () => { const component = renderer.create(); diff --git a/src/workbench/sidebar/__tests__/folderTree.test.tsx b/src/workbench/sidebar/__tests__/folderTree.test.tsx new file mode 100644 index 000000000..f1d19e076 --- /dev/null +++ b/src/workbench/sidebar/__tests__/folderTree.test.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { expectFnCalled } from '@test/utils'; +import { FolderTree } from '../explore'; +import type { IFolderTreeProps } from '../explore/folderTree'; +import type { ITreeNodeItemProps } from 'mo/components'; +import { dragToTargetNode } from 'mo/components/tabs/__tests__/tab.test'; +import { folderTreeClassName, folderTreeEditClassName } from '../explore/base'; + +function FolderTreeViewPanel(props: Omit) { + return ; +} + +const mockFile = { + id: 'file', + name: 'file', + isLeaf: true, +}; + +const mockFileInFolder = { + id: 'folder-file', + name: 'folder-file', + isLeaf: true, +}; + +const mockEditFile = { + ...mockFileInFolder, + name: 'folder-file.tsx', + isEditable: true, +}; + +const mockFolder = { + id: 'folder', + name: 'folder', + isLeaf: false, + children: [mockFileInFolder], +}; + +const mockTreeData: ITreeNodeItemProps[] = [ + { + id: 'root', + name: 'root', + isLeaf: false, + children: [mockFile, mockFolder], + }, +]; + +const mockTreeEditData = [ + { + ...mockTreeData[0], + children: [{ ...mockFolder, children: [mockEditFile] }], + }, +]; + +describe('The FolderTree Component', () => { + afterEach(cleanup); + + test('Should render welcome page without data', () => { + expectFnCalled((mockFn) => { + const { rerender, getByTestId, container } = render( + + ); + const wrapper = container.querySelector('div[data-content="test"]'); + + expect(wrapper?.innerHTML).toContain( + 'you have not yet opened a folder' + ); + + fireEvent.click(wrapper?.querySelector('a')!); + + rerender( + welcome
} + /> + ); + expect(getByTestId('welcome')).toBeInTheDocument(); + }); + }); + + test('Should support to the right click event for tree item', () => { + const mockFn = jest + .fn() + .mockImplementationOnce(() => []) + .mockImplementation(() => [{ id: 'test', name: 'test' }]); + const { getByTitle, container, getByRole } = render( + + ); + + const file = getByTitle('file'); + expect(file).toBeInTheDocument(); + + fireEvent.contextMenu(file); + + expect(mockFn).toBeCalled(); + expect(container.querySelector('div[role="menu"]')).toBeNull(); + + fireEvent.contextMenu(file); + expect(mockFn).toBeCalledTimes(2); + expect(getByRole('menu')).toBeInTheDocument(); + }); + + test('Should support to trigger onClickContextMenu event', () => { + const contextMenu = { id: 'test', name: 'test' }; + const mockFn = jest.fn().mockImplementation(() => [contextMenu]); + const mockContextMenuFn = jest.fn(); + + const { getByTitle, getByRole } = render( + + ); + const file = getByTitle('file'); + fireEvent.contextMenu(file); + + const menu = getByRole('menu'); + fireEvent.click(menu.firstElementChild!); + + expect(mockContextMenuFn).toBeCalled(); + expect(mockContextMenuFn.mock.calls[0][0]).toEqual( + expect.objectContaining(contextMenu) + ); + expect(mockContextMenuFn.mock.calls[0][1]).toEqual( + expect.objectContaining(mockFile) + ); + }); + + test('Should support to render a input for the editing node', async () => { + const { getByRole, container } = render( + + ); + + const input = getByRole('input') as HTMLInputElement; + + expect(input).toBeInTheDocument(); + // expect to pass through name into input's value + expect(input.value).toBe(mockEditFile.name); + + // expect to select the file name automatically + expect(input.selectionStart).toBe(0); + expect(input.selectionEnd).toBe(11); + expect( + container.querySelector(`.${folderTreeClassName}`)?.classList + ).toContain(folderTreeEditClassName); + }); + + test('Should support to update file name via blur or keypress', () => { + const mockFn = jest.fn(); + const { getByRole } = render( + + ); + + const input = getByRole('input'); + const mockEnterValue = 'test-enter'; + fireEvent.keyDown(input, { + keyCode: 13, + target: { value: mockEnterValue }, + }); + expect(mockFn).toBeCalled(); + expect(mockFn.mock.calls[0][0]).toEqual( + expect.objectContaining({ name: mockEnterValue }) + ); + + const mockEscValue = 'test-esc'; + fireEvent.keyDown(input, { + keyCode: 27, + target: { value: mockEscValue }, + }); + expect(mockFn).toBeCalledTimes(2); + expect(mockFn.mock.calls[1][0]).toEqual( + expect.objectContaining({ name: mockEscValue }) + ); + + const mockBlurValue = 'test-blur'; + fireEvent.blur(input, { + target: { value: mockBlurValue }, + }); + expect(mockFn).toBeCalledTimes(3); + expect(mockFn.mock.calls[2][0]).toEqual( + expect.objectContaining({ name: mockBlurValue }) + ); + }); + + test('Should support to drag tree node', () => { + const mockFn = jest.fn(); + const { getByTitle } = render( + + ); + + dragToTargetNode(getByTitle('file'), getByTitle('folder')); + + expect(mockFn).toBeCalled(); + expect(mockFn.mock.calls[0][0]).toEqual([ + { + ...mockTreeData[0], + children: [mockFolder, mockFile], + }, + ]); + }); + + test('Should suppor to init contextMenu', () => { + const contextMenu = { id: 'test', name: 'test' }; + const mockFn = jest.fn(); + const { container, getByRole } = render( + + ); + + const wrapper = container.querySelector(`.${folderTreeClassName}`)!; + + fireEvent.contextMenu(wrapper); + + const menu = getByRole('menu'); + expect(menu).toBeInTheDocument(); + + fireEvent.click(menu.firstElementChild!); + expect(mockFn).toBeCalled(); + expect(mockFn.mock.calls[0][0]).toEqual( + expect.objectContaining(contextMenu) + ); + expect(mockFn.mock.calls[0][1]).toBeUndefined(); + }); +}); diff --git a/src/workbench/sidebar/explore/folderTree.tsx b/src/workbench/sidebar/explore/folderTree.tsx index 9130baa2b..277a0fde0 100644 --- a/src/workbench/sidebar/explore/folderTree.tsx +++ b/src/workbench/sidebar/explore/folderTree.tsx @@ -6,7 +6,7 @@ import { select, getEventPosition } from 'mo/common/dom'; import Tree, { ITreeNodeItemProps } from 'mo/components/tree'; import { IMenuItemProps, Menu } from 'mo/components/menu'; import { Button } from 'mo/components/button'; -import { FolderTreeController } from 'mo/controller/explorer/folderTree'; +import type { IFolderTreeController } from 'mo/controller/explorer/folderTree'; import { useContextView } from 'mo/components/contextView'; import { useContextMenu } from 'mo/components/contextMenu'; import { @@ -18,7 +18,7 @@ import { classNames } from 'mo/common/className'; import { Scrollable } from 'mo/components'; import { DataBaseProps } from 'mo/components/collapse'; -interface IFolderTreeProps extends FolderTreeController, IFolderTree { +export interface IFolderTreeProps extends IFolderTreeController, IFolderTree { panel: DataBaseProps; } @@ -85,7 +85,7 @@ const FolderTree: React.FunctionComponent = (props) => { const { data = [], folderPanelContextMenu = [] } = folderTree; const handleAddRootFolder = () => { - createTreeNode('RootFolder'); + createTreeNode?.('RootFolder'); }; const welcomePage = ( @@ -114,7 +114,7 @@ const FolderTree: React.FunctionComponent = (props) => { const hasEditable = detectHasEditableStatus(data); const onClickMenuItem = (e, item) => { - onClickContextMenu?.(e, item); + onClickContextMenu?.(item); contextMenu.current?.hide(); }; @@ -123,7 +123,11 @@ const FolderTree: React.FunctionComponent = (props) => { return useContextMenu({ anchor: select(`.${folderTreeClassName}`), render: () => ( - + ), }); }; @@ -132,20 +136,22 @@ const FolderTree: React.FunctionComponent = (props) => { item: IMenuItemProps, data: IFolderTreeSubItem ) => { - onClickContextMenu(item, data); + onClickContextMenu?.(item, data); contextView.hide(); }; const handleRightClick = ({ event, node }) => { const { data } = node; - const menuItems = onRightClick(data); - - contextView?.show(getEventPosition(event), () => ( - handleMenuClick(item!, data)} - data={menuItems} - /> - )); + const menuItems = onRightClick?.(data) || []; + + menuItems.length && + contextView?.show(getEventPosition(event), () => ( + handleMenuClick(item!, data)} + data={menuItems} + /> + )); }; const handleUpdateFile = ( @@ -186,6 +192,7 @@ const FolderTree: React.FunctionComponent = (props) => { return isEditable ? ( { function MonacoService() {} const getter = { get: () => {} }; @@ -14,3 +16,14 @@ jest.mock('mo/monaco/monacoService', () => { MonacoService: MonacoService, }; }); + +// mock Scrollable component +jest.mock('mo/components/scrollable', () => { + const originalModule = jest.requireActual('mo/components/scrollable'); + return { + ...originalModule, + Scrollable: ({ children }) => { + return <>{children}; + }, + }; +});