9.2 KB296 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, waitFor} from '@testing-library/react';
9import App from '../App';
10import {generatedFileCache} from '../GeneratedFile';
11import {__TEST__} from '../UncommittedChanges';
12import {readAtom, writeAtom} from '../jotaiUtils';
13import platform from '../platform';
14import {ignoreRTL} from '../testQueries';
15import {
16 closeCommitInfoSidebar,
17 COMMIT,
18 expectMessageSentToServer,
19 openCommitInfoSidebar,
20 resetTestMessages,
21 simulateCommits,
22 simulateMessageFromServer,
23 simulateRepoConnected,
24 simulateUncommittedChangedFiles,
25} from '../testUtils';
26import {GeneratedStatus} from '../types';
27
28/** Generated `num` files, in the repeating pattern: generated, partially generated, manual */
29async function simulateGeneratedFiles(num: number) {
30 const files = new Array(num).fill(null).map((_, i) => `file_${zeroPad(i)}.txt`);
31 act(() => {
32 simulateUncommittedChangedFiles({
33 value: files.map(path => ({
34 path,
35 status: 'M',
36 })),
37 });
38 });
39 await waitFor(() => {
40 expectMessageSentToServer({
41 type: 'fetchGeneratedStatuses',
42 paths: expect.anything(),
43 });
44 });
45 act(() => {
46 simulateMessageFromServer({
47 type: 'fetchedGeneratedStatuses',
48 results: Object.fromEntries(
49 files.map((path, i) => [
50 path,
51 i % 3 === 0
52 ? GeneratedStatus.Generated
53 : i % 3 === 1
54 ? GeneratedStatus.PartiallyGenerated
55 : GeneratedStatus.Manual,
56 ]),
57 ),
58 });
59 });
60}
61
62function zeroPad(n: number): string {
63 return ('000' + n.toString()).slice(-3);
64}
65
66describe('Generated Files', () => {
67 beforeEach(() => {
68 resetTestMessages();
69 render(<App />);
70 generatedFileCache.clear();
71 act(() => {
72 simulateRepoConnected();
73 closeCommitInfoSidebar();
74 expectMessageSentToServer({
75 type: 'subscribe',
76 kind: 'smartlogCommits',
77 subscriptionID: expect.anything(),
78 });
79 simulateCommits({
80 value: [
81 COMMIT('1', 'some public base', '0', {phase: 'public'}),
82 COMMIT('a', 'My Commit', '1'),
83 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
84 ],
85 });
86 expectMessageSentToServer({
87 type: 'subscribe',
88 kind: 'uncommittedChanges',
89 subscriptionID: expect.anything(),
90 });
91 });
92 });
93
94 it('fetches generated files for uncommitted changes', async () => {
95 await simulateGeneratedFiles(5);
96 expectMessageSentToServer({
97 type: 'fetchGeneratedStatuses',
98 paths: ['file_000.txt', 'file_001.txt', 'file_002.txt', 'file_003.txt', 'file_004.txt'],
99 });
100 });
101
102 it('Shows generated files in their own sections', async () => {
103 await simulateGeneratedFiles(10);
104
105 expect(screen.getByText(ignoreRTL('file_002.txt'))).toBeInTheDocument();
106 expect(screen.getByText(ignoreRTL('file_005.txt'))).toBeInTheDocument();
107 expect(screen.getByText(ignoreRTL('file_008.txt'))).toBeInTheDocument();
108 expect(screen.getByText('Generated Files')).toBeInTheDocument();
109 expect(screen.getByText('Partially Generated Files')).toBeInTheDocument();
110 });
111
112 function goToNextPage() {
113 fireEvent.click(screen.getByTestId('changed-files-next-page'));
114 }
115
116 function expectHasPartiallyGeneratedFiles() {
117 expect(screen.queryByText('Partially Generated Files')).toBeInTheDocument();
118 }
119 function expectHasGeneratedFiles() {
120 expect(screen.queryByText('Generated Files')).toBeInTheDocument();
121 }
122 function expectNOTHasGeneratedFiles() {
123 expect(screen.queryByText('Generated Files')).not.toBeInTheDocument();
124 }
125
126 function getChangedFiles() {
127 const found = [...document.querySelectorAll('.changed-file-path-text')].map(e =>
128 (e as HTMLElement).innerHTML.replace(/\u200E/g, ''),
129 );
130 return found;
131 }
132
133 it('Paginates generated files', async () => {
134 await simulateGeneratedFiles(1200);
135 // 1200 files, but 1000 files per fetched batch of generated statuses.
136 // Sorted by status, that puts 1000/3 manual files, then 1000/3 partially generated, then 1000/3 generated,
137 // then the remaining 200/3 manual, 200/3 partially generated, and 200/3 generated,
138 // all in pages of 500 at a time.
139
140 // first page is manual and partial
141 expectHasPartiallyGeneratedFiles();
142 expectNOTHasGeneratedFiles();
143 expect(getChangedFiles()).toMatchSnapshot();
144
145 // next page has partial and generated
146 goToNextPage();
147 expectHasPartiallyGeneratedFiles();
148 expectHasGeneratedFiles();
149 expect(getChangedFiles()).toMatchSnapshot();
150
151 // next page has remaining files from all 3 types
152 goToNextPage();
153 expectHasPartiallyGeneratedFiles();
154 expectHasGeneratedFiles();
155 expect(getChangedFiles()).toMatchSnapshot();
156 });
157
158 it('Warns about too many files to fetch all generated statuses', async () => {
159 await simulateGeneratedFiles(1001);
160 expect(
161 screen.getByText('There are more than 1000 files, some files may appear out of order'),
162 ).toBeInTheDocument();
163 });
164
165 it('remembers expanded state', async () => {
166 writeAtom(__TEST__.generatedFilesInitiallyExpanded, true);
167
168 await simulateGeneratedFiles(1);
169
170 expect(screen.getByText(ignoreRTL('file_000.txt'))).toBeInTheDocument();
171 expect(screen.getByText('Generated Files')).toBeInTheDocument();
172 });
173
174 it('writes expanded state', async () => {
175 expect(readAtom(__TEST__.generatedFilesInitiallyExpanded)).toEqual(false);
176
177 await simulateGeneratedFiles(1);
178
179 fireEvent.click(screen.getByText('Generated Files'));
180
181 expect(readAtom(__TEST__.generatedFilesInitiallyExpanded)).toEqual(true);
182 });
183
184 it('clears generated files cache on refresh click', async () => {
185 act(() => {
186 simulateUncommittedChangedFiles({
187 value: [
188 {
189 path: 'file.txt',
190 status: 'M',
191 },
192 ],
193 });
194 });
195 await waitFor(() => {
196 expectMessageSentToServer({
197 type: 'fetchGeneratedStatuses',
198 paths: ['file.txt'],
199 });
200 });
201 act(() => {
202 simulateMessageFromServer({
203 type: 'fetchedGeneratedStatuses',
204 results: Object.fromEntries([['file.txt', GeneratedStatus.Manual]]),
205 });
206 });
207
208 expect(screen.queryByText('Generated Files')).not.toBeInTheDocument();
209
210 act(() => {
211 fireEvent.click(screen.getByTestId('refresh-button'));
212 });
213 await waitFor(() => {
214 expectMessageSentToServer({
215 type: 'fetchGeneratedStatuses',
216 paths: ['file.txt'],
217 });
218 });
219 act(() => {
220 simulateMessageFromServer({
221 type: 'fetchedGeneratedStatuses',
222 results: Object.fromEntries([['file.txt', GeneratedStatus.Generated]]),
223 });
224 });
225
226 expect(screen.getByText('Generated Files')).toBeInTheDocument();
227 });
228
229 describe('Open All Files', () => {
230 beforeEach(() => act(() => openCommitInfoSidebar()));
231 async function simulateCommitWithFiles(files: Record<string, GeneratedStatus>) {
232 act(() => {
233 simulateCommits({
234 value: [
235 COMMIT('1', 'some public base', '0', {phase: 'public'}),
236 COMMIT('a', 'Commit A', '1', {
237 isDot: true,
238 totalFileCount: 3,
239 filePathsSample: Object.keys(files),
240 }),
241 ],
242 });
243 });
244 await waitFor(() => {
245 expectMessageSentToServer({
246 type: 'fetchGeneratedStatuses',
247 paths: expect.anything(),
248 });
249 });
250 act(() => {
251 simulateMessageFromServer({
252 type: 'fetchedGeneratedStatuses',
253 results: files,
254 });
255 });
256 }
257
258 it('No generated files, opens all files', async () => {
259 const openSpy = jest.spyOn(platform, 'openFiles').mockImplementation(() => {});
260 await simulateCommitWithFiles({
261 'file_partial.txt': GeneratedStatus.PartiallyGenerated,
262 'file_manual.txt': GeneratedStatus.Manual,
263 });
264
265 fireEvent.click(screen.getByText('Open All Files'));
266 expect(openSpy).toHaveBeenCalledTimes(1);
267 expect(openSpy).toHaveBeenCalledWith(['file_partial.txt', 'file_manual.txt']);
268 });
269
270 it('Some generated files, opens all non-generated files', async () => {
271 const openSpy = jest.spyOn(platform, 'openFiles').mockImplementation(() => {});
272 await simulateCommitWithFiles({
273 'file_gen.txt': GeneratedStatus.Generated,
274 'file_partial.txt': GeneratedStatus.PartiallyGenerated,
275 'file_manual.txt': GeneratedStatus.Manual,
276 });
277
278 fireEvent.click(screen.getByText('Open Non-Generated Files'));
279 expect(openSpy).toHaveBeenCalledTimes(1);
280 expect(openSpy).toHaveBeenCalledWith(['file_partial.txt', 'file_manual.txt']);
281 });
282
283 it('All generated files, opens all files', async () => {
284 const openSpy = jest.spyOn(platform, 'openFiles').mockImplementation(() => {});
285 await simulateCommitWithFiles({
286 'file_gen1.txt': GeneratedStatus.Generated,
287 'file_gen2.txt': GeneratedStatus.Generated,
288 });
289
290 fireEvent.click(screen.getByText('Open All Files'));
291 expect(openSpy).toHaveBeenCalledTimes(1);
292 expect(openSpy).toHaveBeenCalledWith(['file_gen1.txt', 'file_gen2.txt']);
293 });
294 });
295});
296