15.4 KB397 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 {act, fireEvent, render, screen} from '@testing-library/react';
9import userEvent from '@testing-library/user-event';
10import App from '../App';
11import {readAtom} from '../jotaiUtils';
12import {individualToggleKey, selectedCommits} from '../selection';
13import {mostRecentSubscriptionIds} from '../serverAPIState';
14import {CommitInfoTestUtils, CommitTreeListTestUtils} from '../testQueries';
15import {
16 closeCommitInfoSidebar,
17 COMMIT,
18 commitInfoIsOpen,
19 expectMessageSentToServer,
20 resetTestMessages,
21 simulateCommits,
22 simulateRepoConnected,
23 TEST_COMMIT_HISTORY,
24} from '../testUtils';
25
26describe('selection', () => {
27 beforeEach(() => {
28 resetTestMessages();
29 render(<App />);
30 act(() => {
31 simulateRepoConnected();
32 expectMessageSentToServer({
33 type: 'subscribe',
34 kind: 'smartlogCommits',
35 subscriptionID: mostRecentSubscriptionIds.smartlogCommits,
36 });
37 simulateCommits({value: TEST_COMMIT_HISTORY});
38 });
39 });
40
41 const click = (
42 name: string,
43 opts?: {shiftKey?: boolean; metaKey?: boolean; ctrlKey?: boolean},
44 ) => {
45 act(
46 () => void fireEvent.click(CommitTreeListTestUtils.withinCommitTree().getByText(name), opts),
47 );
48 };
49
50 const expectNoRealSelection = () =>
51 expect(CommitInfoTestUtils.withinCommitInfo().queryAllByTestId('selected-commit')).toHaveLength(
52 0,
53 );
54
55 const expectOnlyOneCommitSelected = () =>
56 expect(
57 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
58 ).not.toBeInTheDocument();
59
60 const expectNCommitsSelected = (n: number) =>
61 expect(
62 CommitInfoTestUtils.withinCommitInfo().queryByText(`${n} Commits Selected`),
63 ).toBeInTheDocument();
64
65 const upArrow = (shift?: boolean) => {
66 act(() =>
67 userEvent.type(
68 screen.getByTestId('commit-tree-root'),
69 (shift ? '{shift}' : '') + '{arrowup}',
70 ),
71 );
72 };
73 const downArrow = (shift?: boolean) => {
74 act(() =>
75 userEvent.type(
76 screen.getByTestId('commit-tree-root'),
77 (shift ? '{shift}' : '') + '{arrowdown}',
78 ),
79 );
80 };
81
82 const rightArrow = () => {
83 act(() => userEvent.type(screen.getByTestId('commit-tree-root'), '{arrowright}'));
84 };
85
86 it('allows selecting via click', () => {
87 act(() => void fireEvent.click(screen.getByText('Commit A')));
88
89 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
90 });
91
92 it("can't select public commits", () => {
93 act(() => void fireEvent.click(screen.getByText('remote/master')));
94 // it remains selecting the head commit
95 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit E')).toBeInTheDocument();
96 });
97
98 it('click on different commits changes selection', () => {
99 act(() => void fireEvent.click(screen.getByText('Commit A')));
100 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
101 act(() => void fireEvent.click(screen.getByText('Commit B')));
102 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
103
104 // not a multi-selection
105 expect(
106 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
107 ).not.toBeInTheDocument();
108 });
109
110 it('allows multi-selecting via cmd-click', () => {
111 act(() => void fireEvent.click(screen.getByText('Commit A'), {[individualToggleKey]: true}));
112 act(() => void fireEvent.click(screen.getByText('Commit B'), {[individualToggleKey]: true}));
113 act(() => void fireEvent.click(screen.getByText('Commit C'), {[individualToggleKey]: true}));
114
115 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
116 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
117 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
118 expect(
119 CommitInfoTestUtils.withinCommitInfo().getByText('3 Commits Selected'),
120 ).toBeInTheDocument();
121 });
122
123 it('single click after multi-select resets to single selection', () => {
124 act(() => void fireEvent.click(screen.getByText('Commit A'), {[individualToggleKey]: true}));
125 act(() => void fireEvent.click(screen.getByText('Commit B'), {[individualToggleKey]: true}));
126
127 act(() => void fireEvent.click(screen.getByText('Commit C')));
128
129 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit A')).not.toBeInTheDocument();
130 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit B')).not.toBeInTheDocument();
131 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
132
133 // not a multi-selection
134 expect(
135 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
136 ).not.toBeInTheDocument();
137 });
138
139 it('clicking on a commit a second time deselects it', () => {
140 const commitA = screen.getByText('Commit A');
141 act(() => void fireEvent.click(commitA));
142 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
143 act(() => void fireEvent.click(commitA));
144 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit A')).not.toBeInTheDocument();
145 });
146
147 it('cmd-clicking on a commit a second time deselects it', () => {
148 const commitA = screen.getByText('Commit A');
149 act(() => void fireEvent.click(commitA, {[individualToggleKey]: true}));
150 act(() => void fireEvent.click(screen.getByText('Commit B'), {[individualToggleKey]: true}));
151 act(() => void fireEvent.click(screen.getByText('Commit C'), {[individualToggleKey]: true}));
152
153 act(() => void fireEvent.click(commitA, {[individualToggleKey]: true}));
154
155 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit A')).not.toBeInTheDocument();
156 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
157 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
158 expect(
159 CommitInfoTestUtils.withinCommitInfo().getByText('2 Commits Selected'),
160 ).toBeInTheDocument();
161 });
162
163 it('single click after multi-select resets to single selection, even on a previously selected commits', () => {
164 const commitA = screen.getByText('Commit A');
165 act(() => void fireEvent.click(commitA, {[individualToggleKey]: true}));
166 act(() => void fireEvent.click(screen.getByText('Commit B'), {[individualToggleKey]: true}));
167 act(() => void fireEvent.click(screen.getByText('Commit C'), {[individualToggleKey]: true}));
168
169 act(() => void fireEvent.click(commitA));
170
171 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
172 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit B')).not.toBeInTheDocument();
173 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Commit C')).not.toBeInTheDocument();
174
175 // not a multi-selection
176 expect(
177 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
178 ).not.toBeInTheDocument();
179 });
180
181 it("selecting a commit that's no longer available does not render", () => {
182 // add a new commit F, then select it
183 act(() => simulateCommits({value: [COMMIT('f', 'Commit F', 'e'), ...TEST_COMMIT_HISTORY]}));
184 act(() => void fireEvent.click(screen.getByText('Commit F')));
185 // remove that commit from the history
186 act(() => simulateCommits({value: TEST_COMMIT_HISTORY}));
187
188 // F no longer exists to show, so now instead the head commit is selected
189 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit E')).toBeInTheDocument();
190
191 // not a multi-selection
192 expect(
193 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
194 ).not.toBeInTheDocument();
195 });
196
197 it('does not show the submit button for multi selections in GitHub repos', () => {
198 act(() => void fireEvent.click(screen.getByText('Commit A'), {[individualToggleKey]: true}));
199 act(() => void fireEvent.click(screen.getByText('Commit B'), {[individualToggleKey]: true}));
200 expect(
201 CommitInfoTestUtils.withinCommitInfo().queryByText('Submit Selected Commits'),
202 ).not.toBeInTheDocument();
203 });
204
205 it("multi selection commit previews doesn't include uncommitted changes", () => {
206 act(
207 () =>
208 void fireEvent.click(CommitTreeListTestUtils.withinCommitTree().getByText('Commit E'), {
209 [individualToggleKey]: true,
210 }),
211 );
212 act(
213 () =>
214 void fireEvent.click(CommitTreeListTestUtils.withinCommitTree().getByText('Commit D'), {
215 [individualToggleKey]: true,
216 }),
217 );
218 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit E')).toBeInTheDocument();
219 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
220
221 expect(
222 CommitInfoTestUtils.withinCommitInfo().queryByText('You are here'),
223 ).not.toBeInTheDocument();
224 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Uncommit')).not.toBeInTheDocument();
225 expect(CommitInfoTestUtils.withinCommitInfo().queryByText('Go to')).not.toBeInTheDocument();
226 });
227
228 describe('shift click selection', () => {
229 it('selects ranges of commits when shift-clicking', () => {
230 click('Commit B');
231 click('Commit D', {shiftKey: true});
232 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
233 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
234 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
235 expect(
236 CommitInfoTestUtils.withinCommitInfo().getByText('3 Commits Selected'),
237 ).toBeInTheDocument();
238 });
239
240 it('skips public commits, works across stacks and branches', () => {
241 click('Commit D');
242 click('Commit Y', {shiftKey: true});
243 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
244 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit E')).toBeInTheDocument();
245 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit X')).toBeInTheDocument();
246 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit Y')).toBeInTheDocument();
247 expect(
248 CommitInfoTestUtils.withinCommitInfo().getByText('4 Commits Selected'), // skipped '2', the public base of 'Commit X'
249 ).toBeInTheDocument();
250 });
251
252 it('adds to selection', () => {
253 click('Commit A', {[individualToggleKey]: true});
254 click('Commit C', {[individualToggleKey]: true});
255 click('Commit E', {shiftKey: true});
256 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
257 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
258 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
259 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit E')).toBeInTheDocument();
260 expect(
261 CommitInfoTestUtils.withinCommitInfo().getByText('4 Commits Selected'),
262 ).toBeInTheDocument();
263 });
264
265 it('prefers dag range to flatten range', () => {
266 // a-b--c-d-e
267 // \
268 // f-g
269 act(() =>
270 simulateCommits({
271 value: [
272 COMMIT('f', 'Commit F', 'b'),
273 COMMIT('g', 'Commit G', 'f'),
274 ...TEST_COMMIT_HISTORY,
275 ],
276 }),
277 );
278
279 {
280 click('Commit A'); // select
281 click('Commit G', {shiftKey: true});
282 const selected = readAtom(selectedCommits);
283 expect([...selected].sort()).toEqual(['a', 'b', 'f', 'g']);
284 }
285
286 {
287 click('Commit D'); // select
288 click('Commit A', {shiftKey: true});
289 const selected = readAtom(selectedCommits);
290 expect([...selected].sort()).toEqual(['a', 'b', 'c', 'd']);
291 }
292 });
293
294 it('deselecting clears last selected', () => {
295 click('Commit A'); // select
296 click('Commit A'); // deselect
297 click('Commit C', {[individualToggleKey]: true});
298 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
299 // just one commit, C, selected
300 expect(
301 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
302 ).not.toBeInTheDocument();
303 });
304
305 it('shift clicking when nothing selected acts like normal clicking', () => {
306 click('Commit C', {metaKey: true});
307 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
308 expect(
309 CommitInfoTestUtils.withinCommitInfo().queryByText(/\d Commits Selected/),
310 ).not.toBeInTheDocument();
311 });
312 });
313
314 describe('up/down arrows to select', () => {
315 it('down arrow with no selection starts you at the top', () => {
316 expectNoRealSelection();
317 downArrow();
318 expectOnlyOneCommitSelected();
319 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit Z')).toBeInTheDocument();
320 });
321
322 it('up arrow noop if nothing selected', () => {
323 upArrow();
324 upArrow(true);
325 expectNoRealSelection();
326 });
327
328 it('up arrow modifies selection', () => {
329 click('Commit C');
330 upArrow();
331 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
332 expectOnlyOneCommitSelected();
333 });
334
335 it('down arrow modifies selection', () => {
336 click('Commit C');
337 downArrow();
338 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
339 expectOnlyOneCommitSelected();
340 });
341
342 it('multiple arrow keys keep modifying selection', () => {
343 click('Commit A');
344 upArrow();
345 upArrow();
346 upArrow();
347 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
348 expectOnlyOneCommitSelected();
349 });
350
351 it('selection skips public commits', () => {
352 click('Commit A');
353 upArrow(); // B
354 upArrow(); // C
355 upArrow(); // D
356 upArrow(); // E
357 upArrow(); // skip public base, go to X
358 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit X')).toBeInTheDocument();
359 expectOnlyOneCommitSelected();
360 });
361
362 it('goes from last selection if multiple are selected', () => {
363 click('Commit A');
364 click('Commit C', {metaKey: true});
365 upArrow();
366 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
367 expectOnlyOneCommitSelected();
368 });
369
370 it('holding shift extends upwards', () => {
371 click('Commit C');
372 upArrow(/* shift */ true);
373 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
374 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit D')).toBeInTheDocument();
375 expectNCommitsSelected(2);
376 });
377
378 it('holding shift extends downwards', () => {
379 click('Commit C');
380 downArrow(/* shift */ true);
381 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit C')).toBeInTheDocument();
382 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
383 expectNCommitsSelected(2);
384 });
385
386 it('right arrows opens sidebar', () => {
387 click('Commit A');
388 act(() => closeCommitInfoSidebar());
389
390 expect(commitInfoIsOpen()).toEqual(false);
391 rightArrow();
392 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit A')).toBeInTheDocument();
393 expect(commitInfoIsOpen()).toEqual(true);
394 });
395 });
396});
397