| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {Hash} from '../../types'; |
| b69ab31 | | | 9 | |
| b69ab31 | | | 10 | import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react'; |
| b69ab31 | | | 11 | import App from '../../App'; |
| b69ab31 | | | 12 | import {Dag, DagCommitInfo} from '../../dag/dag'; |
| b69ab31 | | | 13 | import {RebaseOperation} from '../../operations/RebaseOperation'; |
| b69ab31 | | | 14 | import {CommitPreview} from '../../previews'; |
| b69ab31 | | | 15 | import {ignoreRTL} from '../../testQueries'; |
| b69ab31 | | | 16 | import { |
| b69ab31 | | | 17 | closeCommitInfoSidebar, |
| b69ab31 | | | 18 | COMMIT, |
| b69ab31 | | | 19 | dragAndDropCommits, |
| b69ab31 | | | 20 | dragCommits, |
| b69ab31 | | | 21 | dropCommits, |
| b69ab31 | | | 22 | expectMessageNOTSentToServer, |
| b69ab31 | | | 23 | expectMessageSentToServer, |
| b69ab31 | | | 24 | resetTestMessages, |
| b69ab31 | | | 25 | scanForkedBranchHashes, |
| b69ab31 | | | 26 | simulateCommits, |
| b69ab31 | | | 27 | simulateUncommittedChangedFiles, |
| b69ab31 | | | 28 | TEST_COMMIT_HISTORY, |
| b69ab31 | | | 29 | } from '../../testUtils'; |
| b69ab31 | | | 30 | import {CommandRunner, succeedableRevset} from '../../types'; |
| b69ab31 | | | 31 | |
| b69ab31 | | | 32 | /*eslint-disable @typescript-eslint/no-non-null-assertion */ |
| b69ab31 | | | 33 | |
| b69ab31 | | | 34 | describe('rebase operation', () => { |
| b69ab31 | | | 35 | // Extend with an obsoleted commit. |
| b69ab31 | | | 36 | const testHistory = TEST_COMMIT_HISTORY.concat([ |
| b69ab31 | | | 37 | COMMIT('ff1', 'Commit FF1 (obsoleted)', 'z', {successorInfo: {hash: 'ff2', type: 'amend'}}), |
| b69ab31 | | | 38 | COMMIT('ff2', 'Commit FF2', 'z'), |
| b69ab31 | | | 39 | ]); |
| b69ab31 | | | 40 | |
| b69ab31 | | | 41 | beforeEach(() => { |
| b69ab31 | | | 42 | jest.useFakeTimers(); |
| b69ab31 | | | 43 | resetTestMessages(); |
| b69ab31 | | | 44 | render(<App />); |
| b69ab31 | | | 45 | act(() => { |
| b69ab31 | | | 46 | closeCommitInfoSidebar(); |
| b69ab31 | | | 47 | expectMessageSentToServer({ |
| b69ab31 | | | 48 | type: 'subscribe', |
| b69ab31 | | | 49 | kind: 'smartlogCommits', |
| b69ab31 | | | 50 | subscriptionID: expect.anything(), |
| b69ab31 | | | 51 | }); |
| b69ab31 | | | 52 | simulateCommits({ |
| b69ab31 | | | 53 | value: testHistory, |
| b69ab31 | | | 54 | }); |
| b69ab31 | | | 55 | }); |
| b69ab31 | | | 56 | }); |
| b69ab31 | | | 57 | |
| b69ab31 | | | 58 | afterEach(() => { |
| b69ab31 | | | 59 | jest.useRealTimers(); |
| b69ab31 | | | 60 | }); |
| b69ab31 | | | 61 | |
| b69ab31 | | | 62 | const getCommitWithPreview = (hash: Hash, preview: CommitPreview): HTMLElement => { |
| b69ab31 | | | 63 | const previewOfCommit = screen |
| b69ab31 | | | 64 | .queryAllByTestId(`commit-${hash}`) |
| b69ab31 | | | 65 | .map(commit => within(commit).queryByTestId('draggable-commit')) |
| b69ab31 | | | 66 | .find(commit => commit?.classList.contains(`commit-preview-${preview}`)); |
| b69ab31 | | | 67 | expect(previewOfCommit).toBeInTheDocument(); |
| b69ab31 | | | 68 | return previewOfCommit!; |
| b69ab31 | | | 69 | }; |
| b69ab31 | | | 70 | |
| b69ab31 | | | 71 | it('previews a rebase on drag & drop onto a public commit', () => { |
| b69ab31 | | | 72 | expect(screen.getAllByText('Commit D')).toHaveLength(1); |
| b69ab31 | | | 73 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 74 | |
| b69ab31 | | | 75 | // original commit AND previewed commit are now in the document |
| b69ab31 | | | 76 | expect(screen.getAllByText('Commit D')).toHaveLength(2); |
| b69ab31 | | | 77 | // also includes descendants |
| b69ab31 | | | 78 | expect(screen.getAllByText('Commit E')).toHaveLength(2); |
| b69ab31 | | | 79 | |
| b69ab31 | | | 80 | // one of them is a rebase preview |
| b69ab31 | | | 81 | expect( |
| b69ab31 | | | 82 | screen |
| b69ab31 | | | 83 | .queryAllByTestId('commit-d') |
| b69ab31 | | | 84 | .some(commit => commit.querySelector('.commit-preview-rebase-root')), |
| b69ab31 | | | 85 | ).toEqual(true); |
| b69ab31 | | | 86 | }); |
| b69ab31 | | | 87 | |
| b69ab31 | | | 88 | it('sets all descendants as the right preview type', () => { |
| b69ab31 | | | 89 | expect(screen.getAllByText('Commit D')).toHaveLength(1); |
| b69ab31 | | | 90 | dragAndDropCommits('a', '2'); |
| b69ab31 | | | 91 | |
| b69ab31 | | | 92 | expect(document.querySelectorAll('.commit-preview-rebase-old')).toHaveLength(5); |
| b69ab31 | | | 93 | expect(document.querySelectorAll('.commit-preview-rebase-root')).toHaveLength(1); |
| b69ab31 | | | 94 | expect(document.querySelectorAll('.commit-preview-rebase-descendant')).toHaveLength(4); |
| b69ab31 | | | 95 | }); |
| b69ab31 | | | 96 | |
| b69ab31 | | | 97 | it('previews onto correct branch', () => { |
| b69ab31 | | | 98 | expect(screen.getAllByText('Commit D')).toHaveLength(1); |
| b69ab31 | | | 99 | dragAndDropCommits('d', 'x'); |
| b69ab31 | | | 100 | expect(scanForkedBranchHashes('x')).toEqual(['d', 'e']); |
| b69ab31 | | | 101 | }); |
| b69ab31 | | | 102 | |
| b69ab31 | | | 103 | it('cannot drag public commits', () => { |
| b69ab31 | | | 104 | dragAndDropCommits('1', '2'); |
| b69ab31 | | | 105 | |
| b69ab31 | | | 106 | // only see one copy of commit 1 |
| b69ab31 | | | 107 | expect(screen.queryAllByTestId('commit-1')).toHaveLength(1); |
| b69ab31 | | | 108 | }); |
| b69ab31 | | | 109 | |
| b69ab31 | | | 110 | it('runs rebase operation', async () => { |
| b69ab31 | | | 111 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 112 | |
| b69ab31 | | | 113 | const runRebaseButton = screen.getByText('Run Rebase'); |
| b69ab31 | | | 114 | expect(runRebaseButton).toBeInTheDocument(); |
| b69ab31 | | | 115 | |
| b69ab31 | | | 116 | fireEvent.click(runRebaseButton); |
| b69ab31 | | | 117 | await waitFor(() => { |
| b69ab31 | | | 118 | expectMessageSentToServer({ |
| b69ab31 | | | 119 | type: 'runOperation', |
| b69ab31 | | | 120 | operation: { |
| b69ab31 | | | 121 | args: ['rebase', '-s', succeedableRevset('d'), '-d', succeedableRevset('remote/master')], |
| b69ab31 | | | 122 | id: expect.anything(), |
| b69ab31 | | | 123 | runner: CommandRunner.Sapling, |
| b69ab31 | | | 124 | trackEventName: 'RebaseOperation', |
| b69ab31 | | | 125 | }, |
| b69ab31 | | | 126 | }); |
| b69ab31 | | | 127 | }); |
| b69ab31 | | | 128 | }); |
| b69ab31 | | | 129 | |
| b69ab31 | | | 130 | it('shows optimistic preview of rebase', async () => { |
| b69ab31 | | | 131 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 132 | |
| b69ab31 | | | 133 | fireEvent.click(screen.getByText('Run Rebase')); |
| b69ab31 | | | 134 | |
| b69ab31 | | | 135 | // original commit is hidden, we only see optimistic commit |
| b69ab31 | | | 136 | expect(screen.queryAllByTestId('commit-d')).toHaveLength(1); |
| b69ab31 | | | 137 | // also includes descendants |
| b69ab31 | | | 138 | expect(screen.queryAllByTestId('commit-e')).toHaveLength(1); |
| b69ab31 | | | 139 | |
| b69ab31 | | | 140 | await waitFor(() => { |
| b69ab31 | | | 141 | expect(screen.getByText('rebasing...')).toBeInTheDocument(); |
| b69ab31 | | | 142 | }); |
| b69ab31 | | | 143 | |
| b69ab31 | | | 144 | expect( |
| b69ab31 | | | 145 | screen.queryByTestId('commit-d')?.querySelector('.commit-preview-rebase-optimistic-root'), |
| b69ab31 | | | 146 | ).toBeInTheDocument(); |
| b69ab31 | | | 147 | }); |
| b69ab31 | | | 148 | |
| b69ab31 | | | 149 | it('allows re-dragging a previously dragged commit back onto the same parent', async () => { |
| b69ab31 | | | 150 | // Drag and drop 'e' normally onto 'c' |
| b69ab31 | | | 151 | dragCommits('e', 'd'); |
| b69ab31 | | | 152 | expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); // dragging on original parent is noop |
| b69ab31 | | | 153 | dragAndDropCommits('e', 'c'); |
| b69ab31 | | | 154 | expect(screen.getByText('Run Rebase')).toBeInTheDocument(); |
| b69ab31 | | | 155 | |
| b69ab31 | | | 156 | // Drag the previously preview'd 'e' from 'c' onto 'b', without dropping |
| b69ab31 | | | 157 | dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'b'); |
| b69ab31 | | | 158 | // Keep dragging the previously preview'd 'e' from 'b' back to 'c' |
| b69ab31 | | | 159 | dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c'); |
| b69ab31 | | | 160 | // Finally, drop on 'c'. This should work, even though the preview'd 'e' started on top of 'c', so it's the "parent" |
| b69ab31 | | | 161 | dropCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c'); |
| b69ab31 | | | 162 | |
| b69ab31 | | | 163 | fireEvent.click(screen.getByText('Run Rebase')); |
| b69ab31 | | | 164 | await waitFor(() => { |
| b69ab31 | | | 165 | expectMessageSentToServer({ |
| b69ab31 | | | 166 | type: 'runOperation', |
| b69ab31 | | | 167 | operation: { |
| b69ab31 | | | 168 | args: ['rebase', '-s', succeedableRevset('e'), '-d', succeedableRevset('c')], |
| b69ab31 | | | 169 | id: expect.anything(), |
| b69ab31 | | | 170 | runner: CommandRunner.Sapling, |
| b69ab31 | | | 171 | trackEventName: 'RebaseOperation', |
| b69ab31 | | | 172 | }, |
| b69ab31 | | | 173 | }); |
| b69ab31 | | | 174 | }); |
| b69ab31 | | | 175 | }); |
| b69ab31 | | | 176 | |
| b69ab31 | | | 177 | it('cancel cancels the preview', () => { |
| b69ab31 | | | 178 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 179 | |
| b69ab31 | | | 180 | const cancelButton = screen.getByText('Cancel'); |
| b69ab31 | | | 181 | expect(cancelButton).toBeInTheDocument(); |
| b69ab31 | | | 182 | |
| b69ab31 | | | 183 | act(() => { |
| b69ab31 | | | 184 | fireEvent.click(cancelButton); |
| b69ab31 | | | 185 | }); |
| b69ab31 | | | 186 | |
| b69ab31 | | | 187 | // now the preview doesn't exist |
| b69ab31 | | | 188 | expect(screen.queryAllByTestId('commit-d')).toHaveLength(1); |
| b69ab31 | | | 189 | |
| b69ab31 | | | 190 | // we didn't run any operation |
| b69ab31 | | | 191 | expectMessageNOTSentToServer({ |
| b69ab31 | | | 192 | type: 'runOperation', |
| b69ab31 | | | 193 | operation: expect.anything(), |
| b69ab31 | | | 194 | }); |
| b69ab31 | | | 195 | }); |
| b69ab31 | | | 196 | |
| b69ab31 | | | 197 | it('cannot drag with uncommitted changes', () => { |
| b69ab31 | | | 198 | act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]})); |
| b69ab31 | | | 199 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 200 | |
| b69ab31 | | | 201 | expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); |
| b69ab31 | | | 202 | expect(screen.getByText('Cannot drag to rebase with uncommitted changes.')).toBeInTheDocument(); |
| b69ab31 | | | 203 | }); |
| b69ab31 | | | 204 | |
| b69ab31 | | | 205 | it('cannot drag obsoleted commits', () => { |
| b69ab31 | | | 206 | dragAndDropCommits('ff1', 'e'); |
| b69ab31 | | | 207 | |
| b69ab31 | | | 208 | expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); |
| b69ab31 | | | 209 | expect(screen.getByText('Cannot rebase obsoleted commits.')).toBeInTheDocument(); |
| b69ab31 | | | 210 | }); |
| b69ab31 | | | 211 | |
| b69ab31 | | | 212 | it('can drag if uncommitted changes are optimistically removed', async () => { |
| b69ab31 | | | 213 | act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]})); |
| b69ab31 | | | 214 | act(() => { |
| b69ab31 | | | 215 | fireEvent.click(screen.getByTestId('quick-commit-button')); |
| b69ab31 | | | 216 | }); |
| b69ab31 | | | 217 | await waitFor(() => { |
| b69ab31 | | | 218 | expect(screen.queryByText(ignoreRTL('file1.txt'))).not.toBeInTheDocument(); |
| b69ab31 | | | 219 | }); |
| b69ab31 | | | 220 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 221 | |
| b69ab31 | | | 222 | expect( |
| b69ab31 | | | 223 | screen.queryByText('Cannot drag to rebase with uncommitted changes.'), |
| b69ab31 | | | 224 | ).not.toBeInTheDocument(); |
| b69ab31 | | | 225 | }); |
| b69ab31 | | | 226 | |
| b69ab31 | | | 227 | it('can drag with untracked changes', () => { |
| b69ab31 | | | 228 | act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: '?'}]})); |
| b69ab31 | | | 229 | dragAndDropCommits('d', '2'); |
| b69ab31 | | | 230 | |
| b69ab31 | | | 231 | expect(screen.queryByText('Run Rebase')).toBeInTheDocument(); |
| b69ab31 | | | 232 | }); |
| b69ab31 | | | 233 | |
| b69ab31 | | | 234 | it('handles partial rebase in optimistic dag', () => { |
| b69ab31 | | | 235 | const dag = new Dag().add(TEST_COMMIT_HISTORY.map(c => DagCommitInfo.fromCommitInfo(c))); |
| b69ab31 | | | 236 | |
| b69ab31 | | | 237 | const type = 'succeedable-revset'; |
| b69ab31 | | | 238 | // Rebase a-b-c-d-e to z |
| b69ab31 | | | 239 | const rebaseOp = new RebaseOperation({type, revset: 'a'}, {type, revset: 'z'}); |
| b69ab31 | | | 240 | // Count commits with the given title in a dag. |
| b69ab31 | | | 241 | const count = (dag: Dag, title: string): number => |
| b69ab31 | | | 242 | dag.getBatch([...dag]).filter(c => c.title === title).length; |
| b69ab31 | | | 243 | // Emulate partial rebased: a-b was rebased to z, but not c-d-e |
| b69ab31 | | | 244 | const partialRebased = dag.rebase(['a', 'b'], 'z'); |
| b69ab31 | | | 245 | // There are 2 "Commit A"s in the partially rebased dag - one obsolsted. |
| b69ab31 | | | 246 | expect(count(partialRebased, 'Commit A')).toBe(2); |
| b69ab31 | | | 247 | expect(count(partialRebased, 'Commit B')).toBe(2); |
| b69ab31 | | | 248 | expect(partialRebased.descendants('z').size).toBe(dag.descendants('z').size + 2); |
| b69ab31 | | | 249 | |
| b69ab31 | | | 250 | // Calculate the optimistic dag from a partial rebase state. |
| b69ab31 | | | 251 | const optimisticDag = rebaseOp.optimisticDag(partialRebased); |
| b69ab31 | | | 252 | // should be only 1 "Commit A"s. |
| b69ab31 | | | 253 | expect(count(optimisticDag, 'Commit A')).toBe(1); |
| b69ab31 | | | 254 | expect(count(optimisticDag, 'Commit B')).toBe(1); |
| b69ab31 | | | 255 | expect(count(optimisticDag, 'Commit E')).toBe(1); |
| b69ab31 | | | 256 | // check the Commit A..E branch is completed rebased. |
| b69ab31 | | | 257 | expect(dag.children(dag.parents('a')).size).toBe( |
| b69ab31 | | | 258 | optimisticDag.children(dag.parents('a')).size + 1, |
| b69ab31 | | | 259 | ); |
| b69ab31 | | | 260 | expect(optimisticDag.descendants('z').size).toBe(dag.descendants('a').size + 1); |
| b69ab31 | | | 261 | }); |
| b69ab31 | | | 262 | |
| b69ab31 | | | 263 | describe('stacking optimistic state', () => { |
| b69ab31 | | | 264 | it('cannot drag and drop preview descendants', () => { |
| b69ab31 | | | 265 | dragAndDropCommits('d', 'a'); |
| b69ab31 | | | 266 | expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']); |
| b69ab31 | | | 267 | |
| b69ab31 | | | 268 | dragAndDropCommits(getCommitWithPreview('e', CommitPreview.REBASE_DESCENDANT), 'b'); |
| b69ab31 | | | 269 | |
| b69ab31 | | | 270 | // we still see same commit preview |
| b69ab31 | | | 271 | expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']); |
| b69ab31 | | | 272 | }); |
| b69ab31 | | | 273 | |
| b69ab31 | | | 274 | it('can drag preview root again', () => { |
| b69ab31 | | | 275 | dragAndDropCommits('d', 'a'); |
| b69ab31 | | | 276 | |
| b69ab31 | | | 277 | dragAndDropCommits(getCommitWithPreview('d', CommitPreview.REBASE_ROOT), 'b'); |
| b69ab31 | | | 278 | |
| b69ab31 | | | 279 | // preview is updated to be based on b |
| b69ab31 | | | 280 | expect(scanForkedBranchHashes('b')).toEqual(['d', 'e']); |
| b69ab31 | | | 281 | }); |
| b69ab31 | | | 282 | |
| b69ab31 | | | 283 | it('can preview drag drop while previous rebase running', async () => { |
| b69ab31 | | | 284 | // c |
| b69ab31 | | | 285 | // c | e |
| b69ab31 | | | 286 | // e b |/ |
| b69ab31 | | | 287 | // d | e b |
| b69ab31 | | | 288 | // c -> | d -> | d |
| b69ab31 | | | 289 | // b |/ |/ |
| b69ab31 | | | 290 | // a a a |
| b69ab31 | | | 291 | dragAndDropCommits('d', 'a'); |
| b69ab31 | | | 292 | fireEvent.click(screen.getByText('Run Rebase')); |
| b69ab31 | | | 293 | await waitFor(() => { |
| b69ab31 | | | 294 | expect(screen.getByText('rebasing...')).toBeInTheDocument(); |
| b69ab31 | | | 295 | }); |
| b69ab31 | | | 296 | |
| b69ab31 | | | 297 | dragAndDropCommits( |
| b69ab31 | | | 298 | getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT), |
| b69ab31 | | | 299 | 'b', |
| b69ab31 | | | 300 | ); |
| b69ab31 | | | 301 | |
| b69ab31 | | | 302 | // original optimistic is still there |
| b69ab31 | | | 303 | expect(scanForkedBranchHashes('a')).toContain('d'); |
| b69ab31 | | | 304 | // also previewing new drag |
| b69ab31 | | | 305 | expect(scanForkedBranchHashes('b')).toEqual(['e']); |
| b69ab31 | | | 306 | }); |
| b69ab31 | | | 307 | |
| b69ab31 | | | 308 | it('can see optimistic drag drop while previous rebase running', async () => { |
| b69ab31 | | | 309 | // c |
| b69ab31 | | | 310 | // c | e |
| b69ab31 | | | 311 | // e b |/ |
| b69ab31 | | | 312 | // d | e b |
| b69ab31 | | | 313 | // c -> | d -> | d |
| b69ab31 | | | 314 | // b |/ |/ |
| b69ab31 | | | 315 | // a a a |
| b69ab31 | | | 316 | dragAndDropCommits('d', 'a'); |
| b69ab31 | | | 317 | fireEvent.click(screen.getByText('Run Rebase')); |
| b69ab31 | | | 318 | await waitFor(() => { |
| b69ab31 | | | 319 | expect(screen.getByText('rebasing...')).toBeInTheDocument(); |
| b69ab31 | | | 320 | }); |
| b69ab31 | | | 321 | dragAndDropCommits( |
| b69ab31 | | | 322 | getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT), |
| b69ab31 | | | 323 | 'b', |
| b69ab31 | | | 324 | ); |
| b69ab31 | | | 325 | fireEvent.click(screen.getByText('Run Rebase')); |
| b69ab31 | | | 326 | |
| b69ab31 | | | 327 | // original optimistic is still there |
| b69ab31 | | | 328 | expect(scanForkedBranchHashes('a')).toContain('d'); |
| b69ab31 | | | 329 | // new optimistic state is also there |
| b69ab31 | | | 330 | expect(scanForkedBranchHashes('b')).toEqual(['e']); |
| b69ab31 | | | 331 | }); |
| b69ab31 | | | 332 | }); |
| b69ab31 | | | 333 | }); |