diff --git a/src/controller/search/search.tsx b/src/controller/search/search.tsx index 85962f200..efc055637 100644 --- a/src/controller/search/search.tsx +++ b/src/controller/search/search.tsx @@ -28,23 +28,23 @@ import { import { ISearchProps, ITreeNodeItemProps } from 'mo/components'; export interface ISearchController { - getSearchIndex: (text: string, queryVal?: string) => number; - setSearchValue: (value?: string) => void; - setReplaceValue: (value?: string) => void; - setValidateInfo: (info: string | ISearchProps['validationInfo']) => void; - toggleMode: (status: boolean) => void; + getSearchIndex?: (text: string, queryVal?: string) => number; + setSearchValue?: (value?: string) => void; + setReplaceValue?: (value?: string) => void; + setValidateInfo?: (info: string | ISearchProps['validationInfo']) => void; + toggleMode?: (status: boolean) => void; toggleAddon?: (addon?: IActionBarItemProps) => void; - validateValue: ( + validateValue?: ( value: string, callback: (err: void | Error) => void ) => void; - onResultClick: ( + onResultClick?: ( item: ITreeNodeItemProps, resultData: ITreeNodeItemProps[] ) => void; - onChange: (value: string, replaceValue: string) => void; - onSearch: (value: string, replaceValue: string) => void; + onChange?: (value: string, replaceValue: string) => void; + onSearch?: (value: string, replaceValue: string) => void; } @singleton() diff --git a/src/workbench/sidebar/__tests__/__snapshots__/explore.test.tsx.snap b/src/workbench/sidebar/__tests__/__snapshots__/explore.test.tsx.snap new file mode 100644 index 000000000..c2bc05a4f --- /dev/null +++ b/src/workbench/sidebar/__tests__/__snapshots__/explore.test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`The Explorer Component Match Snapshot 1`] = ` +
+
+
+

+ 浏览 +

+
+
+
+
+
    +
  • + + test + +
  • +
+
+
+
+
+
+
+
+
+ + Open +
+
+
+
+
+
+ + test +
+
+
+
+
+
+
+`; diff --git a/src/workbench/sidebar/__tests__/explore.test.tsx b/src/workbench/sidebar/__tests__/explore.test.tsx new file mode 100644 index 000000000..51db04a12 --- /dev/null +++ b/src/workbench/sidebar/__tests__/explore.test.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { expectFnCalled } from '@test/utils'; +import { Explorer } from '../explore'; +import { toolbarClassName } from 'mo/components/toolbar'; + +const mockContextMenu = { + id: 'contextMenu', + name: 'contextMenu', + 'data-testid': 'test-toolbar', +}; +const mockToolbar = { + id: 'test', + name: 'test', + contextMenu: [mockContextMenu], +}; +const mockData = [ + { + id: 'open', + name: 'Open', + }, + { + id: 'test', + name: 'test', + panel: [mockToolbar], + }, +]; + +// mock collapse +jest.mock('mo/components/collapse', () => { + const originalModule = jest.requireActual('mo/components/collapse'); + return { + ...originalModule, + Collapse: jest + .fn() + // to mock originalModule once for ensure snapshot is the actual component + .mockImplementationOnce(originalModule.Collapse) + .mockImplementation( + ({ data, onCollapseChange, onToolbarClick }) => ( +
+ {data.map((item, index) => { + const { panel = [] } = item; + return ( + + + onCollapseChange(item.id) + } + > + collapse-{index} + + {panel.map((p, index) => ( + + onToolbarClick(p, item) + } + > + panel-{index} + + ))} + + ); + })} +
+ ) + ), + }; +}); + +describe('The Explorer Component', () => { + afterEach(cleanup); + + test('Match Snapshot', () => { + const component = renderer.create( + + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + + test('Should trigger onToolbarClick event', () => { + expectFnCalled((mockFn) => { + const { getByTestId } = render( + + ); + + const toolbar = getByTestId(`${mockData[1].id}-${mockToolbar.id}`); + fireEvent.click(toolbar); + + expect(mockFn.mock.calls[0][0]).toEqual(mockToolbar); + expect(mockFn.mock.calls[0][1]).toEqual(mockData[1]); + }); + }); + + test('Should trigger onCollapseChange event', () => { + expectFnCalled((mockFn) => { + const { getByTestId } = render( + + ); + + const target = mockData[1]; + const header = getByTestId(target.id); + fireEvent.click(header); + + expect(mockFn.mock.calls[0][0]).toEqual(target.id); + }); + }); + + test('Should trigger onActionsContextMenuClick event', () => { + expectFnCalled((mockFn) => { + const { container, getByTestId } = render( + + ); + + const toolbar = container.querySelector(`.${toolbarClassName}`); + const target = toolbar?.querySelector(`#${mockToolbar.id}`); + + expect(target).toBeInTheDocument(); + fireEvent.contextMenu(target!); + + const toolbarItem = getByTestId(mockContextMenu['data-testid']); + fireEvent.click(toolbarItem); + + expect(mockFn.mock.calls[0][1]).toEqual( + expect.objectContaining(mockContextMenu) + ); + }); + }); + + test('Should trigger onClick event in header toolbar', () => { + expectFnCalled((mockFn) => { + const { container, getByTestId } = render( + + ); + + const toolbar = container.querySelector(`.${toolbarClassName}`); + const target = toolbar?.querySelector(`#${mockToolbar.id}`); + + expect(target).toBeInTheDocument(); + fireEvent.click(target!); + + const toolbarItem = getByTestId(mockContextMenu['data-testid']); + fireEvent.click(toolbarItem); + + expect(mockFn.mock.calls[0][1]).toEqual( + expect.objectContaining(mockToolbar) + ); + }); + }); +}); diff --git a/src/workbench/sidebar/__tests__/searchPanel.test.tsx b/src/workbench/sidebar/__tests__/searchPanel.test.tsx new file mode 100644 index 000000000..43b16ab9a --- /dev/null +++ b/src/workbench/sidebar/__tests__/searchPanel.test.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { expectFnCalled } from '@test/utils'; +import { SearchPanel } from '../search'; +import { + emptyTextValueClassName, + matchSearchValueClassName, + replaceSearchValueClassName, +} from '../search/base'; +import { replaceBtnClassName } from 'mo/components/search/base'; + +const mockResult = [ + { + id: 'test', + name: 'test', + isLeaf: true, + }, + { + id: 'test2', + name: 'qqqqqq-test2', + isLeaf: true, + }, +]; + +describe('The SearchPanel Component', () => { + afterEach(cleanup); + + test('Should render no data tips', () => { + const { container } = render(); + + const noData = container.querySelector(`.${emptyTextValueClassName}`); + expect(noData).toBeInTheDocument(); + }); + + test('Should support to change value in controlled', () => { + expectFnCalled((mockFn) => { + const { container } = render( + + ); + + const seachInput = container.querySelector('textarea'); + expect(seachInput).toBeInTheDocument(); + + const nextValue = 'test-value'; + fireEvent.change(seachInput!, { target: { value: nextValue } }); + expect(mockFn.mock.calls[0][0]).toBe(nextValue); + }); + }); + + test('Should to support to change replace value in controlled', () => { + const mockFn = jest.fn(); + const mockToggleModeFn = jest.fn(); + + const { container } = render( + + ); + + const btn = container.querySelector(`.${replaceBtnClassName}`); + expect(btn).toBeInTheDocument(); + + fireEvent.click(btn!); + + expect(mockToggleModeFn).toBeCalled(); + expect(mockToggleModeFn.mock.calls[0][0]).toBeTruthy(); + + const textareas = container.querySelectorAll('textarea'); + expect(textareas).toHaveLength(2); + + const replaceTextarea = textareas[1]; + const value = 'test'; + fireEvent.change(replaceTextarea, { target: { value } }); + + expect(mockFn).toBeCalled(); + expect(mockFn.mock.calls[0][0]).toBe(value); + }); + + test('Should support to search value', () => { + const errorMessage = 'error'; + const mockValidation = jest + .fn() + .mockImplementationOnce((value, cb) => cb(new Error(errorMessage))) + .mockImplementation((value, cb) => cb()); + const mockSetInfo = jest.fn(); + const mockSearch = jest.fn(); + + const value = 'test'; + const replaceValue = 'replace'; + const { container } = render( + + ); + + const seachInput = container.querySelector('textarea'); + const nextValue = 'test-value'; + fireEvent.change(seachInput!, { target: { value: nextValue } }); + + expect(mockValidation).toBeCalled(); + expect(mockSetInfo).toBeCalled(); + expect(mockSetInfo.mock.calls[0][0]).toEqual({ + type: 'error', + text: errorMessage, + }); + + fireEvent.change(seachInput!, { target: { value: nextValue } }); + expect(mockSearch).toBeCalled(); + expect(mockSearch.mock.calls[0][0]).toBe(nextValue); + expect(mockSearch.mock.calls[0][1]).toBe(replaceValue); + }); + + test('Should support to render title in result tree', () => { + const value = 'test'; + const mockFn = jest + .fn() + .mockImplementationOnce(() => 0) + .mockImplementation(() => 7); + const { container, rerender } = render( + + ); + + let target = mockResult[0]; + let treeNode = container.querySelector( + `div[data-id=mo_treeNode_${target.id}]` + ); + expect(treeNode).toBeInTheDocument(); + expect( + treeNode!.querySelector(`.${matchSearchValueClassName}`)?.innerHTML + ).toContain(value); + + target = mockResult[1]; + treeNode = container.querySelector( + `div[data-id=mo_treeNode_${target.id}]` + ); + expect(treeNode).toBeInTheDocument(); + expect( + treeNode!.querySelector(`.${matchSearchValueClassName}`)?.innerHTML + ).toContain(value); + + const replaceValue = 'replace'; + rerender( + + ); + + treeNode = container.querySelector( + `div[data-id=mo_treeNode_${target.id}]` + ); + expect( + treeNode!.querySelector(`.${replaceSearchValueClassName}`) + ).toBeInTheDocument(); + expect( + treeNode!.querySelector(`.${replaceSearchValueClassName}`) + ?.innerHTML + ).toBe(replaceValue); + }); + + test('Should support to click the result tree node', () => { + expectFnCalled((mockFn) => { + const { container } = render( + 0} + onResultClick={mockFn} + /> + ); + + const target = mockResult[0]; + const treeNode = container.querySelector( + `div[data-id=mo_treeNode_${target.id}]` + ); + + const triggerDom = treeNode?.querySelector( + '.rc-tree-node-content-wrapper' + ); + fireEvent.click(triggerDom!); + + expect(mockFn.mock.calls[0][0]).toEqual(target); + expect(mockFn.mock.calls[0][1]).toEqual(mockResult); + }); + }); + + test('Should directly return a name for non-leaf node', () => { + const resultTree = [ + { + id: 'test', + name: 'non-leaf node', + isLeaf: false, + children: [ + { + id: 'test2', + name: 'qqqqqq-test2', + isLeaf: true, + }, + ], + }, + ]; + const { getByTitle } = render( + + ); + + expect(getByTitle(resultTree[0].name)).toBeInTheDocument(); + }); +}); diff --git a/src/workbench/sidebar/explore/explore.tsx b/src/workbench/sidebar/explore/explore.tsx index 935d9e1f7..944647725 100644 --- a/src/workbench/sidebar/explore/explore.tsx +++ b/src/workbench/sidebar/explore/explore.tsx @@ -7,8 +7,10 @@ import { Toolbar } from 'mo/components/toolbar'; import { defaultExplorerClassName } from './base'; import { localize } from 'mo/i18n/localize'; -export const Explorer: React.FunctionComponent = ( - props: IExplorer & IExplorerController +type IExplorerProps = IExplorer & IExplorerController; + +export const Explorer: React.FunctionComponent = ( + props: IExplorerProps ) => { const { data = [], diff --git a/src/workbench/sidebar/search/searchPanel.tsx b/src/workbench/sidebar/search/searchPanel.tsx index a91de3e6c..27a0a2ced 100644 --- a/src/workbench/sidebar/search/searchPanel.tsx +++ b/src/workbench/sidebar/search/searchPanel.tsx @@ -42,13 +42,13 @@ const SearchPanel = ({ const handleSearchChange = (values: SearchValues = []) => { const [searchVal, replaceVal] = values; - setSearchValue(searchVal); - setReplaceValue(replaceVal); - onChange(searchVal || '', replaceVal || ''); + setSearchValue?.(searchVal); + setReplaceValue?.(replaceVal); + onChange?.(searchVal || '', replaceVal || ''); }; const handleToggleButton = (status: boolean) => { - toggleMode(status); + toggleMode?.(status); }; const renderTitle = (node, _, isLeaf) => { @@ -56,7 +56,7 @@ const SearchPanel = ({ if (!isLeaf) { return name; } - const searchIndex = getSearchIndex(name, value); + const searchIndex = getSearchIndex ? getSearchIndex(name, value) : -1; const beforeStr = name.substr(0, searchIndex); const currentValue = name.substr(searchIndex, value.length); const afterStr = name.substr(searchIndex + value.length); @@ -87,21 +87,21 @@ const SearchPanel = ({ const handleSearch = (values: SearchValues = []) => { const [value, replaceVal] = values; - validateValue(value || '', (err) => { + validateValue?.(value || '', (err) => { if (err) { - setValidateInfo({ + setValidateInfo?.({ type: 'error', text: err.message, }); } else { - onSearch(value || '', replaceVal || ''); + onSearch?.(value || '', replaceVal || ''); } }); }; const handleTreeSelect = (item) => { if (item.isLeaf) { - onResultClick(item, result); + onResultClick?.(item, result); } };