Skip to content

Commit 8c7207c

Browse files
authored
test: Increase code coverage for Linker (#1215)
1 parent 8ac7dc8 commit 8c7207c

7 files changed

Lines changed: 602 additions & 9 deletions

File tree

packages/components/src/DragUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import DragUtils from './DragUtils';
22

33
function makeItems(count = 5) {
4-
const items = [];
4+
const items: number[] = [];
55

66
for (let i = 0; i < count; i += 1) {
77
items.push(i);
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import {
4+
OpenedPanelMap,
5+
PanelComponent,
6+
PanelManager,
7+
} from '@deephaven/dashboard';
8+
import GoldenLayout, { Config } from '@deephaven/golden-layout';
9+
import { TypeValue as FilterTypeValue } from '@deephaven/filters';
10+
import ToolType from './ToolType';
11+
import { Linker } from './Linker';
12+
import { Link, LinkPoint, LinkType } from './LinkerUtils';
13+
14+
// Disable CSSTransition delays to make testing simpler
15+
jest.mock('react-transition-group', () => ({
16+
// eslint-disable-next-line react/display-name, react/prop-types
17+
Transition: ({ children, in: inProp }) =>
18+
inProp !== false ? children : null,
19+
// eslint-disable-next-line react/display-name, react/prop-types
20+
CSSTransition: ({ children, in: inProp }) =>
21+
inProp !== false ? children : null,
22+
}));
23+
24+
function makeLayout() {
25+
return new GoldenLayout({} as Config, undefined);
26+
}
27+
28+
function makePanelManager(layout = makeLayout()) {
29+
const PANEL_ID_A = 'PANEL_ID_A';
30+
const PANEL_ID_B = 'PANEL_ID_B';
31+
const openedMap: OpenedPanelMap = new Map([
32+
[
33+
PANEL_ID_A,
34+
{
35+
getCoordinateForColumn: jest.fn(() => {
36+
const coordinate = [5, 5];
37+
return coordinate; // make coordinates here
38+
}),
39+
} as PanelComponent,
40+
],
41+
[
42+
PANEL_ID_B,
43+
{
44+
getCoordinateForColumn: jest.fn(() => {
45+
const coordinate = [50, 50];
46+
return coordinate; // make coordinates here
47+
}),
48+
} as PanelComponent,
49+
],
50+
]);
51+
return new PanelManager(layout, undefined, undefined, openedMap);
52+
}
53+
54+
function makeLinkPoint(
55+
panelId: string | string[],
56+
columnName: string,
57+
columnType: string | null,
58+
panelComponent?: string | null
59+
): LinkPoint {
60+
return { panelId, panelComponent, columnName, columnType };
61+
}
62+
63+
function makeLink(
64+
start: LinkPoint,
65+
end: LinkPoint,
66+
id: string,
67+
type: LinkType,
68+
isReversed?: boolean | undefined,
69+
operator?: FilterTypeValue | undefined
70+
): Link {
71+
return { start, end, id, isReversed, type, operator };
72+
}
73+
74+
function mountLinker({
75+
links = [] as Link[],
76+
timeZone = 'TIMEZONE',
77+
activeTool = ToolType.LINKER,
78+
localDashboardId = 'TEST_ID',
79+
layout = makeLayout(),
80+
panelManager = makePanelManager(),
81+
setActiveTool = jest.fn(),
82+
setDashboardLinks = jest.fn(),
83+
addDashboardLinks = jest.fn(),
84+
deleteDashboardLinks = jest.fn(),
85+
setDashboardIsolatedLinkerPanelId = jest.fn(),
86+
setDashboardColumnSelectionValidator = jest.fn(),
87+
} = {}) {
88+
return render(
89+
<Linker
90+
links={links}
91+
timeZone={timeZone}
92+
activeTool={activeTool}
93+
localDashboardId={localDashboardId}
94+
layout={layout}
95+
panelManager={panelManager}
96+
setActiveTool={setActiveTool}
97+
setDashboardLinks={setDashboardLinks}
98+
addDashboardLinks={addDashboardLinks}
99+
deleteDashboardLinks={deleteDashboardLinks}
100+
setDashboardIsolatedLinkerPanelId={setDashboardIsolatedLinkerPanelId}
101+
setDashboardColumnSelectionValidator={
102+
setDashboardColumnSelectionValidator
103+
}
104+
/>
105+
);
106+
}
107+
108+
it('closes Linker when escape key or Done button is pressed', async () => {
109+
const setActiveTool = jest.fn();
110+
mountLinker({ setActiveTool });
111+
const dialog = screen.getByTestId('linker-toast-dialog');
112+
const buttons = await screen.findAllByRole('button');
113+
expect(buttons).toHaveLength(3);
114+
115+
const doneButton = screen.getByRole('button', { name: 'Done' });
116+
fireEvent.click(doneButton);
117+
expect(setActiveTool).toHaveBeenCalledWith(ToolType.DEFAULT);
118+
119+
fireEvent.keyDown(dialog, { key: 'Escape' });
120+
expect(setActiveTool).toHaveBeenCalledWith(ToolType.DEFAULT);
121+
});
122+
123+
describe('tests link operations', () => {
124+
const deleteDashboardLinks = jest.fn();
125+
const setDashboardLinks = jest.fn();
126+
let linkPaths: HTMLElement[] = [];
127+
128+
beforeEach(async () => {
129+
const links: Link[] = [];
130+
for (let i = 0; i < 4; i += 1) {
131+
const start = makeLinkPoint(
132+
'PANEL_ID_A',
133+
`COLUMN_A`,
134+
'int',
135+
'PANEL_COMPONENT'
136+
);
137+
const end = makeLinkPoint(
138+
'PANEL_ID_B',
139+
`COLUMN_B_${i}`,
140+
'long',
141+
'PANEL_COMPONENT'
142+
);
143+
const link = makeLink(start, end, `LINK_ID_${i}`, 'tableLink');
144+
links.push(link);
145+
}
146+
mountLinker({ links, deleteDashboardLinks, setDashboardLinks });
147+
linkPaths = screen.getAllByTestId('link-select');
148+
expect(linkPaths).toHaveLength(4);
149+
});
150+
151+
it('deletes correct link with alt+click', async () => {
152+
expect(linkPaths).toHaveLength(4);
153+
fireEvent.click(linkPaths[0], { altKey: true });
154+
expect(deleteDashboardLinks).toHaveBeenCalledWith('TEST_ID', ['LINK_ID_0']);
155+
});
156+
157+
it('deletes all links when Clear All is clicked', async () => {
158+
const clearAllButton = screen.getByRole('button', { name: 'Clear All' });
159+
fireEvent.click(clearAllButton);
160+
expect(setDashboardLinks).toHaveBeenCalledWith('TEST_ID', []);
161+
});
162+
});
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import { Type as FilterType } from '@deephaven/filters';
4+
import LinkerLink from './LinkerLink';
5+
6+
function makeLinkerLink({
7+
x1 = 10,
8+
x2 = 50,
9+
y1 = 10,
10+
y2 = 10,
11+
isSelected = true,
12+
startColumnType = 'int',
13+
id = 'LINK_ID',
14+
className = 'linker-link link-is-selected',
15+
operator = FilterType.eq,
16+
onClick = jest.fn(),
17+
onDelete = jest.fn(),
18+
onOperatorChanged = jest.fn(),
19+
} = {}) {
20+
return render(
21+
<LinkerLink
22+
x1={x1}
23+
x2={x2}
24+
y1={y1}
25+
y2={y2}
26+
id={id}
27+
className={className}
28+
operator={operator}
29+
isSelected={isSelected}
30+
startColumnType={startColumnType}
31+
onClick={onClick}
32+
onDelete={onDelete}
33+
onOperatorChanged={onOperatorChanged}
34+
/>
35+
);
36+
}
37+
38+
it('mounts and renders correct comparison operators for strings', async () => {
39+
const onOperatorChanged = jest.fn();
40+
const props = {
41+
startColumnType: 'java.lang.String',
42+
operator: FilterType.startsWith,
43+
onOperatorChanged,
44+
};
45+
makeLinkerLink(props);
46+
47+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
48+
expect(dropdownAndDeleteButton[0]).toHaveTextContent('a*');
49+
50+
dropdownAndDeleteButton[0].click();
51+
const dropdownMenu = await screen.findAllByRole('button');
52+
expect(dropdownMenu).toHaveLength(8); // includes dropdown and delete button
53+
expect(dropdownMenu[2]).toHaveTextContent('is exactly');
54+
expect(dropdownMenu[3]).toHaveTextContent('is not exactly');
55+
expect(dropdownMenu[4]).toHaveTextContent('contains');
56+
expect(dropdownMenu[5]).toHaveTextContent('does not contain');
57+
expect(dropdownMenu[6]).toHaveTextContent('starts with');
58+
expect(dropdownMenu[7]).toHaveTextContent('ends with');
59+
60+
dropdownMenu[4].click();
61+
expect(onOperatorChanged).toHaveBeenCalledWith(
62+
'LINK_ID',
63+
FilterType.contains
64+
);
65+
});
66+
67+
it('renders correct symbol for endsWith', async () => {
68+
makeLinkerLink({ operator: FilterType.endsWith });
69+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
70+
expect(dropdownAndDeleteButton[0]).toHaveTextContent('*z');
71+
});
72+
73+
it('mounts and renders correct comparison operators for numbers', async () => {
74+
const props = {
75+
x1: 10,
76+
x2: 10,
77+
y1: 30,
78+
y2: 50,
79+
startColumnType: 'long',
80+
operator: FilterType.notEq,
81+
};
82+
makeLinkerLink(props);
83+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
84+
expect(dropdownAndDeleteButton[0]).toHaveTextContent('!=');
85+
86+
dropdownAndDeleteButton[0].click();
87+
const dropdownMenu = await screen.findAllByRole('button');
88+
expect(dropdownMenu).toHaveLength(8); // includes dropdown and delete button
89+
expect(dropdownMenu[2]).toHaveTextContent('is equal to');
90+
expect(dropdownMenu[3]).toHaveTextContent('is not equal to');
91+
expect(dropdownMenu[4]).toHaveTextContent('greater than');
92+
expect(dropdownMenu[5]).toHaveTextContent('greater than or equal to');
93+
expect(dropdownMenu[6]).toHaveTextContent('less than');
94+
expect(dropdownMenu[7]).toHaveTextContent('less than or equal to');
95+
});
96+
97+
it('mounts and renders correct comparison operators for date/time', async () => {
98+
const props = {
99+
x1: 10,
100+
x2: 20,
101+
y1: 50,
102+
y2: 30,
103+
startColumnType: 'io.deephaven.time.DateTime',
104+
operator: FilterType.lessThan,
105+
};
106+
makeLinkerLink(props);
107+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
108+
expect(dropdownAndDeleteButton[0]).toHaveTextContent('<');
109+
110+
dropdownAndDeleteButton[0].click();
111+
const dropdownMenu = await screen.findAllByRole('button');
112+
expect(dropdownMenu).toHaveLength(8); // includes dropdown and delete button
113+
expect(dropdownMenu[2]).toHaveTextContent('date is');
114+
expect(dropdownMenu[3]).toHaveTextContent('date is not');
115+
expect(dropdownMenu[4]).toHaveTextContent('date is after');
116+
expect(dropdownMenu[5]).toHaveTextContent('date is after or equal');
117+
expect(dropdownMenu[6]).toHaveTextContent('date is before');
118+
expect(dropdownMenu[7]).toHaveTextContent('date is before or equal');
119+
});
120+
121+
it('mounts and renders correct comparison operators for booleans', async () => {
122+
const props = {
123+
x1: 10,
124+
x2: 20,
125+
y1: 30,
126+
y2: 100,
127+
startColumnType: 'boolean',
128+
operator: FilterType.greaterThanOrEqualTo,
129+
};
130+
makeLinkerLink(props);
131+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
132+
expect(dropdownAndDeleteButton[0]).toHaveTextContent('>=');
133+
134+
dropdownAndDeleteButton[0].click();
135+
const dropdownMenu = await screen.findAllByRole('button');
136+
expect(dropdownMenu).toHaveLength(4); // includes dropdown and delete button
137+
expect(dropdownMenu[2]).toHaveTextContent('is equal to');
138+
expect(dropdownMenu[3]).toHaveTextContent('is not equal to');
139+
});
140+
141+
it('returns an empty label for invalid column type', async () => {
142+
const startColumnType = 'INVALID_TYPE';
143+
makeLinkerLink({ startColumnType });
144+
expect(LinkerLink.getLabelForLinkFilter(startColumnType, FilterType.eq)).toBe(
145+
''
146+
);
147+
});
148+
149+
it('calls onClick when the link is clicked and onDelete on alt-click and button press', async () => {
150+
const onClick = jest.fn();
151+
const onDelete = jest.fn();
152+
makeLinkerLink({ onClick, onDelete });
153+
154+
const linkPath = screen.getByTestId('link-select');
155+
fireEvent.click(linkPath);
156+
expect(onClick).toHaveBeenCalledTimes(1);
157+
158+
fireEvent.click(linkPath, { altKey: true });
159+
expect(onDelete).toHaveBeenCalledTimes(1);
160+
const dropdownAndDeleteButton = await screen.findAllByRole('button');
161+
dropdownAndDeleteButton[1].click();
162+
expect(onDelete).toHaveBeenCalledTimes(2);
163+
});

packages/dashboard-core-plugins/src/linker/LinkerLink.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ export class LinkerLink extends Component<LinkerLinkProps, LinkerLinkState> {
314314
onMouseEnter={this.handleMouseEnter}
315315
onMouseLeave={this.handleMouseLeave}
316316
clipPath={`url(#${clipPathId})`}
317+
data-testid="link-select"
317318
/>
318319
<path className="link-background" d={path} />
319320
<path className="link-foreground" d={path} />

0 commit comments

Comments
 (0)