Skip to content

Commit e6ef066

Browse files
committed
Add unit tests
1 parent 5634574 commit e6ef066

5 files changed

Lines changed: 278 additions & 31 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { EventShimCustomEvent } from '@deephaven/utils';
5+
import CustomColumnBuilder, {
6+
CustomColumnBuilderProps,
7+
} from './CustomColumnBuilder';
8+
import IrisGridTestUtils from '../IrisGridTestUtils';
9+
import IrisGridModel from '../IrisGridModel';
10+
11+
function Builder({
12+
model = IrisGridTestUtils.makeModel(),
13+
customColumns = [],
14+
onSave = jest.fn(),
15+
onCancel = jest.fn(),
16+
}: Partial<CustomColumnBuilderProps>) {
17+
return (
18+
<CustomColumnBuilder
19+
model={model}
20+
customColumns={customColumns}
21+
onSave={onSave}
22+
onCancel={onCancel}
23+
/>
24+
);
25+
}
26+
27+
test('Renders the default state', async () => {
28+
render(<Builder />);
29+
expect(screen.getByPlaceholderText('Column Name')).toBeInTheDocument();
30+
expect(screen.getByText('Column Formula')).toBeInTheDocument();
31+
});
32+
33+
test('Calls on save', async () => {
34+
const user = userEvent.setup();
35+
const customColumns = ['abc=def', 'foo=bar'];
36+
const mockSave = jest.fn();
37+
render(<Builder onSave={mockSave} customColumns={customColumns} />);
38+
39+
await user.type(screen.getByDisplayValue('abc'), 'cba');
40+
await user.click(screen.getByText(/Save/));
41+
expect(mockSave).toHaveBeenLastCalledWith(['abccba=def', 'foo=bar']);
42+
});
43+
44+
test('Switches to loader button while saving', async () => {
45+
jest.useFakeTimers();
46+
const user = userEvent.setup({ delay: null });
47+
const model = IrisGridTestUtils.makeModel();
48+
const mockSave = jest.fn(() =>
49+
setTimeout(() => {
50+
model.dispatchEvent(
51+
new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED)
52+
);
53+
}, 50)
54+
);
55+
56+
render(
57+
<Builder model={model} onSave={mockSave} customColumns={['foo=bar']} />
58+
);
59+
60+
await user.click(screen.getByText(/Save/));
61+
expect(screen.getByText('Applying')).toBeInTheDocument();
62+
jest.advanceTimersByTime(50);
63+
expect(screen.getByText('Success')).toBeInTheDocument();
64+
jest.advanceTimersByTime(CustomColumnBuilder.SUCCESS_SHOW_DURATION);
65+
expect(screen.getByText(/Save/)).toBeInTheDocument();
66+
67+
// Component should ignore this event and not change the save button
68+
model.dispatchEvent(
69+
new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED)
70+
);
71+
expect(screen.getByText(/Save/)).toBeInTheDocument();
72+
jest.useRealTimers();
73+
});
74+
75+
test('Adds a column', async () => {
76+
const user = userEvent.setup();
77+
render(<Builder />);
78+
79+
await user.click(screen.getByText('Add Another Column'));
80+
expect(screen.getAllByPlaceholderText('Column Name').length).toBe(2);
81+
expect(screen.getAllByText('Column Formula').length).toBe(2);
82+
});
83+
84+
test('Ignores empty names or formulas on save', async () => {
85+
const user = userEvent.setup();
86+
const customColumns = ['foo=bar'];
87+
const mockSave = jest.fn();
88+
render(<Builder customColumns={customColumns} onSave={mockSave} />);
89+
90+
await user.click(screen.getByText('Add Another Column'));
91+
await user.type(screen.getAllByPlaceholderText('Column Name')[1], 'test');
92+
await user.click(screen.getByText(/Save/));
93+
expect(mockSave).toBeCalledWith(customColumns);
94+
});
95+
96+
test('Deletes columns', async () => {
97+
const user = userEvent.setup();
98+
const customColumns = ['abc=def', 'foo=bar'];
99+
render(<Builder customColumns={customColumns} />);
100+
101+
await user.click(screen.getAllByLabelText(/Delete/)[0]);
102+
expect(screen.queryByDisplayValue('abc')).toBeNull();
103+
expect(screen.queryByDisplayValue('def')).toBeNull();
104+
expect(screen.getByDisplayValue('foo')).toBeInTheDocument();
105+
expect(screen.getByDisplayValue('bar')).toBeInTheDocument();
106+
107+
await user.click(screen.getByLabelText(/Delete/));
108+
expect(screen.queryByDisplayValue('foo')).toBeNull();
109+
expect(screen.queryByDisplayValue('bar')).toBeNull();
110+
expect(screen.getByPlaceholderText('Column Name')).toBeInTheDocument();
111+
expect(screen.getByText('Column Formula')).toBeInTheDocument();
112+
});
113+
114+
test('Displays request failure message', async () => {
115+
const user = userEvent.setup();
116+
const model = IrisGridTestUtils.makeModel();
117+
render(<Builder model={model} customColumns={['foo=bar']} />);
118+
119+
// Should ignore this since not in saving state
120+
model.dispatchEvent(
121+
new EventShimCustomEvent(IrisGridModel.EVENT.REQUEST_FAILED, {
122+
detail: { errorMessage: 'Error message' },
123+
})
124+
);
125+
expect(screen.queryByText(/Error message/)).toBeNull();
126+
127+
await user.click(screen.getByText(/Save/));
128+
model.dispatchEvent(
129+
new EventShimCustomEvent(IrisGridModel.EVENT.REQUEST_FAILED, {
130+
detail: { errorMessage: 'Error message' },
131+
})
132+
);
133+
134+
expect(screen.getByText(/Error message/)).toBeInTheDocument();
135+
136+
const input = screen.getByDisplayValue('foo');
137+
await user.click(input);
138+
expect(input).not.toHaveClass('is-invalid');
139+
});
140+
141+
test('Handles focus changes via keyboard', async () => {
142+
const user = userEvent.setup();
143+
const { container } = render(
144+
<Builder customColumns={['abc=bar', 'foo=bar']} />
145+
);
146+
147+
const nameInputs = screen.getAllByPlaceholderText('Column Name');
148+
const formulaInputs = container.querySelectorAll(
149+
'.input-editor-wrapper textarea'
150+
);
151+
const deleteButtons = screen.getAllByLabelText(/Delete/);
152+
const dragHandles = screen.getAllByLabelText(/Drag/);
153+
await user.click(nameInputs[0]);
154+
155+
await user.keyboard('{Tab}');
156+
expect(deleteButtons[0]).toHaveFocus();
157+
await user.keyboard('{Tab}');
158+
expect(dragHandles[0]).toHaveFocus();
159+
await user.keyboard('{Tab}');
160+
expect(formulaInputs[0]).toHaveFocus();
161+
162+
await user.keyboard('{Tab}');
163+
expect(nameInputs[1]).toHaveFocus();
164+
await user.keyboard('{Tab}');
165+
expect(deleteButtons[1]).toHaveFocus();
166+
await user.keyboard('{Tab}');
167+
expect(dragHandles[1]).toHaveFocus();
168+
await user.keyboard('{Tab}');
169+
expect(formulaInputs[1]).toHaveFocus();
170+
171+
await user.keyboard('{Tab}');
172+
expect(screen.getByText('Add Another Column')).toHaveFocus();
173+
174+
await user.keyboard('{Shift>}{Tab}{/Shift}');
175+
expect(formulaInputs[1]).toHaveFocus();
176+
await user.keyboard('{Shift>}{Tab}{/Shift}');
177+
expect(dragHandles[1]).toHaveFocus();
178+
});

