addons/isl/src/__tests__/CommitTreeList.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 {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
b69ab319import App from '../App';
b69ab3110import {CommitInfoTestUtils, ignoreRTL} from '../testQueries';
b69ab3111import {
b69ab3112 closeCommitInfoSidebar,
b69ab3113 COMMIT,
b69ab3114 commitInfoIsOpen,
b69ab3115 expectMessageSentToServer,
b69ab3116 openCommitInfoSidebar,
b69ab3117 resetTestMessages,
b69ab3118 simulateCommits,
b69ab3119 simulateRepoConnected,
b69ab3120 simulateUncommittedChangedFiles,
b69ab3121} from '../testUtils';
b69ab3122import {CommandRunner} from '../types';
b69ab3123
b69ab3124describe('CommitTreeList', () => {
b69ab3125 beforeEach(() => {
b69ab3126 resetTestMessages();
b69ab3127 });
b69ab3128
b69ab3129 it('shows loading spinner on mount', () => {
b69ab3130 render(<App />);
b69ab3131
b69ab3132 expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
b69ab3133 });
b69ab3134
b69ab3135 it('shows bug button even on error', () => {
b69ab3136 render(<App />);
b69ab3137
b69ab3138 act(() => {
b69ab3139 simulateCommits({
b69ab3140 error: new Error('invalid certificate'),
b69ab3141 });
b69ab3142 });
b69ab3143
b69ab3144 expect(screen.getByTestId('bug-button')).toBeInTheDocument();
b69ab3145 expect(screen.getByText('invalid certificate')).toBeInTheDocument();
b69ab3146 });
b69ab3147
b69ab3148 describe('after commits loaded', () => {
b69ab3149 beforeEach(() => {
b69ab3150 render(<App />);
b69ab3151 act(() => {
b69ab3152 simulateRepoConnected();
b69ab3153 closeCommitInfoSidebar();
b69ab3154 expectMessageSentToServer({
b69ab3155 type: 'subscribe',
b69ab3156 kind: 'smartlogCommits',
b69ab3157 subscriptionID: expect.anything(),
b69ab3158 });
b69ab3159 simulateCommits({
b69ab3160 value: [
b69ab3161 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab3162 COMMIT('a', 'My Commit', '1'),
b69ab3163 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
b69ab3164 ],
b69ab3165 });
b69ab3166 });
b69ab3167 });
b69ab3168
b69ab3169 it('renders commits', () => {
b69ab3170 expect(screen.getByText('My Commit')).toBeInTheDocument();
b69ab3171 expect(screen.getByText('Another Commit')).toBeInTheDocument();
b69ab3172 expect(screen.queryByText('some public base')).not.toBeInTheDocument();
b69ab3173 });
b69ab3174
b69ab3175 it('renders exactly one head', () => {
b69ab3176 expect(screen.getByText('You are here')).toBeInTheDocument();
b69ab3177 });
b69ab3178
b69ab3179 describe('uncommitted changes', () => {
b69ab3180 beforeEach(() => {
b69ab3181 act(() => {
b69ab3182 expectMessageSentToServer({
b69ab3183 type: 'subscribe',
b69ab3184 kind: 'uncommittedChanges',
b69ab3185 subscriptionID: expect.anything(),
b69ab3186 });
b69ab3187 simulateUncommittedChangedFiles({
b69ab3188 value: [
b69ab3189 {path: 'src/file.js', status: 'M'},
b69ab3190 {path: 'src/file_add.js', status: 'A'},
b69ab3191 {path: 'src/file_removed.js', status: 'R'},
b69ab3192 {path: 'src/file_untracked.js', status: '?'},
b69ab3193 {path: 'src/file_missing.js', status: '!'},
b69ab3194 ],
b69ab3195 });
b69ab3196 });
b69ab3197 });
b69ab3198
b69ab3199 it('renders uncommitted changes', () => {
b69ab31100 expect(screen.getByText(ignoreRTL('file.js'), {exact: false})).toBeInTheDocument();
b69ab31101 expect(screen.getByText(ignoreRTL('file_add.js'), {exact: false})).toBeInTheDocument();
b69ab31102 expect(screen.getByText(ignoreRTL('file_removed.js'), {exact: false})).toBeInTheDocument();
b69ab31103 expect(
b69ab31104 screen.getByText(ignoreRTL('file_untracked.js'), {exact: false}),
b69ab31105 ).toBeInTheDocument();
b69ab31106 expect(screen.getByText(ignoreRTL('file_missing.js'), {exact: false})).toBeInTheDocument();
b69ab31107 });
b69ab31108
b69ab31109 it('shows quick commit button', () => {
b69ab31110 expect(screen.getByText('Commit')).toBeInTheDocument();
b69ab31111 });
b69ab31112 it('shows quick amend button only on non-public commits', () => {
b69ab31113 expect(screen.getByText('Amend')).toBeInTheDocument();
b69ab31114 // checkout a public commit
b69ab31115 act(() => {
b69ab31116 simulateCommits({
b69ab31117 value: [
b69ab31118 COMMIT('1', 'some public base', '0', {phase: 'public', isDot: true}),
b69ab31119 COMMIT('a', 'My Commit', '1', {successorInfo: {hash: 'a2', type: 'land'}}),
b69ab31120 COMMIT('b', 'Another Commit', 'a'),
b69ab31121 ],
b69ab31122 });
b69ab31123 });
b69ab31124 // no longer see quick amend
b69ab31125 expect(screen.queryByText('Amend')).not.toBeInTheDocument();
b69ab31126 });
b69ab31127
b69ab31128 it('shows file actions', () => {
b69ab31129 const fileActions = screen.getAllByTestId('file-actions');
b69ab31130 expect(fileActions).toHaveLength(5); // 5 files
b69ab31131 const revertButtons = screen.getAllByTestId('file-revert-button');
b69ab31132 expect(revertButtons).toHaveLength(3); // modified, removed, missing files can be reverted
b69ab31133 });
b69ab31134
b69ab31135 it('runs revert command when clicking revert button', async () => {
b69ab31136 const revertButtons = screen.getAllByTestId('file-revert-button');
b69ab31137 jest.spyOn(window, 'confirm').mockImplementation(() => true);
b69ab31138 act(() => {
b69ab31139 fireEvent.click(revertButtons[0]);
b69ab31140 });
b69ab31141 await waitFor(() => {
b69ab31142 expect(window.confirm).toHaveBeenCalled();
b69ab31143 expectMessageSentToServer({
b69ab31144 type: 'runOperation',
b69ab31145 operation: {
b69ab31146 args: ['revert', {type: 'repo-relative-file-list', paths: ['src/file.js']}],
b69ab31147 id: expect.anything(),
b69ab31148 runner: CommandRunner.Sapling,
b69ab31149 trackEventName: 'RevertOperation',
b69ab31150 },
b69ab31151 });
b69ab31152 });
b69ab31153 });
b69ab31154
b69ab31155 describe('addremove', () => {
b69ab31156 it('hides addremove if all files tracked', () => {
b69ab31157 act(() => {
b69ab31158 simulateUncommittedChangedFiles({
b69ab31159 value: [
b69ab31160 {path: 'src/file.js', status: 'M'},
b69ab31161 {path: 'src/file_add.js', status: 'A'},
b69ab31162 {path: 'src/file_removed.js', status: 'R'},
b69ab31163 ],
b69ab31164 });
b69ab31165 });
b69ab31166 expect(screen.queryByTestId('addremove-button')).not.toBeInTheDocument();
b69ab31167
b69ab31168 act(() => {
b69ab31169 simulateUncommittedChangedFiles({
b69ab31170 value: [
b69ab31171 {path: 'src/file.js', status: 'M'},
b69ab31172 {path: 'src/file_add.js', status: 'A'},
b69ab31173 {path: 'src/file_removed.js', status: 'R'},
b69ab31174 {path: 'src/file_untracked.js', status: '?'},
b69ab31175 {path: 'src/file_missing.js', status: '!'},
b69ab31176 ],
b69ab31177 });
b69ab31178 });
b69ab31179 expect(screen.queryByTestId('addremove-button')).toBeInTheDocument();
b69ab31180 });
b69ab31181
b69ab31182 it('runs addremove', async () => {
b69ab31183 const addremove = screen.getByTestId('addremove-button');
b69ab31184 act(() => {
b69ab31185 fireEvent.click(addremove);
b69ab31186 });
b69ab31187 await waitFor(() => {
b69ab31188 expectMessageSentToServer({
b69ab31189 type: 'runOperation',
b69ab31190 operation: {
b69ab31191 args: ['addremove'],
b69ab31192 id: expect.anything(),
b69ab31193 runner: CommandRunner.Sapling,
b69ab31194 trackEventName: 'AddRemoveOperation',
b69ab31195 },
b69ab31196 });
b69ab31197 });
b69ab31198 });
b69ab31199
b69ab31200 it('optimistically updates file statuses while addremove is running', async () => {
b69ab31201 const addremove = screen.getByTestId('addremove-button');
b69ab31202 act(() => {
b69ab31203 fireEvent.click(addremove);
b69ab31204 });
b69ab31205 await waitFor(() => {
b69ab31206 expectMessageSentToServer({
b69ab31207 type: 'runOperation',
b69ab31208 operation: {
b69ab31209 args: ['addremove'],
b69ab31210 id: expect.anything(),
b69ab31211 runner: CommandRunner.Sapling,
b69ab31212 trackEventName: 'AddRemoveOperation',
b69ab31213 },
b69ab31214 });
b69ab31215 });
b69ab31216
b69ab31217 expect(
b69ab31218 document.querySelectorAll('.changed-files .changed-file.file-ignored'),
b69ab31219 ).toHaveLength(0);
b69ab31220 });
b69ab31221
b69ab31222 it('runs addremove only on selected files that are untracked', async () => {
b69ab31223 const ignoredFileCheckboxes = document.querySelectorAll(
b69ab31224 '.changed-files .changed-file.file-ignored input[type="checkbox"]',
b69ab31225 );
b69ab31226 const missingFileCheckboxes = document.querySelectorAll(
b69ab31227 '.changed-files .changed-file.file-missing input[type="checkbox"]',
b69ab31228 );
b69ab31229 expect(ignoredFileCheckboxes).toHaveLength(1); // file_untracked.js
b69ab31230 expect(missingFileCheckboxes).toHaveLength(1); // file_missing.js
b69ab31231 act(() => {
b69ab31232 fireEvent.click(missingFileCheckboxes[0]);
b69ab31233 });
b69ab31234
b69ab31235 const addremove = screen.getByTestId('addremove-button');
b69ab31236 act(() => {
b69ab31237 fireEvent.click(addremove);
b69ab31238 });
b69ab31239 await waitFor(() => {
b69ab31240 expectMessageSentToServer({
b69ab31241 type: 'runOperation',
b69ab31242 operation: {
b69ab31243 // note: although src/file.js & others are selected, they aren't passed to addremove as they aren't untracked
b69ab31244 args: ['addremove', {path: 'src/file_untracked.js', type: 'repo-relative-file'}],
b69ab31245 id: expect.anything(),
b69ab31246 runner: CommandRunner.Sapling,
b69ab31247 trackEventName: 'AddRemoveOperation',
b69ab31248 },
b69ab31249 });
b69ab31250 });
b69ab31251 });
b69ab31252 });
b69ab31253 });
b69ab31254
b69ab31255 it('shows log errors', () => {
b69ab31256 act(() => {
b69ab31257 simulateCommits({
b69ab31258 error: new Error('error running log'),
b69ab31259 });
b69ab31260 });
b69ab31261 expect(screen.getByText('Failed to fetch commits')).toBeInTheDocument();
b69ab31262 expect(screen.getByText('error running log')).toBeInTheDocument();
b69ab31263
b69ab31264 // we should still have commits from the last successful fetch
b69ab31265 expect(screen.getByText('My Commit')).toBeInTheDocument();
b69ab31266 expect(screen.getByText('Another Commit')).toBeInTheDocument();
b69ab31267 expect(screen.queryByText('some public base')).not.toBeInTheDocument();
b69ab31268 });
b69ab31269
b69ab31270 it('shows status errors', () => {
b69ab31271 act(() => {
b69ab31272 simulateUncommittedChangedFiles({
b69ab31273 error: new Error('error running status'),
b69ab31274 });
b69ab31275 });
b69ab31276 expect(screen.getByText('Failed to fetch Uncommitted Changes')).toBeInTheDocument();
b69ab31277 expect(screen.getByText('error running status')).toBeInTheDocument();
b69ab31278 });
b69ab31279
b69ab31280 it('shows successor info', () => {
b69ab31281 act(() => {
b69ab31282 simulateCommits({
b69ab31283 value: [
b69ab31284 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31285 COMMIT('a', 'My Commit', '1', {successorInfo: {hash: 'a2', type: 'land'}}),
b69ab31286 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
b69ab31287 ],
b69ab31288 });
b69ab31289 });
b69ab31290 expect(screen.getByText('Landed as a newer commit', {exact: false})).toBeInTheDocument();
b69ab31291 });
b69ab31292
b69ab31293 it('shows button to open sidebar', () => {
b69ab31294 act(() => {
b69ab31295 simulateCommits({
b69ab31296 value: [
b69ab31297 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31298 COMMIT('a', 'Commit A', '1', {isDot: true}),
b69ab31299 COMMIT('b', 'Commit B', '1'),
b69ab31300 ],
b69ab31301 });
b69ab31302 });
b69ab31303 expect(commitInfoIsOpen()).toBeFalsy();
b69ab31304
b69ab31305 // doesn't appear for public commits
b69ab31306 expect(
b69ab31307 within(screen.getByTestId('commit-1')).queryByTestId('open-commit-info-button'),
b69ab31308 ).not.toBeInTheDocument();
b69ab31309
b69ab31310 const openButton = within(screen.getByTestId('commit-b')).getByTestId(
b69ab31311 'open-commit-info-button',
b69ab31312 );
b69ab31313 expect(openButton).toBeInTheDocument();
b69ab31314 // screen reader accessible
b69ab31315 expect(screen.getByLabelText('Open commit "Commit B"')).toBeInTheDocument();
b69ab31316 fireEvent.click(openButton);
b69ab31317 expect(commitInfoIsOpen()).toBeTruthy();
b69ab31318 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
b69ab31319 });
b69ab31320
b69ab31321 it('sets to amend mode when clicking open button', () => {
b69ab31322 act(() => {
b69ab31323 simulateCommits({
b69ab31324 value: [
b69ab31325 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31326 COMMIT('a', 'Commit A', '1', {isDot: true}),
b69ab31327 COMMIT('b', 'Commit B', '1'),
b69ab31328 ],
b69ab31329 });
b69ab31330 });
b69ab31331 act(() => {
b69ab31332 openCommitInfoSidebar();
b69ab31333 });
b69ab31334 CommitInfoTestUtils.clickCommitMode();
b69ab31335
b69ab31336 const openButton = within(screen.getByTestId('commit-b')).getByTestId(
b69ab31337 'open-commit-info-button',
b69ab31338 );
b69ab31339 expect(openButton).toBeInTheDocument();
b69ab31340 fireEvent.click(openButton);
b69ab31341 expect(commitInfoIsOpen()).toBeTruthy();
b69ab31342 expect(CommitInfoTestUtils.withinCommitInfo().getByText('Commit B')).toBeInTheDocument();
b69ab31343 });
b69ab31344 });
b69ab31345
b69ab31346 describe('render dag subset', () => {
b69ab31347 describe('irrelevant cwd stacks', () => {
b69ab31348 beforeEach(() => {
b69ab31349 render(<App />);
b69ab31350 act(() => {
b69ab31351 // Set repo root to "/repo" and cwd to "/repo/www"
b69ab31352 simulateRepoConnected('/repo', '/repo/www');
b69ab31353 closeCommitInfoSidebar();
b69ab31354 expectMessageSentToServer({
b69ab31355 type: 'subscribe',
b69ab31356 kind: 'smartlogCommits',
b69ab31357 subscriptionID: expect.anything(),
b69ab31358 });
b69ab31359 simulateCommits({
b69ab31360 value: [
b69ab31361 COMMIT('1', 'Public base', '0', {phase: 'public'}),
b69ab31362 // Commits with different maxCommonPathPrefix values
b69ab31363 COMMIT('a', 'Commit in www', '1', {maxCommonPathPrefix: 'www/'}),
b69ab31364 COMMIT('b', 'Commit in addons', '1', {maxCommonPathPrefix: 'addons/'}),
b69ab31365 COMMIT('c', 'Commit in root', '1', {maxCommonPathPrefix: ''}),
b69ab31366 COMMIT('d', 'Commit in www/js', '1', {
b69ab31367 maxCommonPathPrefix: 'www/js/',
b69ab31368 isDot: true,
b69ab31369 }),
b69ab31370 ],
b69ab31371 });
b69ab31372 });
b69ab31373 });
b69ab31374
b69ab31375 it('shows irrelevant commits by default', () => {
b69ab31376 // By default, hideIrrelevantCwdStacks is false, so all commits should be visible
b69ab31377 expect(screen.queryByText('Commit in www')).toBeInTheDocument();
b69ab31378 expect(screen.queryByText('Commit in addons')).toBeInTheDocument();
b69ab31379 expect(screen.queryByText('Commit in root')).toBeInTheDocument();
b69ab31380 expect(screen.queryByText('Commit in www/js')).toBeInTheDocument();
b69ab31381
b69ab31382 // But the addons/ commit should be marked as irrelevant
b69ab31383 expect(document.body.querySelectorAll('.commit.irrelevant')).toHaveLength(1);
b69ab31384 });
b69ab31385
b69ab31386 it('can hide irrelevant commits when enabled', async () => {
b69ab31387 fireEvent.click(screen.getByTestId('settings-gear-button'));
b69ab31388
b69ab31389 const settingsDropdown = screen.getByTestId('settings-dropdown');
b69ab31390 expect(settingsDropdown).toBeInTheDocument();
b69ab31391 const cwdIrrelevantDropdown =
b69ab31392 within(settingsDropdown).getByTestId('cwd-irrelevant-commits');
b69ab31393 expect(cwdIrrelevantDropdown).toBeInTheDocument();
b69ab31394
b69ab31395 // Change the dropdown value to 'hide'
b69ab31396 fireEvent.change(cwdIrrelevantDropdown, {target: {value: 'hide'}});
b69ab31397
b69ab31398 expect(screen.queryByText('Commit in www')).toBeInTheDocument();
b69ab31399 expect(screen.queryByText('Commit in root')).toBeInTheDocument();
b69ab31400 expect(screen.queryByText('Commit in www/js')).toBeInTheDocument();
b69ab31401
b69ab31402 expect(screen.queryByText('Commit in addons')).not.toBeInTheDocument();
b69ab31403 });
b69ab31404 });
b69ab31405
b69ab31406 describe('obsolete stacks', () => {
b69ab31407 beforeEach(() => {
b69ab31408 render(<App />);
b69ab31409 act(() => {
b69ab31410 simulateRepoConnected();
b69ab31411 closeCommitInfoSidebar();
b69ab31412 expectMessageSentToServer({
b69ab31413 type: 'subscribe',
b69ab31414 kind: 'smartlogCommits',
b69ab31415 subscriptionID: expect.anything(),
b69ab31416 });
b69ab31417 simulateCommits({
b69ab31418 value: [
b69ab31419 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31420 COMMIT('a', 'Commit A', '1', {successorInfo: {hash: 'a2', type: 'rebase'}}),
b69ab31421 COMMIT('b', 'Commit B', 'a', {successorInfo: {hash: 'b2', type: 'rebase'}}),
b69ab31422 COMMIT('c', 'Commit C', 'b', {successorInfo: {hash: 'c2', type: 'rebase'}}),
b69ab31423 COMMIT('d', 'Commit D', 'c', {successorInfo: {hash: 'd2', type: 'rebase'}}),
b69ab31424 COMMIT('e', 'Commit E', 'd', {isDot: true}),
b69ab31425 ],
b69ab31426 });
b69ab31427 });
b69ab31428 });
b69ab31429 it('hides obsolete stacks by default', () => {
b69ab31430 expect(screen.queryByText('Commit A')).toBeInTheDocument();
b69ab31431 expect(screen.queryByText('Commit B')).not.toBeInTheDocument();
b69ab31432 expect(screen.queryByText('Commit C')).not.toBeInTheDocument();
b69ab31433 expect(screen.queryByText('Commit D')).toBeInTheDocument();
b69ab31434 expect(screen.queryByText('Commit E')).toBeInTheDocument();
b69ab31435 });
b69ab31436
b69ab31437 it('can configure to not hide obsolete stacks', () => {
b69ab31438 fireEvent.click(screen.getByTestId('settings-gear-button'));
b69ab31439 fireEvent.click(screen.getByTestId('condense-obsolete-stacks'));
b69ab31440 expect(screen.queryByText('Commit A')).toBeInTheDocument();
b69ab31441 expect(screen.queryByText('Commit B')).toBeInTheDocument();
b69ab31442 expect(screen.queryByText('Commit C')).toBeInTheDocument();
b69ab31443 expect(screen.queryByText('Commit D')).toBeInTheDocument();
b69ab31444 expect(screen.queryByText('Commit E')).toBeInTheDocument();
b69ab31445 });
b69ab31446 });
b69ab31447 });
b69ab31448});