9.5 KB306 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9
10import {act, fireEvent, render, screen, within} from '@testing-library/react';
11import userEvent from '@testing-library/user-event';
12import {Tooltip} from 'isl-components/Tooltip';
13import {ViewportOverlayRoot} from 'isl-components/ViewportOverlay';
14import App from '../App';
15import {
16 COMMIT,
17 closeCommitInfoSidebar,
18 expectMessageSentToServer,
19 resetTestMessages,
20 simulateCommits,
21} from '../testUtils';
22
23/* eslint-disable @typescript-eslint/no-non-null-assertion */
24
25describe('tooltips in ISL', () => {
26 let unmount: () => void;
27 beforeEach(() => {
28 resetTestMessages();
29 unmount = render(<App />).unmount;
30
31 act(() => {
32 closeCommitInfoSidebar();
33 expectMessageSentToServer({
34 type: 'subscribe',
35 kind: 'smartlogCommits',
36 subscriptionID: expect.anything(),
37 });
38 simulateCommits({
39 value: [
40 COMMIT('1', 'some public base', '0', {phase: 'public'}),
41 COMMIT('a', 'My Commit', '1'),
42 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
43 ],
44 });
45 });
46 });
47 afterEach(() => {
48 unmount();
49 });
50
51 describe('click to show', () => {
52 const clickSettingsGearToMakeTooltip = () => {
53 const settingsButtonTooltipCreator =
54 screen.getByTestId('settings-gear-button').parentElement!;
55 expect(settingsButtonTooltipCreator).toBeInTheDocument();
56 act(() => {
57 fireEvent.click(settingsButtonTooltipCreator);
58 });
59 };
60
61 it('shows settings dropdown when clicked', () => {
62 clickSettingsGearToMakeTooltip();
63
64 const settingsDropdown = within(screen.getByTestId('viewport-overlay-root')).getByTestId(
65 'settings-dropdown',
66 );
67 expect(settingsDropdown).toBeInTheDocument();
68 });
69
70 it('clicking inside tooltip does not dismiss it', () => {
71 clickSettingsGearToMakeTooltip();
72
73 const settingsDropdown = within(screen.getByTestId('viewport-overlay-root')).getByTestId(
74 'settings-dropdown',
75 );
76 const themeDropdown = within(settingsDropdown).getByText('Theme');
77 expect(themeDropdown).toBeInTheDocument();
78 act(() => {
79 fireEvent.click(themeDropdown!);
80 });
81
82 const settingsDropdown2 = within(screen.getByTestId('viewport-overlay-root')).getByTestId(
83 'settings-dropdown',
84 );
85 expect(settingsDropdown2).toBeInTheDocument();
86 });
87
88 it('clicking outside tooltip dismisses it', () => {
89 const settingsButton = screen.getByTestId('settings-gear-button');
90 act(() => {
91 fireEvent.click(settingsButton);
92 });
93
94 const settingsDropdown = within(screen.getByTestId('viewport-overlay-root')).queryByTestId(
95 'settings-dropdown',
96 );
97 expect(settingsDropdown).toBeInTheDocument();
98
99 act(() => {
100 fireEvent.click(screen.getByTestId('commit-a')!);
101 });
102
103 const settingsDropdown2 = within(screen.getByTestId('viewport-overlay-root')).queryByTestId(
104 'settings-dropdown',
105 );
106 expect(settingsDropdown2).not.toBeInTheDocument();
107 });
108 });
109
110 describe('hover to show', () => {
111 const REFRESH_BUTTON_HOVER_TEXT = 'Re-fetch latest commits and uncommitted changes.';
112 it('hovering refresh button shows tooltip', () => {
113 const refreshButton = screen.getByTestId('refresh-button').parentElement as HTMLElement;
114 userEvent.hover(refreshButton);
115
116 const refreshButtonTooltip = within(screen.getByTestId('viewport-overlay-root')).getByText(
117 REFRESH_BUTTON_HOVER_TEXT,
118 );
119 expect(refreshButtonTooltip).toBeInTheDocument();
120
121 userEvent.unhover(refreshButton);
122
123 expect(
124 within(screen.getByTestId('viewport-overlay-root')).queryByText(REFRESH_BUTTON_HOVER_TEXT),
125 ).not.toBeInTheDocument();
126 });
127
128 it('escape key dismisses tooltip', () => {
129 const refreshButton = screen.getByTestId('refresh-button').parentElement as HTMLElement;
130 userEvent.hover(refreshButton);
131
132 const refreshButtonTooltip = within(screen.getByTestId('viewport-overlay-root')).getByText(
133 REFRESH_BUTTON_HOVER_TEXT,
134 );
135 expect(refreshButtonTooltip).toBeInTheDocument();
136
137 userEvent.keyboard('{Escape}');
138
139 expect(
140 within(screen.getByTestId('viewport-overlay-root')).queryByText(REFRESH_BUTTON_HOVER_TEXT),
141 ).not.toBeInTheDocument();
142 });
143 });
144});
145
146describe('tooltip', () => {
147 function renderCustom(node: ReactNode) {
148 render(
149 <div className="isl-root">
150 <ViewportOverlayRoot />
151 {node}
152 </div>,
153 );
154 }
155
156 describe('onDismiss', () => {
157 it('calls onDismiss when hover leaves', () => {
158 const onDismiss = jest.fn();
159 renderCustom(
160 <Tooltip trigger="hover" title="hi" onDismiss={onDismiss}>
161 hover me
162 </Tooltip>,
163 );
164 const tooltip = screen.getByText('hover me');
165 userEvent.hover(tooltip);
166 expect(onDismiss).not.toHaveBeenCalled();
167 userEvent.unhover(tooltip);
168 expect(onDismiss).toHaveBeenCalledTimes(1);
169 });
170
171 it('calls onDismiss when pressing escape', () => {
172 const onDismiss = jest.fn();
173 renderCustom(
174 <Tooltip trigger="hover" title="hi" onDismiss={onDismiss}>
175 hover me
176 </Tooltip>,
177 );
178 const tooltip = screen.getByText('hover me');
179 userEvent.hover(tooltip);
180 expect(onDismiss).not.toHaveBeenCalled();
181 userEvent.keyboard('{Escape}');
182 expect(onDismiss).toHaveBeenCalledTimes(1);
183 });
184
185 it('calls onDismiss when clicking outside', () => {
186 const onDismiss = jest.fn();
187 renderCustom(
188 <div>
189 <div>something else</div>
190 <Tooltip trigger="click" component={() => <div>hi</div>} onDismiss={onDismiss}>
191 click me
192 </Tooltip>
193 </div>,
194 );
195 const tooltip = screen.getByText('click me');
196 fireEvent.click(tooltip);
197 expect(onDismiss).not.toHaveBeenCalled();
198 const other = screen.getByText('something else');
199 fireEvent.click(other);
200 expect(onDismiss).toHaveBeenCalledTimes(1);
201 });
202
203 it('title fields on click tooltips does not trigger onDismiss', () => {
204 const onDismiss = jest.fn();
205 renderCustom(
206 <div>
207 <div>something else</div>
208 <Tooltip
209 trigger="click"
210 component={() => <div>hi</div>}
211 title="hovered"
212 onDismiss={onDismiss}>
213 click me
214 </Tooltip>
215 </div>,
216 );
217 const tooltip = screen.getByText('click me');
218 userEvent.hover(tooltip);
219 expect(onDismiss).not.toHaveBeenCalled();
220 userEvent.unhover(tooltip);
221 expect(onDismiss).not.toHaveBeenCalled();
222 });
223
224 it('dismiss prop in tooltip components calls onDismiss', () => {
225 const onDismiss = jest.fn();
226 renderCustom(
227 <Tooltip
228 trigger="click"
229 component={dismiss => (
230 <>
231 <div>hi</div>
232 <button onClick={dismiss}>my button</button>
233 </>
234 )}
235 title="hovered"
236 onDismiss={onDismiss}>
237 click me
238 </Tooltip>,
239 );
240 const tooltip = screen.getByText('click me');
241 fireEvent.click(tooltip);
242 expect(onDismiss).not.toHaveBeenCalled();
243
244 // clicking inside tooltip is fine
245 const innerText = screen.getByText('hi');
246 fireEvent.click(innerText);
247 expect(onDismiss).not.toHaveBeenCalled();
248
249 // action that causes dismiss prop causes onDismiss
250 const innerDismiss = screen.getByText('my button');
251 fireEvent.click(innerDismiss);
252 expect(onDismiss).toHaveBeenCalledTimes(1);
253 });
254 });
255
256 describe('groups', () => {
257 it('dismisses other tooltips in the same group', () => {
258 const content = (value: string) => {
259 return () => <div>{value}</div>;
260 };
261 renderCustom(
262 <div>
263 <Tooltip trigger="click" group="test" component={content('Tooltip A')}>
264 Button A
265 </Tooltip>
266 <Tooltip trigger="click" group="test" component={content('Tooltip B')}>
267 Button B
268 </Tooltip>
269 </div>,
270 );
271 const a = screen.getByText('Button A');
272 const b = screen.getByText('Button B');
273 fireEvent.click(a);
274 expect(screen.getByText('Tooltip A')).toBeInTheDocument();
275 expect(screen.queryByText('Tooltip B')).not.toBeInTheDocument();
276 fireEvent.click(b);
277 expect(screen.queryByText('Tooltip A')).not.toBeInTheDocument();
278 expect(screen.getByText('Tooltip B')).toBeInTheDocument();
279 });
280
281 it('does not dismiss from other groups', () => {
282 const content = (value: string) => {
283 return () => <div>{value}</div>;
284 };
285 renderCustom(
286 <div>
287 <Tooltip trigger="click" group="test1" component={content('Tooltip A')}>
288 Button A
289 </Tooltip>
290 <Tooltip trigger="click" group="test2" component={content('Tooltip B')}>
291 Button B
292 </Tooltip>
293 </div>,
294 );
295 const a = screen.getByText('Button A');
296 const b = screen.getByText('Button B');
297 fireEvent.click(a);
298 expect(screen.getByText('Tooltip A')).toBeInTheDocument();
299 expect(screen.queryByText('Tooltip B')).not.toBeInTheDocument();
300 fireEvent.click(b);
301 expect(screen.getByText('Tooltip A')).toBeInTheDocument();
302 expect(screen.getByText('Tooltip B')).toBeInTheDocument();
303 });
304 });
305});
306