packages/iris-grid/src/sidebar/CustomColumnBuilder.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type Input = {
1818
name: string;
1919
formula: string;
2020
};
21-
interface CustomColumnBuilderProps {
21+
export interface CustomColumnBuilderProps {
2222
model: IrisGridModel;
2323
customColumns: string[];
2424
onSave: (columns: string[]) => void;
@@ -38,12 +38,6 @@ class CustomColumnBuilder extends Component<
3838
> {
3939
static SUCCESS_SHOW_DURATION = 750;
4040

41-
static defaultProps = {
42-
customColumns: [],
43-
onSave: (): void => undefined,
44-
onCancel: (): void => undefined,
45-
};
46-
4741
static makeCustomColumnInputEventKey(): string {
4842
return shortid.generate();
4943
}
@@ -266,15 +260,15 @@ class CustomColumnBuilder extends Component<
266260
const { inputs } = this.state;
267261
// focus on drag handle
268262
if (shiftKey) {
269-
(this.container?.querySelectorAll(`.btn-drag-handle`)[
263+
(this.container?.querySelectorAll('.btn-drag-handle')[
270264
focusEditorIndex
271265
] as HTMLButtonElement).focus();
272266
return;
273267
}
274268
if (focusEditorIndex === inputs.length - 1) {
275-
(this.container?.querySelectorAll(`.btn-add-column`)[
276-
focusEditorIndex
277-
] as HTMLButtonElement).focus();
269+
(this.container?.querySelector(
270+
'.btn-add-column'
271+
) as HTMLButtonElement)?.focus();
278272
} else {
279273
// focus on next column name input
280274
const nextFocusIndex = focusEditorIndex + 1;
@@ -364,7 +358,6 @@ class CustomColumnBuilder extends Component<
364358
<span>
365359
<LoadingSpinner />
366360
<span className="btn-normal-content">Applying</span>
367-
<span className="btn-hover-content">Applying</span>
368361
</span>
369362
)}
370363
{!isSuccessShowing && !isCustomColumnApplying && saveText}
@@ -377,7 +370,7 @@ class CustomColumnBuilder extends Component<
377370
);
378371
}
379372

380-
render(): ReactElement {
373+
render(): JSX.Element {
381374
const { onCancel } = this.props;
382375
const { errorMessage } = this.state;
383376
return (
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
5+
import CustomColumnInput, { CustomColumnInputProps } from './CustomColumnInput';
6+
7+
const TEST_ID = 'TEST_ID';
8+
9+
function Input({
10+
eventKey = TEST_ID,
11+
inputIndex = 0,
12+
name = '',
13+
formula = '',
14+
onChange = jest.fn(),
15+
onDeleteColumn = jest.fn(),
16+
onTabInEditor = jest.fn(),
17+
invalid = false,
18+
isDuplicate = false,
19+
}: Partial<CustomColumnInputProps>) {
20+
return (
21+
<div>
22+
<DragDropContext onDragEnd={jest.fn()}>
23+
<Droppable droppableId="test-droppable">
24+
{() => (
25+
<CustomColumnInput
26+
eventKey={eventKey}
27+
inputIndex={inputIndex}
28+
name={name}
29+
formula={formula}
30+
onChange={onChange}
31+
onDeleteColumn={onDeleteColumn}
32+
onTabInEditor={onTabInEditor}
33+
invalid={invalid}
34+
isDuplicate={isDuplicate}
35+
/>
36+
)}
37+
</Droppable>
38+
</DragDropContext>
39+
</div>
40+
);
41+
}
42+
43+
test('Fires change events', async () => {
44+
const user = userEvent.setup();
45+
const mockOnChange = jest.fn();
46+
render(<Input onChange={mockOnChange} />);
47+
await user.type(screen.getByPlaceholderText('Column Name'), 'a');
48+
expect(mockOnChange).toBeCalledWith(TEST_ID, 'name', 'a');
49+
50+
mockOnChange.mockClear();
51+
await user.click(screen.getByText('Column Formula'));
52+
await user.keyboard('b');
53+
expect(mockOnChange).toBeCalledWith(TEST_ID, 'formula', 'b');
54+
await user.keyboard('[Backspace]');
55+
expect(mockOnChange).toBeCalledWith(TEST_ID, 'formula', 'b');
56+
});
57+
58+
test('Fires delete event', async () => {
59+
const user = userEvent.setup();
60+
const mockDelete = jest.fn();
61+
render(<Input onDeleteColumn={mockDelete} />);
62+
await user.click(screen.getByLabelText('Delete custom column'));
63+
expect(mockDelete).toBeCalledWith(TEST_ID);
64+
});
65+
66+
test('Displays validation errors', async () => {
67+
const { rerender } = render(<Input isDuplicate />);
68+
expect(screen.getByText('Duplicate name')).toBeInTheDocument();
69+
70+
rerender(<Input name=":abc" />);
71+
expect(screen.getByText('Invalid name')).toBeInTheDocument();
72+
});

packages/iris-grid/src/sidebar/CustomColumnInput.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, { useCallback } from 'react';
22
import classNames from 'classnames';
33
import { Draggable } from 'react-beautiful-dnd';
4-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5-
import { Button, Tooltip } from '@deephaven/components';
4+
import { Button } from '@deephaven/components';
65
import { vsTrash, vsGripper } from '@deephaven/icons';
76
import { DbNameValidator } from '@deephaven/utils';
87
import InputEditor from './InputEditor';
98
import { CustomColumnKey } from './CustomColumnBuilder';
109

11-
interface CustomColumnInputProps {
10+
export interface CustomColumnInputProps {
1211
eventKey: string;
1312
inputIndex: number;
1413
name: string;
@@ -29,6 +28,10 @@ const INPUT_TYPE = Object.freeze({
2928
FORMULA: 'formula',
3029
});
3130

31+
const EMPTY_FN = () => {
32+
// no-op
33+
};
34+
3235
function CustomColumnInput({
3336
eventKey,
3437
name,
@@ -89,19 +92,19 @@ function CustomColumnInput({
8992
tooltip="Delete custom column"
9093
/>
9194

92-
<button
93-
type="button"
94-
className="btn btn-link btn-link-icon px-2 btn-drag-handle"
95+
<Button
96+
kind="ghost"
97+
className="btn-drag-handle"
98+
onClick={EMPTY_FN}
9599
// eslint-disable-next-line react/jsx-props-no-spreading
96100
{...provided.dragHandleProps}
97-
>
98-
<Tooltip>Drag column to re-order</Tooltip>
99-
<FontAwesomeIcon icon={vsGripper} />
100-
</button>
101+
icon={vsGripper}
102+
tooltip="Drag column to re-order"
103+
/>
101104
</div>
102105
{(!isValidName || isDuplicate) && (
103106
<p className="validate-label-error text-danger mb-0 mt-2 pl-1">
104-
{!isValidName ? 'Invalid column name' : 'Duplicate name'}
107+
{!isValidName ? 'Invalid name' : 'Duplicate name'}
105108
</p>
106109
)}
107110
</div>

0 commit comments

Comments
 (0)