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`] = `
+
+`;
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);
}
};