11.4 KB334 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 {Hash} from '../../types';
9
10import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
11import App from '../../App';
12import {Dag, DagCommitInfo} from '../../dag/dag';
13import {RebaseOperation} from '../../operations/RebaseOperation';
14import {CommitPreview} from '../../previews';
15import {ignoreRTL} from '../../testQueries';
16import {
17 closeCommitInfoSidebar,
18 COMMIT,
19 dragAndDropCommits,
20 dragCommits,
21 dropCommits,
22 expectMessageNOTSentToServer,
23 expectMessageSentToServer,
24 resetTestMessages,
25 scanForkedBranchHashes,
26 simulateCommits,
27 simulateUncommittedChangedFiles,
28 TEST_COMMIT_HISTORY,
29} from '../../testUtils';
30import {CommandRunner, succeedableRevset} from '../../types';
31
32/*eslint-disable @typescript-eslint/no-non-null-assertion */
33
34describe('rebase operation', () => {
35 // Extend with an obsoleted commit.
36 const testHistory = TEST_COMMIT_HISTORY.concat([
37 COMMIT('ff1', 'Commit FF1 (obsoleted)', 'z', {successorInfo: {hash: 'ff2', type: 'amend'}}),
38 COMMIT('ff2', 'Commit FF2', 'z'),
39 ]);
40
41 beforeEach(() => {
42 jest.useFakeTimers();
43 resetTestMessages();
44 render(<App />);
45 act(() => {
46 closeCommitInfoSidebar();
47 expectMessageSentToServer({
48 type: 'subscribe',
49 kind: 'smartlogCommits',
50 subscriptionID: expect.anything(),
51 });
52 simulateCommits({
53 value: testHistory,
54 });
55 });
56 });
57
58 afterEach(() => {
59 jest.useRealTimers();
60 });
61
62 const getCommitWithPreview = (hash: Hash, preview: CommitPreview): HTMLElement => {
63 const previewOfCommit = screen
64 .queryAllByTestId(`commit-${hash}`)
65 .map(commit => within(commit).queryByTestId('draggable-commit'))
66 .find(commit => commit?.classList.contains(`commit-preview-${preview}`));
67 expect(previewOfCommit).toBeInTheDocument();
68 return previewOfCommit!;
69 };
70
71 it('previews a rebase on drag & drop onto a public commit', () => {
72 expect(screen.getAllByText('Commit D')).toHaveLength(1);
73 dragAndDropCommits('d', '2');
74
75 // original commit AND previewed commit are now in the document
76 expect(screen.getAllByText('Commit D')).toHaveLength(2);
77 // also includes descendants
78 expect(screen.getAllByText('Commit E')).toHaveLength(2);
79
80 // one of them is a rebase preview
81 expect(
82 screen
83 .queryAllByTestId('commit-d')
84 .some(commit => commit.querySelector('.commit-preview-rebase-root')),
85 ).toEqual(true);
86 });
87
88 it('sets all descendants as the right preview type', () => {
89 expect(screen.getAllByText('Commit D')).toHaveLength(1);
90 dragAndDropCommits('a', '2');
91
92 expect(document.querySelectorAll('.commit-preview-rebase-old')).toHaveLength(5);
93 expect(document.querySelectorAll('.commit-preview-rebase-root')).toHaveLength(1);
94 expect(document.querySelectorAll('.commit-preview-rebase-descendant')).toHaveLength(4);
95 });
96
97 it('previews onto correct branch', () => {
98 expect(screen.getAllByText('Commit D')).toHaveLength(1);
99 dragAndDropCommits('d', 'x');
100 expect(scanForkedBranchHashes('x')).toEqual(['d', 'e']);
101 });
102
103 it('cannot drag public commits', () => {
104 dragAndDropCommits('1', '2');
105
106 // only see one copy of commit 1
107 expect(screen.queryAllByTestId('commit-1')).toHaveLength(1);
108 });
109
110 it('runs rebase operation', async () => {
111 dragAndDropCommits('d', '2');
112
113 const runRebaseButton = screen.getByText('Run Rebase');
114 expect(runRebaseButton).toBeInTheDocument();
115
116 fireEvent.click(runRebaseButton);
117 await waitFor(() => {
118 expectMessageSentToServer({
119 type: 'runOperation',
120 operation: {
121 args: ['rebase', '-s', succeedableRevset('d'), '-d', succeedableRevset('remote/master')],
122 id: expect.anything(),
123 runner: CommandRunner.Sapling,
124 trackEventName: 'RebaseOperation',
125 },
126 });
127 });
128 });
129
130 it('shows optimistic preview of rebase', async () => {
131 dragAndDropCommits('d', '2');
132
133 fireEvent.click(screen.getByText('Run Rebase'));
134
135 // original commit is hidden, we only see optimistic commit
136 expect(screen.queryAllByTestId('commit-d')).toHaveLength(1);
137 // also includes descendants
138 expect(screen.queryAllByTestId('commit-e')).toHaveLength(1);
139
140 await waitFor(() => {
141 expect(screen.getByText('rebasing...')).toBeInTheDocument();
142 });
143
144 expect(
145 screen.queryByTestId('commit-d')?.querySelector('.commit-preview-rebase-optimistic-root'),
146 ).toBeInTheDocument();
147 });
148
149 it('allows re-dragging a previously dragged commit back onto the same parent', async () => {
150 // Drag and drop 'e' normally onto 'c'
151 dragCommits('e', 'd');
152 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); // dragging on original parent is noop
153 dragAndDropCommits('e', 'c');
154 expect(screen.getByText('Run Rebase')).toBeInTheDocument();
155
156 // Drag the previously preview'd 'e' from 'c' onto 'b', without dropping
157 dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'b');
158 // Keep dragging the previously preview'd 'e' from 'b' back to 'c'
159 dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c');
160 // Finally, drop on 'c'. This should work, even though the preview'd 'e' started on top of 'c', so it's the "parent"
161 dropCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c');
162
163 fireEvent.click(screen.getByText('Run Rebase'));
164 await waitFor(() => {
165 expectMessageSentToServer({
166 type: 'runOperation',
167 operation: {
168 args: ['rebase', '-s', succeedableRevset('e'), '-d', succeedableRevset('c')],
169 id: expect.anything(),
170 runner: CommandRunner.Sapling,
171 trackEventName: 'RebaseOperation',
172 },
173 });
174 });
175 });
176
177 it('cancel cancels the preview', () => {
178 dragAndDropCommits('d', '2');
179
180 const cancelButton = screen.getByText('Cancel');
181 expect(cancelButton).toBeInTheDocument();
182
183 act(() => {
184 fireEvent.click(cancelButton);
185 });
186
187 // now the preview doesn't exist
188 expect(screen.queryAllByTestId('commit-d')).toHaveLength(1);
189
190 // we didn't run any operation
191 expectMessageNOTSentToServer({
192 type: 'runOperation',
193 operation: expect.anything(),
194 });
195 });
196
197 it('cannot drag with uncommitted changes', () => {
198 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]}));
199 dragAndDropCommits('d', '2');
200
201 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument();
202 expect(screen.getByText('Cannot drag to rebase with uncommitted changes.')).toBeInTheDocument();
203 });
204
205 it('cannot drag obsoleted commits', () => {
206 dragAndDropCommits('ff1', 'e');
207
208 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument();
209 expect(screen.getByText('Cannot rebase obsoleted commits.')).toBeInTheDocument();
210 });
211
212 it('can drag if uncommitted changes are optimistically removed', async () => {
213 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]}));
214 act(() => {
215 fireEvent.click(screen.getByTestId('quick-commit-button'));
216 });
217 await waitFor(() => {
218 expect(screen.queryByText(ignoreRTL('file1.txt'))).not.toBeInTheDocument();
219 });
220 dragAndDropCommits('d', '2');
221
222 expect(
223 screen.queryByText('Cannot drag to rebase with uncommitted changes.'),
224 ).not.toBeInTheDocument();
225 });
226
227 it('can drag with untracked changes', () => {
228 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: '?'}]}));
229 dragAndDropCommits('d', '2');
230
231 expect(screen.queryByText('Run Rebase')).toBeInTheDocument();
232 });
233
234 it('handles partial rebase in optimistic dag', () => {
235 const dag = new Dag().add(TEST_COMMIT_HISTORY.map(c => DagCommitInfo.fromCommitInfo(c)));
236
237 const type = 'succeedable-revset';
238 // Rebase a-b-c-d-e to z
239 const rebaseOp = new RebaseOperation({type, revset: 'a'}, {type, revset: 'z'});
240 // Count commits with the given title in a dag.
241 const count = (dag: Dag, title: string): number =>
242 dag.getBatch([...dag]).filter(c => c.title === title).length;
243 // Emulate partial rebased: a-b was rebased to z, but not c-d-e
244 const partialRebased = dag.rebase(['a', 'b'], 'z');
245 // There are 2 "Commit A"s in the partially rebased dag - one obsolsted.
246 expect(count(partialRebased, 'Commit A')).toBe(2);
247 expect(count(partialRebased, 'Commit B')).toBe(2);
248 expect(partialRebased.descendants('z').size).toBe(dag.descendants('z').size + 2);
249
250 // Calculate the optimistic dag from a partial rebase state.
251 const optimisticDag = rebaseOp.optimisticDag(partialRebased);
252 // should be only 1 "Commit A"s.
253 expect(count(optimisticDag, 'Commit A')).toBe(1);
254 expect(count(optimisticDag, 'Commit B')).toBe(1);
255 expect(count(optimisticDag, 'Commit E')).toBe(1);
256 // check the Commit A..E branch is completed rebased.
257 expect(dag.children(dag.parents('a')).size).toBe(
258 optimisticDag.children(dag.parents('a')).size + 1,
259 );
260 expect(optimisticDag.descendants('z').size).toBe(dag.descendants('a').size + 1);
261 });
262
263 describe('stacking optimistic state', () => {
264 it('cannot drag and drop preview descendants', () => {
265 dragAndDropCommits('d', 'a');
266 expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']);
267
268 dragAndDropCommits(getCommitWithPreview('e', CommitPreview.REBASE_DESCENDANT), 'b');
269
270 // we still see same commit preview
271 expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']);
272 });
273
274 it('can drag preview root again', () => {
275 dragAndDropCommits('d', 'a');
276
277 dragAndDropCommits(getCommitWithPreview('d', CommitPreview.REBASE_ROOT), 'b');
278
279 // preview is updated to be based on b
280 expect(scanForkedBranchHashes('b')).toEqual(['d', 'e']);
281 });
282
283 it('can preview drag drop while previous rebase running', async () => {
284 // c
285 // c | e
286 // e b |/
287 // d | e b
288 // c -> | d -> | d
289 // b |/ |/
290 // a a a
291 dragAndDropCommits('d', 'a');
292 fireEvent.click(screen.getByText('Run Rebase'));
293 await waitFor(() => {
294 expect(screen.getByText('rebasing...')).toBeInTheDocument();
295 });
296
297 dragAndDropCommits(
298 getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
299 'b',
300 );
301
302 // original optimistic is still there
303 expect(scanForkedBranchHashes('a')).toContain('d');
304 // also previewing new drag
305 expect(scanForkedBranchHashes('b')).toEqual(['e']);
306 });
307
308 it('can see optimistic drag drop while previous rebase running', async () => {
309 // c
310 // c | e
311 // e b |/
312 // d | e b
313 // c -> | d -> | d
314 // b |/ |/
315 // a a a
316 dragAndDropCommits('d', 'a');
317 fireEvent.click(screen.getByText('Run Rebase'));
318 await waitFor(() => {
319 expect(screen.getByText('rebasing...')).toBeInTheDocument();
320 });
321 dragAndDropCommits(
322 getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
323 'b',
324 );
325 fireEvent.click(screen.getByText('Run Rebase'));
326
327 // original optimistic is still there
328 expect(scanForkedBranchHashes('a')).toContain('d');
329 // new optimistic state is also there
330 expect(scanForkedBranchHashes('b')).toEqual(['e']);
331 });
332 });
333});
334