addons/isl/src/__tests__/ChangedFiles.test.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ChangedFileStatus, RepoRelativePath, UncommittedChanges} from '../types';
b69ab319
b69ab3110import {act, fireEvent, render, screen} from '@testing-library/react';
b69ab3111import userEvent from '@testing-library/user-event';
b69ab3112import App from '../App';
b69ab3113import {defaultChangedFilesDisplayType} from '../ChangedFileDisplayTypePicker';
b69ab3114import {CommitInfoTestUtils, ignoreRTL} from '../testQueries';
b69ab3115import {
b69ab3116 closeCommitInfoSidebar,
b69ab3117 COMMIT,
b69ab3118 expectMessageSentToServer,
b69ab3119 resetTestMessages,
b69ab3120 simulateCommits,
b69ab3121 simulateMessageFromServer,
b69ab3122 simulateRepoConnected,
b69ab3123 simulateUncommittedChangedFiles,
b69ab3124} from '../testUtils';
b69ab3125import {leftPad} from '../utils';
b69ab3126
b69ab3127jest.mock('isl-components/OperatingSystem', () => ({
b69ab3128 isMac: true,
b69ab3129}));
b69ab3130
b69ab3131describe('Changed Files', () => {
b69ab3132 beforeEach(() => {
b69ab3133 resetTestMessages();
b69ab3134 render(<App />);
b69ab3135 act(() => {
b69ab3136 simulateRepoConnected();
b69ab3137 closeCommitInfoSidebar();
b69ab3138 expectMessageSentToServer({
b69ab3139 type: 'subscribe',
b69ab3140 kind: 'smartlogCommits',
b69ab3141 subscriptionID: expect.anything(),
b69ab3142 });
b69ab3143 simulateCommits({
b69ab3144 value: [
b69ab3145 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab3146 COMMIT('a', 'My Commit', '1'),
b69ab3147 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
b69ab3148 ],
b69ab3149 });
b69ab3150 // Reset to the default display type.
b69ab3151 act(() => {
b69ab3152 simulateMessageFromServer({
b69ab3153 type: 'gotConfig',
b69ab3154 name: 'isl.changedFilesDisplayType',
b69ab3155 value: JSON.stringify(defaultChangedFilesDisplayType),
b69ab3156 });
b69ab3157 });
b69ab3158 expectMessageSentToServer({
b69ab3159 type: 'subscribe',
b69ab3160 kind: 'uncommittedChanges',
b69ab3161 subscriptionID: expect.anything(),
b69ab3162 });
b69ab3163 simulateUncommittedChangedFiles({
b69ab3164 value: [
b69ab3165 {path: 'file1.js', status: 'M'},
b69ab3166 {path: 'src/file2.js', status: 'A'},
b69ab3167 {path: 'src/file3.js', status: 'A'},
b69ab3168 {path: 'src/a/foo.js', status: 'M'},
b69ab3169 {path: 'src/b/foo.js', status: 'M'},
b69ab3170 {path: 'src/subfolder/file4.js', status: 'R'},
b69ab3171 {path: 'src/subfolder/another/yet/another/file5.js', status: 'R'},
b69ab3172 ],
b69ab3173 });
b69ab3174 });
b69ab3175 });
b69ab3176
b69ab3177 function openChangedFileStatusPicker() {
b69ab3178 const picker = screen.getByTestId('changed-file-display-type-picker');
b69ab3179 expect(picker).toBeInTheDocument();
b69ab3180
b69ab3181 act(() => {
b69ab3182 fireEvent.click(picker);
b69ab3183 });
b69ab3184 }
b69ab3185
b69ab3186 it('Allows picking changed files display type', () => {
b69ab3187 openChangedFileStatusPicker();
b69ab3188 expect(screen.getByText('Short file names')).toBeInTheDocument();
b69ab3189 expect(screen.getByText('Full file paths')).toBeInTheDocument();
b69ab3190 expect(screen.getByText('Tree')).toBeInTheDocument();
b69ab3191 expect(screen.getByText('One-letter directories')).toBeInTheDocument();
b69ab3192 });
b69ab3193
b69ab3194 it('Persists choice for display type', () => {
b69ab3195 openChangedFileStatusPicker();
b69ab3196 act(() => {
b69ab3197 fireEvent.click(screen.getByText('Tree'));
b69ab3198 });
b69ab3199 expectMessageSentToServer({
b69ab31100 type: 'setConfig',
b69ab31101 name: 'isl.changedFilesDisplayType',
b69ab31102 value: '"tree"',
b69ab31103 });
b69ab31104 });
b69ab31105
b69ab31106 it('Updates when config is fetched', () => {
b69ab31107 openChangedFileStatusPicker();
b69ab31108 act(() => {
b69ab31109 simulateMessageFromServer({
b69ab31110 type: 'gotConfig',
b69ab31111 name: 'isl.changedFilesDisplayType',
b69ab31112 value: '"fullPaths"',
b69ab31113 });
b69ab31114 });
b69ab31115 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
b69ab31116 });
b69ab31117
b69ab31118 it('Uses LTR markers to render paths correctly', () => {
b69ab31119 act(() => {
b69ab31120 simulateUncommittedChangedFiles({
b69ab31121 value: [
b69ab31122 {path: '.gitignore', status: 'M'},
b69ab31123 {path: 'src/.gitignore', status: 'A'},
b69ab31124 ],
b69ab31125 });
b69ab31126 });
b69ab31127 expect(screen.getByText('\u200E.gitignore\u200E')).toBeInTheDocument();
b69ab31128 expect(screen.getByText('\u200Esrc/.gitignore\u200E')).toBeInTheDocument();
b69ab31129 });
b69ab31130
b69ab31131 describe('default changed files', () => {
b69ab31132 it('disambiguates file paths', () => {
b69ab31133 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31134 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
b69ab31135 expect(screen.getByText(ignoreRTL('a/foo.js'))).toBeInTheDocument();
b69ab31136 expect(screen.getByText(ignoreRTL('b/foo.js'))).toBeInTheDocument();
b69ab31137
b69ab31138 expect(screen.queryByText(ignoreRTL('src/file2.js'))).not.toBeInTheDocument();
b69ab31139 });
b69ab31140 });
b69ab31141
b69ab31142 describe('full file paths', () => {
b69ab31143 it('shows full paths', () => {
b69ab31144 openChangedFileStatusPicker();
b69ab31145 act(() => {
b69ab31146 fireEvent.click(screen.getByText('Full file paths'));
b69ab31147 });
b69ab31148
b69ab31149 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31150 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
b69ab31151 expect(screen.getByText(ignoreRTL('src/a/foo.js'))).toBeInTheDocument();
b69ab31152 expect(screen.getByText(ignoreRTL('src/b/foo.js'))).toBeInTheDocument();
b69ab31153 expect(
b69ab31154 screen.getByText(ignoreRTL('src/subfolder/another/yet/another/file5.js')),
b69ab31155 ).toBeInTheDocument();
b69ab31156 });
b69ab31157
b69ab31158 it('shows full paths when holding alt', () => {
b69ab31159 expect(screen.queryByText(ignoreRTL('src/b/foo.js'))).not.toBeInTheDocument();
b69ab31160 act(() => {
b69ab31161 userEvent.keyboard('{Alt>}'); // '>' means keep pressed
b69ab31162 });
b69ab31163 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31164 expect(screen.getByText(ignoreRTL('src/file2.js'))).toBeInTheDocument();
b69ab31165 expect(screen.getByText(ignoreRTL('src/a/foo.js'))).toBeInTheDocument();
b69ab31166 expect(screen.getByText(ignoreRTL('src/b/foo.js'))).toBeInTheDocument();
b69ab31167 expect(
b69ab31168 screen.getByText(ignoreRTL('src/subfolder/another/yet/another/file5.js')),
b69ab31169 ).toBeInTheDocument();
b69ab31170 });
b69ab31171 });
b69ab31172
b69ab31173 describe('one-letter-per-directory file paths', () => {
b69ab31174 it('shows one-letter-per-directory file paths', () => {
b69ab31175 openChangedFileStatusPicker();
b69ab31176 act(() => {
b69ab31177 fireEvent.click(screen.getByText('One-letter directories'));
b69ab31178 });
b69ab31179
b69ab31180 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31181 expect(screen.getByText(ignoreRTL('s/file2.js'))).toBeInTheDocument();
b69ab31182 expect(screen.getByText(ignoreRTL('s/a/foo.js'))).toBeInTheDocument();
b69ab31183 expect(screen.getByText(ignoreRTL('s/b/foo.js'))).toBeInTheDocument();
b69ab31184 expect(screen.getByText(ignoreRTL('s/s/file4.js'))).toBeInTheDocument();
b69ab31185 expect(screen.getByText(ignoreRTL('s/s/a/y/a/file5.js'))).toBeInTheDocument();
b69ab31186 });
b69ab31187 });
b69ab31188
b69ab31189 describe('tree', () => {
b69ab31190 beforeEach(() => {
b69ab31191 openChangedFileStatusPicker();
b69ab31192 act(() => {
b69ab31193 fireEvent.click(screen.getByText('Tree'));
b69ab31194 });
b69ab31195 });
b69ab31196
b69ab31197 it('shows non-disambiguated file basenames', () => {
b69ab31198 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31199 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
b69ab31200 expect(screen.getByText(ignoreRTL('file3.js'))).toBeInTheDocument();
b69ab31201 expect(screen.getAllByText(ignoreRTL('foo.js'))).toHaveLength(2);
b69ab31202 expect(screen.getByText(ignoreRTL('file4.js'))).toBeInTheDocument();
b69ab31203 expect(screen.getByText(ignoreRTL('file5.js'))).toBeInTheDocument();
b69ab31204 });
b69ab31205
b69ab31206 it('shows folder names', () => {
b69ab31207 expect(screen.getByText(ignoreRTL('src'))).toBeInTheDocument();
b69ab31208 expect(screen.getByText(ignoreRTL('a'))).toBeInTheDocument();
b69ab31209 expect(screen.getByText(ignoreRTL('b'))).toBeInTheDocument();
b69ab31210 expect(screen.getByText(ignoreRTL('subfolder'))).toBeInTheDocument();
b69ab31211 expect(screen.getByText(ignoreRTL('another/yet/another'))).toBeInTheDocument();
b69ab31212 });
b69ab31213
b69ab31214 it('clicking folder name hides contents', () => {
b69ab31215 act(() => {
b69ab31216 fireEvent.click(screen.getByText('subfolder'));
b69ab31217 });
b69ab31218 expect(screen.queryByText(ignoreRTL('file4.js'))).not.toBeInTheDocument();
b69ab31219 expect(screen.queryByText(ignoreRTL('file5.js'))).not.toBeInTheDocument();
b69ab31220
b69ab31221 expect(screen.getByText(ignoreRTL('file1.js'))).toBeInTheDocument();
b69ab31222 expect(screen.getByText(ignoreRTL('file2.js'))).toBeInTheDocument();
b69ab31223 expect(screen.getByText(ignoreRTL('file3.js'))).toBeInTheDocument();
b69ab31224 });
b69ab31225
b69ab31226 it('clicking folders with the same name do not collapse each other', () => {
b69ab31227 act(() => {
b69ab31228 simulateUncommittedChangedFiles({
b69ab31229 value: [
b69ab31230 {path: 'a/foo/file1.js', status: 'M'},
b69ab31231 {path: 'a/file2.js', status: 'M'},
b69ab31232 {path: 'b/foo/file3.js', status: 'M'},
b69ab31233 {path: 'b/file4.js', status: 'M'},
b69ab31234 ],
b69ab31235 });
b69ab31236 });
b69ab31237 act(() => {
b69ab31238 fireEvent.click(screen.getAllByText('foo')[0]);
b69ab31239 });
b69ab31240 expect(screen.queryByText(ignoreRTL('file1.js'))).not.toBeInTheDocument();
b69ab31241 expect(screen.queryByText(ignoreRTL('file3.js'))).toBeInTheDocument();
b69ab31242 });
b69ab31243 });
b69ab31244
b69ab31245 describe('truncated list of changed files', () => {
b69ab31246 function makeFiles(n: number): Array<RepoRelativePath> {
b69ab31247 return new Array(n).fill(null).map((_, i) => `file${leftPad(i, 3, '0')}.txt`);
b69ab31248 }
b69ab31249
b69ab31250 function withStatus(
b69ab31251 changes: Array<RepoRelativePath>,
b69ab31252 status: ChangedFileStatus,
b69ab31253 ): UncommittedChanges {
b69ab31254 return changes.map(path => ({path, status}));
b69ab31255 }
b69ab31256
b69ab31257 it('only first 500 files are shown', () => {
b69ab31258 act(() => {
b69ab31259 simulateUncommittedChangedFiles({
b69ab31260 value: withStatus(makeFiles(510), 'M'),
b69ab31261 });
b69ab31262 });
b69ab31263 const files = screen.getAllByText(/file\d+\.txt/);
b69ab31264 expect(files).toHaveLength(500);
b69ab31265 });
b69ab31266
b69ab31267 it('banner is shown if some files are hidden', () => {
b69ab31268 act(() => {
b69ab31269 simulateUncommittedChangedFiles({
b69ab31270 value: withStatus(makeFiles(700), 'M'),
b69ab31271 });
b69ab31272 });
b69ab31273 expect(screen.getByText('Showing first 500 files out of 700 total')).toBeInTheDocument();
b69ab31274 });
b69ab31275
b69ab31276 it('if more than 500 files are provided, there are page navigation buttons', () => {
b69ab31277 act(() => {
b69ab31278 simulateUncommittedChangedFiles({
b69ab31279 value: withStatus(makeFiles(510), 'M'),
b69ab31280 });
b69ab31281 });
b69ab31282 expect(screen.getByTestId('changed-files-next-page')).toBeInTheDocument();
b69ab31283 expect(screen.getByTestId('changed-files-previous-page')).toBeInTheDocument();
b69ab31284 expect(screen.getByTestId('changed-files-previous-page')).toBeDisabled();
b69ab31285 expect(screen.getByText('Showing first 500 files out of 510 total')).toBeInTheDocument();
b69ab31286 });
b69ab31287
b69ab31288 it('can click buttons to navigate pages', () => {
b69ab31289 act(() => {
b69ab31290 simulateUncommittedChangedFiles({
b69ab31291 value: withStatus(makeFiles(1010), 'M'),
b69ab31292 });
b69ab31293 });
b69ab31294 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31295 expect(screen.getByText('Showing files 501 – 1000 out of 1010 total')).toBeInTheDocument();
b69ab31296 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31297 expect(screen.getByText('Showing files 1001 – 1010 out of 1010 total')).toBeInTheDocument();
b69ab31298
b69ab31299 expect(screen.getByTestId('changed-files-next-page')).toBeDisabled();
b69ab31300
b69ab31301 fireEvent.click(screen.getByTestId('changed-files-previous-page'));
b69ab31302 expect(screen.getByText('Showing files 501 – 1000 out of 1010 total')).toBeInTheDocument();
b69ab31303 fireEvent.click(screen.getByTestId('changed-files-previous-page'));
b69ab31304 expect(screen.getByText('Showing first 500 files out of 1010 total')).toBeInTheDocument();
b69ab31305 });
b69ab31306
b69ab31307 it("if more than 500 files exist, but only 500 are provided, don't show pagination buttons", () => {
b69ab31308 act(() => {
b69ab31309 simulateUncommittedChangedFiles({
b69ab31310 value: [],
b69ab31311 });
b69ab31312 simulateCommits({
b69ab31313 value: [
b69ab31314 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31315 COMMIT('a', 'Commit', '1', {
b69ab31316 isDot: true,
b69ab31317 filePathsSample: makeFiles(500),
b69ab31318 totalFileCount: 1010,
b69ab31319 }),
b69ab31320 ],
b69ab31321 });
b69ab31322 CommitInfoTestUtils.openCommitInfoSidebar();
b69ab31323 });
b69ab31324
b69ab31325 const changedFiles = CommitInfoTestUtils.withinCommitInfo().getByTestId('changed-files');
b69ab31326 expect(changedFiles).toBeInTheDocument();
b69ab31327
b69ab31328 // banner shows truncation
b69ab31329 expect(
b69ab31330 CommitInfoTestUtils.withinCommitInfo().getByText(
b69ab31331 'Showing first 500 files out of 1010 total',
b69ab31332 ),
b69ab31333 ).toBeInTheDocument();
b69ab31334
b69ab31335 // but no pagination buttons, since we only provide first 25 anyway
b69ab31336 expect(
b69ab31337 CommitInfoTestUtils.withinCommitInfo().queryByTestId('changed-files-next-page'),
b69ab31338 ).not.toBeInTheDocument();
b69ab31339 expect(
b69ab31340 CommitInfoTestUtils.withinCommitInfo().queryByTestId('changed-files-previous-page'),
b69ab31341 ).not.toBeInTheDocument();
b69ab31342 });
b69ab31343
b69ab31344 it('if the number of files changes, restrict the page number to fit', () => {
b69ab31345 act(() => {
b69ab31346 simulateUncommittedChangedFiles({
b69ab31347 value: withStatus(makeFiles(2020), 'M'),
b69ab31348 });
b69ab31349 });
b69ab31350 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31351 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31352 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31353 fireEvent.click(screen.getByTestId('changed-files-next-page'));
b69ab31354 expect(screen.getByText('Showing files 2001 – 2020 out of 2020 total')).toBeInTheDocument();
b69ab31355
b69ab31356 // now some file changes are removed (e.g. discarded)
b69ab31357 act(() => {
b69ab31358 simulateUncommittedChangedFiles({
b69ab31359 value: withStatus(makeFiles(700), 'M'),
b69ab31360 });
b69ab31361 });
b69ab31362
b69ab31363 // ranges are remapped
b69ab31364 expect(screen.getByText('Showing files 501 – 700 out of 700 total')).toBeInTheDocument();
b69ab31365 });
b69ab31366 });
b69ab31367});