13.2 KB368 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 {ChangedFileStatus, RepoRelativePath, UncommittedChanges} from '../types';
9
10import {act, fireEvent, render, screen} from '@testing-library/react';
11import userEvent from '@testing-library/user-event';
12import App from '../App';
13import {defaultChangedFilesDisplayType} from '../ChangedFileDisplayTypePicker';
14import {CommitInfoTestUtils, ignoreRTL} from '../testQueries';
15import {
16 closeCommitInfoSidebar,
17 COMMIT,
18 expectMessageSentToServer,
19 resetTestMessages,
20 simulateCommits,
21 simulateMessageFromServer,
22 simulateRepoConnected,
23 simulateUncommittedChangedFiles,
24} from '../testUtils';
25import {leftPad} from '../utils';
26
27jest.mock('isl-components/OperatingSystem', () => ({
28 isMac: true,
29}));
30
31describe('Changed Files', () => {
32 beforeEach(() => {
33 resetTestMessages();
34 render(<App />);
35 act(() => {
36 simulateRepoConnected();
37 closeCommitInfoSidebar();
38 expectMessageSentToServer({
39 type: 'subscribe',
40 kind: 'smartlogCommits',
41 subscriptionID: expect.anything(),
42 });
43 simulateCommits({
44 value: [
45 COMMIT('1', 'some public base', '0', {phase: 'public'}),
46 COMMIT('a', 'My Commit', '1'),
47 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
48 ],
49 });
50 // Reset to the default display type.
51 act(() => {
52 simulateMessageFromServer({
53 type: 'gotConfig',
54 name: 'isl.changedFilesDisplayType',
55 value: JSON.stringify(defaultChangedFilesDisplayType),
56 });
57 });
58 expectMessageSentToServer({
59 type: 'subscribe',
60 kind: 'uncommittedChanges',
61 subscriptionID: expect.anything(),
62 });
63 simulateUncommittedChangedFiles({
64 value: [
65 {path: 'file1.js', status: 'M'},
66 {path: 'src/file2.js', status: 'A'},
67 {path: 'src/file3.js', status: 'A'},
68 {path: 'src/a/foo.js', status: 'M'},
69 {path: 'src/b/foo.js', status: 'M'},
70 {path: 'src/subfolder/file4.js', status: 'R'},
71 {path: 'src/subfolder/another/yet/another/file5.js', status: 'R'},
72 ],
73 });
74 });
75 });
76
77 function openChangedFileStatusPicker() {
78 const picker = screen.getByTestId('changed-file-display-type-picker');
79 expect(picker).toBeInTheDocument();
80
81 act(() => {
82 fireEvent.click(picker);
83 });
84 }
85
86 it('Allows picking changed files display type', () => {
87 openChangedFileStatusPicker();
88 expect(screen.getByText('Short file names')).toBeInTheDocument();
89 expect(screen.getByText('Full file paths')).toBeInTheDocument();
90 expect(screen.getByText('Tree')).toBeInTheDocument();
91 expect(screen.getByText('One-letter directories')).toBeInTheDocument();
92 });
93
94 it('Persists choice for display type', () => {
95 openChangedFileStatusPicker();
96 act(() => {
97 fireEvent.click(screen.getByText('Tree'));
98 });
99 expectMessageSentToServer({
100 type: 'setConfig',
101 name: 'isl.changedFilesDisplayType',
102 value: '"tree"',
103 });
104 });
105
106 it('Updates when config is fetched', () => {
107 openChangedFileStatusPicker();
108 act(() => {
109 simulateMessageFromServer({
110 type: 'gotConfig',
111 name: 'isl.changedFilesDisplayType',
112 value: '"fullPaths"',
113 });
114 });
115 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
116 });
117
118 it('Uses LTR markers to render paths correctly', () => {
119 act(() => {
120 simulateUncommittedChangedFiles({
121 value: [
122 {path: '.gitignore', status: 'M'},
123 {path: 'src/.gitignore', status: 'A'},
124 ],
125 });
126 });
127 expect(screen.getByText('\u200E.gitignore\u200E')).toBeInTheDocument();
128 expect(screen.getByText('\u200Esrc/.gitignore\u200E')).toBeInTheDocument();
129 });
130
131 describe('default changed files', () => {
132 it('disambiguates file paths', () => {
133 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
134 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
135 expect(screen.getByText(ignoreRTL('a/foo.js'))).toBeInTheDocument();
136 expect(screen.getByText(ignoreRTL('b/foo.js'))).toBeInTheDocument();
137
138 expect(screen.queryByText(ignoreRTL('src/file2.js'))).not.toBeInTheDocument();
139 });
140 });
141
142 describe('full file paths', () => {
143 it('shows full paths', () => {
144 openChangedFileStatusPicker();
145 act(() => {
146 fireEvent.click(screen.getByText('Full file paths'));
147 });
148
149 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
150 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
151 expect(screen.getByText(ignoreRTL('src/a/foo.js'))).toBeInTheDocument();
152 expect(screen.getByText(ignoreRTL('src/b/foo.js'))).toBeInTheDocument();
153 expect(
154 screen.getByText(ignoreRTL('src/subfolder/another/yet/another/file5.js')),
155 ).toBeInTheDocument();
156 });
157
158 it('shows full paths when holding alt', () => {
159 expect(screen.queryByText(ignoreRTL('src/b/foo.js'))).not.toBeInTheDocument();
160 act(() => {
161 userEvent.keyboard('{Alt>}'); // '>' means keep pressed
162 });
163 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
164 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
165 expect(screen.getByText(ignoreRTL('src/a/foo.js'))).toBeInTheDocument();
166 expect(screen.getByText(ignoreRTL('src/b/foo.js'))).toBeInTheDocument();
167 expect(
168 screen.getByText(ignoreRTL('src/subfolder/another/yet/another/file5.js')),
169 ).toBeInTheDocument();
170 });
171 });
172
173 describe('one-letter-per-directory file paths', () => {
174 it('shows one-letter-per-directory file paths', () => {
175 openChangedFileStatusPicker();
176 act(() => {
177 fireEvent.click(screen.getByText('One-letter directories'));
178 });
179
180 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
181 expect(screen.getByText(ignoreRTL('s/file2.js'))).toBeInTheDocument();
182 expect(screen.getByText(ignoreRTL('s/a/foo.js'))).toBeInTheDocument();
183 expect(screen.getByText(ignoreRTL('s/b/foo.js'))).toBeInTheDocument();
184 expect(screen.getByText(ignoreRTL('s/s/file4.js'))).toBeInTheDocument();
185 expect(screen.getByText(ignoreRTL('s/s/a/y/a/file5.js'))).toBeInTheDocument();
186 });
187 });
188
189 describe('tree', () => {
190 beforeEach(() => {
191 openChangedFileStatusPicker();
192 act(() => {
193 fireEvent.click(screen.getByText('Tree'));
194 });
195 });
196
197 it('shows non-disambiguated file basenames', () => {
198 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
199 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
200 expect(screen.getByText(ignoreRTL('file3.js'))).toBeInTheDocument();
201 expect(screen.getAllByText(ignoreRTL('foo.js'))).toHaveLength(2);
202 expect(screen.getByText(ignoreRTL('file4.js'))).toBeInTheDocument();
203 expect(screen.getByText(ignoreRTL('file5.js'))).toBeInTheDocument();
204 });
205
206 it('shows folder names', () => {
207 expect(screen.getByText(ignoreRTL('src'))).toBeInTheDocument();
208 expect(screen.getByText(ignoreRTL('a'))).toBeInTheDocument();
209 expect(screen.getByText(ignoreRTL('b'))).toBeInTheDocument();
210 expect(screen.getByText(ignoreRTL('subfolder'))).toBeInTheDocument();
211 expect(screen.getByText(ignoreRTL('another/yet/another'))).toBeInTheDocument();
212 });
213
214 it('clicking folder name hides contents', () => {
215 act(() => {
216 fireEvent.click(screen.getByText('subfolder'));
217 });
218 expect(screen.queryByText(ignoreRTL('file4.js'))).not.toBeInTheDocument();
219 expect(screen.queryByText(ignoreRTL('file5.js'))).not.toBeInTheDocument();
220
221 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
222 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
223 expect(screen.getByText(ignoreRTL('file3.js'))).toBeInTheDocument();
224 });
225
226 it('clicking folders with the same name do not collapse each other', () => {
227 act(() => {
228 simulateUncommittedChangedFiles({
229 value: [
230 {path: 'a/foo/file1.js', status: 'M'},
231 {path: 'a/file2.js', status: 'M'},
232 {path: 'b/foo/file3.js', status: 'M'},
233 {path: 'b/file4.js', status: 'M'},
234 ],
235 });
236 });
237 act(() => {
238 fireEvent.click(screen.getAllByText('foo')[0]);
239 });
240 expect(screen.queryByText(ignoreRTL('file1.js'))).not.toBeInTheDocument();
241 expect(screen.queryByText(ignoreRTL('file3.js'))).toBeInTheDocument();
242 });
243 });
244
245 describe('truncated list of changed files', () => {
246 function makeFiles(n: number): Array<RepoRelativePath> {
247 return new Array(n).fill(null).map((_, i) => `file${leftPad(i, 3, '0')}.txt`);
248 }
249
250 function withStatus(
251 changes: Array<RepoRelativePath>,
252 status: ChangedFileStatus,
253 ): UncommittedChanges {
254 return changes.map(path => ({path, status}));
255 }
256
257 it('only first 500 files are shown', () => {
258 act(() => {
259 simulateUncommittedChangedFiles({
260 value: withStatus(makeFiles(510), 'M'),
261 });
262 });
263 const files = screen.getAllByText(/file\d+\.txt/);
264 expect(files).toHaveLength(500);
265 });
266
267 it('banner is shown if some files are hidden', () => {
268 act(() => {
269 simulateUncommittedChangedFiles({
270 value: withStatus(makeFiles(700), 'M'),
271 });
272 });
273 expect(screen.getByText('Showing first 500 files out of 700 total')).toBeInTheDocument();
274 });
275
276 it('if more than 500 files are provided, there are page navigation buttons', () => {
277 act(() => {
278 simulateUncommittedChangedFiles({
279 value: withStatus(makeFiles(510), 'M'),
280 });
281 });
282 expect(screen.getByTestId('changed-files-next-page')).toBeInTheDocument();
283 expect(screen.getByTestId('changed-files-previous-page')).toBeInTheDocument();
284 expect(screen.getByTestId('changed-files-previous-page')).toBeDisabled();
285 expect(screen.getByText('Showing first 500 files out of 510 total')).toBeInTheDocument();
286 });
287
288 it('can click buttons to navigate pages', () => {
289 act(() => {
290 simulateUncommittedChangedFiles({
291 value: withStatus(makeFiles(1010), 'M'),
292 });
293 });
294 fireEvent.click(screen.getByTestId('changed-files-next-page'));
295 expect(screen.getByText('Showing files 501 – 1000 out of 1010 total')).toBeInTheDocument();
296 fireEvent.click(screen.getByTestId('changed-files-next-page'));
297 expect(screen.getByText('Showing files 1001 – 1010 out of 1010 total')).toBeInTheDocument();
298
299 expect(screen.getByTestId('changed-files-next-page')).toBeDisabled();
300
301 fireEvent.click(screen.getByTestId('changed-files-previous-page'));
302 expect(screen.getByText('Showing files 501 – 1000 out of 1010 total')).toBeInTheDocument();
303 fireEvent.click(screen.getByTestId('changed-files-previous-page'));
304 expect(screen.getByText('Showing first 500 files out of 1010 total')).toBeInTheDocument();
305 });
306
307 it("if more than 500 files exist, but only 500 are provided, don't show pagination buttons", () => {
308 act(() => {
309 simulateUncommittedChangedFiles({
310 value: [],
311 });
312 simulateCommits({
313 value: [
314 COMMIT('1', 'some public base', '0', {phase: 'public'}),
315 COMMIT('a', 'Commit', '1', {
316 isDot: true,
317 filePathsSample: makeFiles(500),
318 totalFileCount: 1010,
319 }),
320 ],
321 });
322 CommitInfoTestUtils.openCommitInfoSidebar();
323 });
324
325 const changedFiles = CommitInfoTestUtils.withinCommitInfo().getByTestId('changed-files');
326 expect(changedFiles).toBeInTheDocument();
327
328 // banner shows truncation
329 expect(
330 CommitInfoTestUtils.withinCommitInfo().getByText(
331 'Showing first 500 files out of 1010 total',
332 ),
333 ).toBeInTheDocument();
334
335 // but no pagination buttons, since we only provide first 25 anyway
336 expect(
337 CommitInfoTestUtils.withinCommitInfo().queryByTestId('changed-files-next-page'),
338 ).not.toBeInTheDocument();
339 expect(
340 CommitInfoTestUtils.withinCommitInfo().queryByTestId('changed-files-previous-page'),
341 ).not.toBeInTheDocument();
342 });
343
344 it('if the number of files changes, restrict the page number to fit', () => {
345 act(() => {
346 simulateUncommittedChangedFiles({
347 value: withStatus(makeFiles(2020), 'M'),
348 });
349 });
350 fireEvent.click(screen.getByTestId('changed-files-next-page'));
351 fireEvent.click(screen.getByTestId('changed-files-next-page'));
352 fireEvent.click(screen.getByTestId('changed-files-next-page'));
353 fireEvent.click(screen.getByTestId('changed-files-next-page'));
354 expect(screen.getByText('Showing files 2001 – 2020 out of 2020 total')).toBeInTheDocument();
355
356 // now some file changes are removed (e.g. discarded)
357 act(() => {
358 simulateUncommittedChangedFiles({
359 value: withStatus(makeFiles(700), 'M'),
360 });
361 });
362
363 // ranges are remapped
364 expect(screen.getByText('Showing files 501 – 700 out of 700 total')).toBeInTheDocument();
365 });
366 });
367});
368