12.0 KB388 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, within} from '@testing-library/react';
9import {nextTick} from 'shared/testUtils';
10import App from '../../App';
11import platform from '../../platform';
12import {CommitInfoTestUtils, CommitTreeListTestUtils, ignoreRTL} from '../../testQueries';
13import {
14 COMMIT,
15 expectMessageNOTSentToServer,
16 expectMessageSentToServer,
17 resetTestMessages,
18 simulateCommits,
19 simulateUncommittedChangedFiles,
20} from '../../testUtils';
21import {CommandRunner} from '../../types';
22
23/* eslint-disable require-await */
24
25describe('RevertOperation', () => {
26 beforeEach(() => {
27 resetTestMessages();
28 render(<App />);
29 act(() => {
30 expectMessageSentToServer({
31 type: 'subscribe',
32 kind: 'smartlogCommits',
33 subscriptionID: expect.anything(),
34 });
35 simulateCommits({
36 value: [
37 COMMIT('c', 'Commit C', 'b', {
38 filePathsSample: ['file.txt'],
39 totalFileCount: 1,
40 isDot: true,
41 }),
42 COMMIT('b', 'Commit B', 'a', {filePathsSample: ['file.txt'], totalFileCount: 1}),
43 COMMIT('a', 'Commit A', '1', {filePathsSample: ['file.txt'], totalFileCount: 1}),
44 COMMIT('1', 'Commit 1', '0', {phase: 'public'}),
45 ],
46 });
47 });
48
49 // confirm all prompts about reverting files
50 jest.spyOn(platform, 'confirm').mockImplementation(() => Promise.resolve(true));
51 });
52
53 const clickRevert = async (inside: HTMLElement, fileName: string) => {
54 await act(async () => {
55 const revertButton = within(
56 within(inside).getByTestId(`changed-file-${fileName}`),
57 ).getByTestId('file-revert-button');
58 expect(revertButton).toBeInTheDocument();
59 fireEvent.click(revertButton);
60 // confirm modal takes 1 tick to resolve
61 await nextTick();
62 });
63 };
64
65 const clickDelete = async (inside: HTMLElement, fileName: string) => {
66 await act(async () => {
67 const revertButton = within(
68 within(inside).getByTestId(`changed-file-${fileName}`),
69 ).getByTestId('file-action-delete');
70 expect(revertButton).toBeInTheDocument();
71 fireEvent.click(revertButton);
72 // confirm modal takes 1 tick to resolve
73 await nextTick();
74 });
75 };
76
77 const clickCheckboxForFile = async (inside: HTMLElement, fileName: string) => {
78 await act(async () => {
79 const checkbox = within(within(inside).getByTestId(`changed-file-${fileName}`)).getByTestId(
80 'file-selection-checkbox',
81 );
82 expect(checkbox).toBeInTheDocument();
83 fireEvent.click(checkbox);
84 });
85 };
86
87 describe('from uncommitted changes', () => {
88 beforeEach(() => {
89 act(() => {
90 simulateUncommittedChangedFiles({
91 value: [
92 {path: 'myFile1.txt', status: 'M'},
93 {path: 'myFile2.txt', status: 'M'},
94 ],
95 });
96 });
97 });
98
99 it('runs revert from uncommitted changes', async () => {
100 await clickRevert(screen.getByTestId('commit-tree-root'), 'myFile1.txt');
101
102 expectMessageSentToServer({
103 type: 'runOperation',
104 operation: {
105 args: ['revert', {type: 'repo-relative-file-list', paths: ['myFile1.txt']}],
106 id: expect.anything(),
107 runner: CommandRunner.Sapling,
108 trackEventName: 'RevertOperation',
109 },
110 });
111 });
112
113 it('renders optimistic state while running revert', async () => {
114 expect(
115 CommitTreeListTestUtils.withinCommitTree().getByText(ignoreRTL('myFile1.txt')),
116 ).toBeInTheDocument();
117 await clickRevert(screen.getByTestId('commit-tree-root'), 'myFile1.txt');
118 expect(
119 CommitTreeListTestUtils.withinCommitTree().queryByText(ignoreRTL('myFile1.txt')),
120 ).not.toBeInTheDocument();
121 });
122
123 describe('untracked files get purged', () => {
124 beforeEach(() => {
125 act(() => {
126 simulateUncommittedChangedFiles({
127 value: [
128 {path: 'myFile1.txt', status: 'M'},
129 {path: 'untracked.txt', status: '?'},
130 ],
131 });
132 });
133 });
134
135 it('runs purge for untracked uncommitted changes', async () => {
136 await clickDelete(screen.getByTestId('commit-tree-root'), 'untracked.txt');
137
138 expectMessageSentToServer({
139 type: 'runOperation',
140 operation: {
141 args: [
142 'purge',
143 '--files',
144 '--abort-on-err',
145 {type: 'repo-relative-file-list', paths: ['untracked.txt']},
146 ],
147 id: expect.anything(),
148 runner: CommandRunner.Sapling,
149 trackEventName: 'PurgeOperation',
150 },
151 });
152 });
153
154 it('renders optimistic state while running purge', async () => {
155 expect(
156 CommitTreeListTestUtils.withinCommitTree().getByText(ignoreRTL('untracked.txt')),
157 ).toBeInTheDocument();
158 await clickDelete(screen.getByTestId('commit-tree-root'), 'untracked.txt');
159 expect(
160 CommitTreeListTestUtils.withinCommitTree().queryByText(ignoreRTL('untracked.txt')),
161 ).not.toBeInTheDocument();
162 });
163 });
164 });
165
166 describe('bulk discard', () => {
167 let confirmSpy: jest.SpyInstance;
168 beforeEach(() => {
169 confirmSpy = jest.spyOn(platform, 'confirm').mockImplementation(() => Promise.resolve(true));
170 act(() => {
171 simulateUncommittedChangedFiles({
172 value: [
173 {path: 'myFile1.txt', status: 'M'},
174 {path: 'myFile2.txt', status: 'M'},
175 {path: 'untracked1.txt', status: '?'},
176 {path: 'untracked2.txt', status: '?'},
177 ],
178 });
179 });
180 });
181
182 it('discards all changes with goto --clean if everything selected', async () => {
183 await act(async () => {
184 fireEvent.click(
185 within(screen.getByTestId('commit-tree-root')).getByTestId('discard-all-selected-button'),
186 );
187 });
188
189 expectMessageSentToServer({
190 type: 'runOperation',
191 operation: {
192 args: ['goto', '--clean', '.'],
193 id: expect.anything(),
194 runner: CommandRunner.Sapling,
195 trackEventName: 'DiscardOperation',
196 },
197 });
198
199 expectMessageSentToServer({
200 type: 'runOperation',
201 operation: {
202 args: ['purge', '--files', '--abort-on-err'],
203 id: expect.anything(),
204 runner: CommandRunner.Sapling,
205 trackEventName: 'PurgeOperation',
206 },
207 });
208
209 expect(confirmSpy).toHaveBeenCalled();
210 });
211
212 it('discards selected changes with revert and purge', async () => {
213 const commitTree = screen.getByTestId('commit-tree-root');
214 await clickCheckboxForFile(commitTree, 'myFile1.txt');
215 await clickCheckboxForFile(commitTree, 'untracked1.txt');
216
217 await act(async () => {
218 fireEvent.click(
219 within(screen.getByTestId('commit-tree-root')).getByTestId('discard-all-selected-button'),
220 );
221 });
222
223 expectMessageSentToServer({
224 type: 'runOperation',
225 operation: {
226 args: ['revert', {type: 'repo-relative-file-list', paths: ['myFile2.txt']}],
227 id: expect.anything(),
228 runner: CommandRunner.Sapling,
229 trackEventName: 'RevertOperation',
230 },
231 });
232
233 expectMessageSentToServer({
234 type: 'runOperation',
235 operation: {
236 args: [
237 'purge',
238 '--files',
239 '--abort-on-err',
240 {type: 'repo-relative-file-list', paths: ['untracked2.txt']},
241 ],
242 id: expect.anything(),
243 runner: CommandRunner.Sapling,
244 trackEventName: 'PurgeOperation',
245 },
246 });
247
248 expect(confirmSpy).toHaveBeenCalled();
249 });
250
251 it('uses purge for added and renamed files for selected changes', async () => {
252 act(() => {
253 simulateUncommittedChangedFiles({
254 value: [
255 {path: 'myFile1.txt', status: 'A'},
256 {path: 'myFile2.txt', status: 'A'},
257 {path: 'movedFrom.txt', status: 'R'},
258 {path: 'movedTo.txt', status: 'A', copy: 'movedFrom.txt'},
259 ],
260 });
261 });
262 const commitTree = screen.getByTestId('commit-tree-root');
263 await clickCheckboxForFile(commitTree, 'myFile2.txt');
264
265 await act(async () => {
266 fireEvent.click(
267 within(screen.getByTestId('commit-tree-root')).getByTestId('discard-all-selected-button'),
268 );
269 });
270
271 expectMessageSentToServer({
272 type: 'runOperation',
273 operation: {
274 args: [
275 'revert',
276 {
277 type: 'repo-relative-file-list',
278 paths: ['myFile1.txt', 'movedFrom.txt', 'movedTo.txt'],
279 },
280 ],
281 id: expect.anything(),
282 runner: CommandRunner.Sapling,
283 trackEventName: 'RevertOperation',
284 },
285 });
286
287 expectMessageSentToServer({
288 type: 'runOperation',
289 operation: {
290 args: [
291 'purge',
292 '--files',
293 '--abort-on-err',
294 {type: 'repo-relative-file-list', paths: ['myFile1.txt', 'movedTo.txt']},
295 ],
296 id: expect.anything(),
297 runner: CommandRunner.Sapling,
298 trackEventName: 'PurgeOperation',
299 },
300 });
301
302 expect(confirmSpy).toHaveBeenCalled();
303 });
304
305 it('no need to run purge if no files are untracked', async () => {
306 const commitTree = screen.getByTestId('commit-tree-root');
307 await clickCheckboxForFile(commitTree, 'untracked1.txt');
308 await clickCheckboxForFile(commitTree, 'untracked2.txt');
309
310 await act(async () => {
311 fireEvent.click(
312 within(screen.getByTestId('commit-tree-root')).getByTestId('discard-all-selected-button'),
313 );
314 });
315
316 expectMessageSentToServer({
317 type: 'runOperation',
318 operation: {
319 args: [
320 'revert',
321 {type: 'repo-relative-file-list', paths: ['myFile1.txt', 'myFile2.txt']},
322 ],
323 id: expect.anything(),
324 runner: CommandRunner.Sapling,
325 trackEventName: 'RevertOperation',
326 },
327 });
328
329 expectMessageNOTSentToServer({
330 type: 'runOperation',
331 operation: {
332 args: expect.arrayContaining(['purge', '--files']),
333 id: expect.anything(),
334 runner: CommandRunner.Sapling,
335 trackEventName: expect.anything(),
336 },
337 });
338
339 expect(confirmSpy).toHaveBeenCalled();
340 });
341 });
342
343 describe('in commit info view for a given commit', () => {
344 it('hides revert button on non-head commits', () => {
345 CommitInfoTestUtils.clickToSelectCommit('a');
346
347 const revertButton = within(
348 within(screen.getByTestId('commit-info-view')).getByTestId(`changed-file-file.txt`),
349 ).queryByTestId('file-revert-button');
350 expect(revertButton).not.toBeInTheDocument();
351 });
352
353 it('reverts before head commit', async () => {
354 CommitInfoTestUtils.clickToSelectCommit('c');
355 await clickRevert(screen.getByTestId('commit-info-view'), 'file.txt');
356
357 expectMessageSentToServer({
358 type: 'runOperation',
359 operation: {
360 args: [
361 'revert',
362 '--rev',
363 {type: 'succeedable-revset', revset: '.^'},
364 {type: 'repo-relative-file-list', paths: ['file.txt']},
365 ],
366 id: expect.anything(),
367 runner: CommandRunner.Sapling,
368 trackEventName: 'RevertOperation',
369 },
370 });
371 });
372
373 it('renders optimistic state while running', async () => {
374 CommitInfoTestUtils.clickToSelectCommit('c');
375 expect(
376 CommitTreeListTestUtils.withinCommitTree().queryByText(ignoreRTL('file.txt')),
377 ).not.toBeInTheDocument();
378
379 await clickRevert(screen.getByTestId('commit-info-view'), 'file.txt');
380
381 // file is not hidden from the tree, instead it's inserted
382 expect(
383 CommitTreeListTestUtils.withinCommitTree().getByText(ignoreRTL('file.txt')),
384 ).toBeInTheDocument();
385 });
386 });
387});
388