5.8 KB175 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 {ChangedFile, ChangedFileStatus, RepoRelativePath} from '../types';
9
10import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
11import {nextTick} from 'shared/testUtils';
12import App from '../App';
13import {__TEST__} from '../ChangedFilesWithFetching';
14import {CommitInfoTestUtils, ignoreRTL} from '../testQueries';
15import {
16 COMMIT,
17 expectMessageNOTSentToServer,
18 expectMessageSentToServer,
19 resetTestMessages,
20 simulateCommits,
21 simulateMessageFromServer,
22 simulateRepoConnected,
23} from '../testUtils';
24import {leftPad} from '../utils';
25
26function makeFiles(n: number): Array<RepoRelativePath> {
27 return new Array(n).fill(null).map((_, i) => `file${leftPad(i, 3, '0')}.txt`);
28}
29function withStatus(files: Array<RepoRelativePath>, status: ChangedFileStatus): Array<ChangedFile> {
30 return files.map(path => ({path, status}));
31}
32
33describe('ChangedFilesWithFetching', () => {
34 beforeEach(() => {
35 resetTestMessages();
36 render(<App />);
37 act(() => {
38 simulateRepoConnected();
39 expectMessageSentToServer({
40 type: 'subscribe',
41 kind: 'smartlogCommits',
42 subscriptionID: expect.anything(),
43 });
44 simulateCommits({
45 value: [
46 COMMIT('1', 'some public base', '0', {phase: 'public', isDot: true}),
47 COMMIT('a', 'My Commit', '1', {
48 totalFileCount: 2,
49 filePathsSample: ['file1.js', 'file2.js'],
50 }),
51 COMMIT('b', 'Another Commit', 'a', {
52 totalFileCount: 700,
53 filePathsSample: makeFiles(500),
54 }),
55 COMMIT('c', 'Another Commit 2', 'a', {
56 totalFileCount: 700,
57 filePathsSample: makeFiles(500),
58 }),
59 ],
60 });
61 });
62 });
63
64 afterEach(() => {
65 // reset cache between tests
66 __TEST__.commitFilesCache.clear();
67 });
68
69 async function waitForNextPageToLoad() {
70 await waitFor(() => {
71 expect(screen.getByTestId('changed-files-next-page')).toBeInTheDocument();
72 });
73 }
74
75 it("Does not fetch files if they're already all fetched", () => {
76 CommitInfoTestUtils.clickToSelectCommit('a');
77
78 expectMessageNOTSentToServer({
79 type: 'fetchCommitChangedFiles',
80 hash: 'a',
81 limit: expect.anything(),
82 });
83 });
84
85 it('Fetches files and shows additional pages', async () => {
86 CommitInfoTestUtils.clickToSelectCommit('b');
87
88 expectMessageSentToServer({type: 'fetchCommitChangedFiles', hash: 'b', limit: undefined});
89 act(() => {
90 simulateMessageFromServer({
91 type: 'fetchedCommitChangedFiles',
92 hash: 'b',
93 result: {value: {filesSample: withStatus(makeFiles(510), 'M'), totalFileCount: 510}},
94 });
95 });
96
97 await waitForNextPageToLoad();
98
99 expect(screen.getByText(ignoreRTL('file000.txt'))).toBeInTheDocument();
100 expect(screen.getByText(ignoreRTL('file499.txt'))).toBeInTheDocument();
101 expect(screen.queryByText(ignoreRTL('file500.txt'))).not.toBeInTheDocument();
102 fireEvent.click(screen.getByTestId('changed-files-next-page'));
103 expect(screen.queryByText(ignoreRTL('file499.txt'))).not.toBeInTheDocument();
104 expect(screen.getByText(ignoreRTL('file500.txt'))).toBeInTheDocument();
105 expect(screen.getByText(ignoreRTL('file509.txt'))).toBeInTheDocument();
106 });
107
108 it('Caches files', () => {
109 CommitInfoTestUtils.clickToSelectCommit('c');
110 CommitInfoTestUtils.clickToSelectCommit('a');
111 resetTestMessages();
112
113 CommitInfoTestUtils.clickToSelectCommit('c');
114 expectMessageNOTSentToServer({type: 'fetchCommitChangedFiles', hash: expect.anything()});
115 });
116
117 it('Handles race condition when switching commits and responses come in wrong order', async () => {
118 // Select commit 'b' - this will start fetching files
119 CommitInfoTestUtils.clickToSelectCommit('b');
120 expectMessageSentToServer({type: 'fetchCommitChangedFiles', hash: 'b', limit: undefined});
121
122 // Before 'b' response comes back, switch to commit 'c'
123 CommitInfoTestUtils.clickToSelectCommit('c');
124 expectMessageSentToServer({type: 'fetchCommitChangedFiles', hash: 'c', limit: undefined});
125
126 // Simulate 'c' response coming back first (fast)
127 act(() => {
128 simulateMessageFromServer({
129 type: 'fetchedCommitChangedFiles',
130 hash: 'c',
131 result: {
132 value: {
133 filesSample: withStatus(['fileC1.txt', 'fileC2.txt', 'fileC3.txt'], 'M'),
134 totalFileCount: 3,
135 },
136 },
137 });
138 });
139
140 // Verify we're showing commit 'c' files
141 await waitFor(() => {
142 expect(screen.getByText(ignoreRTL('fileC1.txt'))).toBeInTheDocument();
143 expect(screen.getByText(ignoreRTL('fileC2.txt'))).toBeInTheDocument();
144 });
145
146 // Now simulate 'b' response coming back late (slow)
147 act(() => {
148 simulateMessageFromServer({
149 type: 'fetchedCommitChangedFiles',
150 hash: 'b',
151 result: {
152 value: {
153 filesSample: withStatus(['fileB1.txt', 'fileB2.txt', 'fileB3.txt'], 'M'),
154 totalFileCount: 3,
155 },
156 },
157 });
158 });
159
160 // Wait a bit to ensure any state updates have completed
161 await nextTick();
162
163 await waitFor(() => {
164 // The files should STILL be from commit 'c', not 'b'
165 // With the fix enabled, the cancellation token prevents 'b' from overwriting 'c'
166 expect(screen.getByText(ignoreRTL('fileC1.txt'))).toBeInTheDocument();
167 expect(screen.getByText(ignoreRTL('fileC2.txt'))).toBeInTheDocument();
168 });
169
170 // Verify we're NOT showing commit 'b' files
171 expect(screen.queryByText(ignoreRTL('fileB1.txt'))).not.toBeInTheDocument();
172 expect(screen.queryByText(ignoreRTL('fileB2.txt'))).not.toBeInTheDocument();
173 });
174});
175