diff --git a/src/components/TreePicker/TreePicker.spec.jsx b/src/components/TreePicker/TreePicker.spec.jsx
new file mode 100644
index 000000000..0c717dba8
--- /dev/null
+++ b/src/components/TreePicker/TreePicker.spec.jsx
@@ -0,0 +1,505 @@
+import _ from 'lodash';
+import React from 'react';
+import { render, screen, user, within, waitFor } from 'testing';
+
+import TreePicker from '.';
+
+const rootNodes = [
+ { id: 'audience', label: 'Audience', type: 'root', header: 'Segment' },
+ { id: 'site', label: 'Site', type: 'root', header: 'Segment' },
+ { id: 'geo', label: 'Geo', type: 'root', header: 'Segment' },
+];
+const audSegmentNodes = [
+ { id: '111', label: 'Fyllo', type: 'segment' },
+ { id: '222', label: 'Acxiom', type: 'segment' },
+];
+
+const audSegmentValuesNodes = [{ id: '1111', label: 'MRI', type: 'value' }];
+
+it('should display empty state when there is no root nodes', async () => {
+ render(
+ (
+
+
+ {node.label} ({node.id})
+
+
+ )}
+ >
+
+
+ []} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Label')).toBeInTheDocument();
+ });
+
+ expect(screen.getByClass('aui--tree-picker-empty')).toBeInTheDocument();
+ expect(screen.getByClass('empty-title')).toHaveTextContent('No Results.');
+});
+
+it('should display with root nodes', async () => {
+ render(
+ (
+
+
+ {node.label} ({node.id})
+
+
+ )}
+ >
+
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience (audience)')).toBeInTheDocument();
+ });
+
+ expect(screen.getByClass('tree-picker')).toBeInTheDocument();
+
+ const treePickerTree = screen.getByClass('aui--tree-picker-tree');
+
+ const nodes = within(treePickerTree).getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(3);
+
+ expect(_.map(nodes, (n) => n.textContent).sort()).toEqual(['Audience (audience)', 'Site (site)', 'Geo (geo)'].sort());
+});
+
+it('should hide all hidden nodes', async () => {
+ render(
+ (
+
+ {node.label}
+
+ )}
+ >
+
+
+ rootNodes} hiddenNodeIds={['geo', 'site']} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Label')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ expect(screen.queryByText('Site')).not.toBeInTheDocument();
+ expect(screen.queryByText('Geo')).not.toBeInTheDocument();
+});
+
+it('should display nodes and header correctly when clicking expandable node', async () => {
+ render(
+ (
+
+ {node.label}
+ _.noop} />
+ audSegmentNodes} />
+
+ )}
+ >
+
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ let includeAllBtn = screen.queryByClass('aui--tree-picker-header-add-all');
+ let headerContent = screen.queryByClass('aui--tree-picker-header');
+ let nodes = within(screen.getByClass('aui--tree-picker-tree')).getAllByClass('aui--tree-picker-row-content');
+
+ expect(nodes).toHaveLength(3);
+ expect(includeAllBtn).not.toBeInTheDocument();
+ expect(headerContent).not.toBeInTheDocument();
+
+ const nodeExpands = screen.getAllByClass('aui--tree-picker-node-expand');
+ expect(nodeExpands).toHaveLength(3);
+ expect(nodeExpands[1]).toHaveClass('disabled');
+
+ await user.click(nodeExpands[1]); // disabled
+ includeAllBtn = screen.queryByClass('aui--tree-picker-header-add-all');
+ expect(within(screen.getByClass('aui--tree-picker-tree')).getAllByClass('aui--tree-picker-row-content')).toHaveLength(
+ 3
+ ); // still render root nodes
+ expect(includeAllBtn).not.toBeInTheDocument();
+
+ await user.click(nodeExpands[0]);
+
+ nodes = within(screen.getByClass('aui--tree-picker-tree')).getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(2);
+ expect(_.map(nodes, (n) => n.textContent).sort()).toEqual(['Fyllo', 'Acxiom'].sort());
+
+ headerContent = screen.getByClass('aui--tree-picker-header');
+ expect(headerContent).toHaveTextContent('Segment');
+
+ includeAllBtn = screen.getByClass('aui--tree-picker-header-add-all');
+ expect(includeAllBtn).toBeInTheDocument(); // include all button
+});
+
+it('should display nodes correctly when expanding and navigating', async () => {
+ render(
+ (
+
+ {node.label}
+ {
+ if (node.id === 'audience') return audSegmentNodes;
+ if (node.id === '111') return audSegmentValuesNodes;
+ if (node.id === '1111') return [];
+ }}
+ />
+
+ )}
+ >
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ expect(screen.queryByClass('aui--tree-picker-header')).not.toBeInTheDocument();
+
+ const nodeExpands = screen.getAllByClass('aui--tree-picker-node-expand');
+
+ // expanding audience node
+ await user.click(nodeExpands[0]);
+
+ let navNodes = screen.getAllByClass('aui--breadcrumb-node');
+
+ expect(navNodes).toHaveLength(2);
+ expect(navNodes.map((node) => node.textContent)).toEqual(['All', 'Audience']);
+
+ // expanding Fyllo node
+ await user.click(screen.getAllByClass('aui--tree-picker-node-expand')[0]);
+
+ navNodes = screen.getAllByClass('aui--breadcrumb-node');
+ expect(navNodes).toHaveLength(3);
+ expect(navNodes.map((node) => node.textContent)).toEqual(['All', 'Audience', 'Fyllo']);
+
+ expect(screen.getAllByClass('aui--tree-picker-row-content')).toHaveLength(1); //1 audSegmentValuesNodes
+
+ // expanding MRI node
+ await user.click(screen.getAllByClass('aui--tree-picker-node-expand')[0]); // 0 node
+ const emptyTreePicker = screen.getByClass('aui--tree-picker-empty');
+ expect(emptyTreePicker).toBeInTheDocument();
+ expect(within(emptyTreePicker).getByClass('empty-title')).toHaveTextContent('No Results.');
+
+ await user.click(within(emptyTreePicker).getByText('Go Back'));
+ expect(screen.getAllByClass('aui--tree-picker-row-content')).toHaveLength(1); //1 audSegmentValuesNodes
+
+ // clicking Audience nav node
+ await user.click(navNodes[1]);
+ let nodes = screen.getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(2); // 2 audSegmentNodes
+
+ // clicking All nav node
+ await user.click(navNodes[0]);
+ nodes = screen.getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(3); // 3 rootNodes
+});
+
+it('should expand nodes inline correctly when inline flag set to true', async () => {
+ render(
+ (
+
+ {node.label}
+ {
+ if (node.id === 'audience') return audSegmentNodes;
+ if (node.id === '111') return audSegmentValuesNodes;
+ if (node.id === '222') return [{ id: 'hidden-node-1', label: 'Hidden Node' }];
+ return [{ id: '11111', label: 'MRI-sub-node' }];
+ }}
+ />
+
+ )}
+ >
+
+ rootNodes} hiddenNodeIds={['hidden-node-1']} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ let nodeExpanders = screen.getAllByClass('aui--tree-picker-node-expand');
+
+ // Expanding audience node
+ await user.click(nodeExpanders[0]);
+
+ const nodeBranch = screen.getByClass('aui--tree-picker-node-branch');
+ expect(nodeBranch).toBeInTheDocument();
+ expect(nodeBranch).toHaveClass('is-odd');
+
+ const inlineNodes = within(nodeBranch).getAllByClass('aui--tree-picker-row');
+ expect(inlineNodes).toHaveLength(2); // 2 audSegmentNodes
+
+ // Expanding the inline audience (Fyllo) node:
+ let expander = within(inlineNodes[0]).getByClass('aui--tree-picker-node-expand');
+ await user.click(expander);
+
+ let nodeBranches = screen.getAllByClass('aui--tree-picker-node-branch');
+ expect(nodeBranches).toHaveLength(2);
+ expect(nodeBranches[1]).toHaveClass('is-even');
+
+ // Expanding the inline audience (Acxiom) node:
+ expander = within(inlineNodes[1]).getByClass('aui--tree-picker-node-expand');
+ await user.click(expander);
+
+ nodeBranches = screen.getAllByClass('aui--tree-picker-node-branch');
+ expect(nodeBranches).toHaveLength(2); // still two branches
+
+ // Expanding the inline audience value (MRI) node:
+ expander = within(nodeBranches[1]).getAllByClass('aui--tree-picker-node-expand')[0];
+ await user.click(expander);
+
+ nodeBranches = screen.getAllByClass('aui--tree-picker-node-branch');
+ expect(nodeBranches).toHaveLength(3);
+ expect(nodeBranches[2]).toHaveClass('is-odd');
+});
+
+it('should trigger props.onAdd when add node and include all nodes', async () => {
+ const onAdd = jest.fn();
+ render(
+ (
+
+ {node.label}
+ audSegmentNodes} />
+ ;
+
+ )}
+ >
+
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ const nodeAddButtons = screen.getAllByClass('aui--tree-picker-node-add');
+ expect(nodeAddButtons).toHaveLength(3);
+
+ const includeAllButton = screen.getByClass('aui--tree-picker-header-add-all');
+ expect(includeAllButton).toBeInTheDocument();
+
+ await user.click(nodeAddButtons[0]); // add first root node
+
+ expect(onAdd).toHaveBeenCalledTimes(1);
+ expect(onAdd).toHaveBeenCalledWith(rootNodes[0], false);
+
+ await user.click(includeAllButton); // include all root nodes
+ expect(onAdd).toHaveBeenCalledTimes(4);
+ expect(onAdd).toHaveBeenNthCalledWith(2, rootNodes[0], true);
+ expect(onAdd).toHaveBeenNthCalledWith(3, rootNodes[1], true);
+ expect(onAdd).toHaveBeenNthCalledWith(4, rootNodes[2], true);
+});
+
+it('should trigger resolveNodes when searching', async () => {
+ const resolveNodes = jest.fn().mockReturnValue([{ id: 1, label: 'test' }]);
+ render(
+ (
+
+ {node.label}
+ audSegmentNodes} />
+
+ )}
+ >
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ const treePickerSearch = screen.getByClass('aui--tree-picker-search');
+ expect(treePickerSearch).toBeInTheDocument();
+
+ await user.click(screen.getByPlaceholderText('Search'));
+ await user.paste('test');
+
+ let nodes = screen.getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(1);
+ expect(nodes[0]).toHaveTextContent('test');
+
+ expect(resolveNodes).toHaveBeenCalledTimes(1);
+ expect(resolveNodes).toHaveBeenLastCalledWith('test', null);
+
+ await user.clear(screen.getByTestId('search-input'));
+ nodes = screen.getAllByClass('aui--tree-picker-row-content');
+ expect(nodes).toHaveLength(3); // clear search, clear search nodes
+});
+
+it('should be able to reset to root nodes when no search results', async () => {
+ const resolveNodes = jest.fn().mockReturnValue([]);
+ render(
+ (
+
+ {node.label}
+ audSegmentNodes} />
+
+ )}
+ >
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ const treePickerSearch = screen.getByClass('aui--tree-picker-search');
+ expect(treePickerSearch).toBeInTheDocument();
+
+ await user.click(screen.getByPlaceholderText('Search'));
+ await user.paste('test');
+
+ expect(screen.queryByClass('aui--tree-picker-row-content')).not.toBeInTheDocument();
+
+ expect(screen.getByClass('aui--tree-picker-empty')).toBeInTheDocument();
+ await user.click(screen.getByText('Reset Search'));
+
+ expect(screen.getAllByClass('aui--tree-picker-row-content')).toHaveLength(3); // reset to root nodes
+});
+
+it('should log error and display an empty state when failing to resolve root nodes', async () => {
+ jest.spyOn(console, 'error').mockReturnValue();
+ const resolveRootNodes = jest.fn().mockRejectedValue({ error: 'Failed.' });
+
+ render(
+ (
+
+ {node.label}
+
+ )}
+ >
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByClass('aui--tree-picker-empty')).toBeInTheDocument();
+ });
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith('[TreePickerTree]', { error: 'Failed.' });
+});
+
+it('should log error for failed node resolution during search', async () => {
+ jest.spyOn(console, 'error').mockReturnValue();
+ const resolveMatchedNodes = jest.fn().mockRejectedValue({ error: 'Failed.' });
+
+ render(
+ (
+
+ {node.label}
+
+ )}
+ >
+
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ const searchInput = within(screen.getByClass('aui--tree-picker-search')).getByTestId('search-input');
+
+ await user.click(searchInput);
+ await user.paste('test');
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith('[TreePickerSearch]', { error: 'Failed.' });
+});
+
+it('should log error for failed node resolution during expanding', async () => {
+ jest.spyOn(console, 'error').mockReturnValue();
+ const resolveExpandedNodes = jest.fn().mockRejectedValue({ error: 'Failed.' });
+
+ render(
+ (
+
+ {node.label}
+
+
+ )}
+ >
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ // expanding audience node
+ await user.click(screen.getAllByClass('aui--tree-picker-node-expand')[0]);
+
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error).toHaveBeenCalledWith('[TreePickerNodeExpand]', { error: 'Failed.' });
+});
+
+it('should do nothing when expand resolveNodes() is not provided', async () => {
+ render(
+ (
+
+ {node.label}
+
+
+ )}
+ >
+ rootNodes} />
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Audience')).toBeInTheDocument();
+ });
+
+ let nodesContent = within(screen.getByClass('aui--tree-picker-tree')).getAllByClass('aui--tree-picker-row-content');
+ expect(nodesContent).toHaveLength(3);
+ expect(_.map(nodesContent, (n) => n.textContent).sort()).toEqual(['Audience', 'Site', 'Geo'].sort());
+
+ // expanding audience node
+ await user.click(screen.getAllByClass('aui--tree-picker-node-expand')[0]);
+
+ // remaining the same nodes:
+ nodesContent = within(screen.getByClass('aui--tree-picker-tree')).getAllByClass('aui--tree-picker-row-content');
+ expect(nodesContent).toHaveLength(3);
+
+ expect(_.map(nodesContent, (n) => n.textContent).sort()).toEqual(['Audience', 'Site', 'Geo'].sort());
+});