addons/isl/src/__tests__/operations/rebaseOperation.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 {Hash} from '../../types';
b69ab319
b69ab3110import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
b69ab3111import App from '../../App';
b69ab3112import {Dag, DagCommitInfo} from '../../dag/dag';
b69ab3113import {RebaseOperation} from '../../operations/RebaseOperation';
b69ab3114import {CommitPreview} from '../../previews';
b69ab3115import {ignoreRTL} from '../../testQueries';
b69ab3116import {
b69ab3117 closeCommitInfoSidebar,
b69ab3118 COMMIT,
b69ab3119 dragAndDropCommits,
b69ab3120 dragCommits,
b69ab3121 dropCommits,
b69ab3122 expectMessageNOTSentToServer,
b69ab3123 expectMessageSentToServer,
b69ab3124 resetTestMessages,
b69ab3125 scanForkedBranchHashes,
b69ab3126 simulateCommits,
b69ab3127 simulateUncommittedChangedFiles,
b69ab3128 TEST_COMMIT_HISTORY,
b69ab3129} from '../../testUtils';
b69ab3130import {CommandRunner, succeedableRevset} from '../../types';
b69ab3131
b69ab3132/*eslint-disable @typescript-eslint/no-non-null-assertion */
b69ab3133
b69ab3134describe('rebase operation', () => {
b69ab3135 // Extend with an obsoleted commit.
b69ab3136 const testHistory = TEST_COMMIT_HISTORY.concat([
b69ab3137 COMMIT('ff1', 'Commit FF1 (obsoleted)', 'z', {successorInfo: {hash: 'ff2', type: 'amend'}}),
b69ab3138 COMMIT('ff2', 'Commit FF2', 'z'),
b69ab3139 ]);
b69ab3140
b69ab3141 beforeEach(() => {
b69ab3142 jest.useFakeTimers();
b69ab3143 resetTestMessages();
b69ab3144 render(<App />);
b69ab3145 act(() => {
b69ab3146 closeCommitInfoSidebar();
b69ab3147 expectMessageSentToServer({
b69ab3148 type: 'subscribe',
b69ab3149 kind: 'smartlogCommits',
b69ab3150 subscriptionID: expect.anything(),
b69ab3151 });
b69ab3152 simulateCommits({
b69ab3153 value: testHistory,
b69ab3154 });
b69ab3155 });
b69ab3156 });
b69ab3157
b69ab3158 afterEach(() => {
b69ab3159 jest.useRealTimers();
b69ab3160 });
b69ab3161
b69ab3162 const getCommitWithPreview = (hash: Hash, preview: CommitPreview): HTMLElement => {
b69ab3163 const previewOfCommit = screen
b69ab3164 .queryAllByTestId(`commit-${hash}`)
b69ab3165 .map(commit => within(commit).queryByTestId('draggable-commit'))
b69ab3166 .find(commit => commit?.classList.contains(`commit-preview-${preview}`));
b69ab3167 expect(previewOfCommit).toBeInTheDocument();
b69ab3168 return previewOfCommit!;
b69ab3169 };
b69ab3170
b69ab3171 it('previews a rebase on drag & drop onto a public commit', () => {
b69ab3172 expect(screen.getAllByText('Commit D')).toHaveLength(1);
b69ab3173 dragAndDropCommits('d', '2');
b69ab3174
b69ab3175 // original commit AND previewed commit are now in the document
b69ab3176 expect(screen.getAllByText('Commit D')).toHaveLength(2);
b69ab3177 // also includes descendants
b69ab3178 expect(screen.getAllByText('Commit E')).toHaveLength(2);
b69ab3179
b69ab3180 // one of them is a rebase preview
b69ab3181 expect(
b69ab3182 screen
b69ab3183 .queryAllByTestId('commit-d')
b69ab3184 .some(commit => commit.querySelector('.commit-preview-rebase-root')),
b69ab3185 ).toEqual(true);
b69ab3186 });
b69ab3187
b69ab3188 it('sets all descendants as the right preview type', () => {
b69ab3189 expect(screen.getAllByText('Commit D')).toHaveLength(1);
b69ab3190 dragAndDropCommits('a', '2');
b69ab3191
b69ab3192 expect(document.querySelectorAll('.commit-preview-rebase-old')).toHaveLength(5);
b69ab3193 expect(document.querySelectorAll('.commit-preview-rebase-root')).toHaveLength(1);
b69ab3194 expect(document.querySelectorAll('.commit-preview-rebase-descendant')).toHaveLength(4);
b69ab3195 });
b69ab3196
b69ab3197 it('previews onto correct branch', () => {
b69ab3198 expect(screen.getAllByText('Commit D')).toHaveLength(1);
b69ab3199 dragAndDropCommits('d', 'x');
b69ab31100 expect(scanForkedBranchHashes('x')).toEqual(['d', 'e']);
b69ab31101 });
b69ab31102
b69ab31103 it('cannot drag public commits', () => {
b69ab31104 dragAndDropCommits('1', '2');
b69ab31105
b69ab31106 // only see one copy of commit 1
b69ab31107 expect(screen.queryAllByTestId('commit-1')).toHaveLength(1);
b69ab31108 });
b69ab31109
b69ab31110 it('runs rebase operation', async () => {
b69ab31111 dragAndDropCommits('d', '2');
b69ab31112
b69ab31113 const runRebaseButton = screen.getByText('Run Rebase');
b69ab31114 expect(runRebaseButton).toBeInTheDocument();
b69ab31115
b69ab31116 fireEvent.click(runRebaseButton);
b69ab31117 await waitFor(() => {
b69ab31118 expectMessageSentToServer({
b69ab31119 type: 'runOperation',
b69ab31120 operation: {
b69ab31121 args: ['rebase', '-s', succeedableRevset('d'), '-d', succeedableRevset('remote/master')],
b69ab31122 id: expect.anything(),
b69ab31123 runner: CommandRunner.Sapling,
b69ab31124 trackEventName: 'RebaseOperation',
b69ab31125 },
b69ab31126 });
b69ab31127 });
b69ab31128 });
b69ab31129
b69ab31130 it('shows optimistic preview of rebase', async () => {
b69ab31131 dragAndDropCommits('d', '2');
b69ab31132
b69ab31133 fireEvent.click(screen.getByText('Run Rebase'));
b69ab31134
b69ab31135 // original commit is hidden, we only see optimistic commit
b69ab31136 expect(screen.queryAllByTestId('commit-d')).toHaveLength(1);
b69ab31137 // also includes descendants
b69ab31138 expect(screen.queryAllByTestId('commit-e')).toHaveLength(1);
b69ab31139
b69ab31140 await waitFor(() => {
b69ab31141 expect(screen.getByText('rebasing...')).toBeInTheDocument();
b69ab31142 });
b69ab31143
b69ab31144 expect(
b69ab31145 screen.queryByTestId('commit-d')?.querySelector('.commit-preview-rebase-optimistic-root'),
b69ab31146 ).toBeInTheDocument();
b69ab31147 });
b69ab31148
b69ab31149 it('allows re-dragging a previously dragged commit back onto the same parent', async () => {
b69ab31150 // Drag and drop 'e' normally onto 'c'
b69ab31151 dragCommits('e', 'd');
b69ab31152 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); // dragging on original parent is noop
b69ab31153 dragAndDropCommits('e', 'c');
b69ab31154 expect(screen.getByText('Run Rebase')).toBeInTheDocument();
b69ab31155
b69ab31156 // Drag the previously preview'd 'e' from 'c' onto 'b', without dropping
b69ab31157 dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'b');
b69ab31158 // Keep dragging the previously preview'd 'e' from 'b' back to 'c'
b69ab31159 dragCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c');
b69ab31160 // Finally, drop on 'c'. This should work, even though the preview'd 'e' started on top of 'c', so it's the "parent"
b69ab31161 dropCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c');
b69ab31162
b69ab31163 fireEvent.click(screen.getByText('Run Rebase'));
b69ab31164 await waitFor(() => {
b69ab31165 expectMessageSentToServer({
b69ab31166 type: 'runOperation',
b69ab31167 operation: {
b69ab31168 args: ['rebase', '-s', succeedableRevset('e'), '-d', succeedableRevset('c')],
b69ab31169 id: expect.anything(),
b69ab31170 runner: CommandRunner.Sapling,
b69ab31171 trackEventName: 'RebaseOperation',
b69ab31172 },
b69ab31173 });
b69ab31174 });
b69ab31175 });
b69ab31176
b69ab31177 it('cancel cancels the preview', () => {
b69ab31178 dragAndDropCommits('d', '2');
b69ab31179
b69ab31180 const cancelButton = screen.getByText('Cancel');
b69ab31181 expect(cancelButton).toBeInTheDocument();
b69ab31182
b69ab31183 act(() => {
b69ab31184 fireEvent.click(cancelButton);
b69ab31185 });
b69ab31186
b69ab31187 // now the preview doesn't exist
b69ab31188 expect(screen.queryAllByTestId('commit-d')).toHaveLength(1);
b69ab31189
b69ab31190 // we didn't run any operation
b69ab31191 expectMessageNOTSentToServer({
b69ab31192 type: 'runOperation',
b69ab31193 operation: expect.anything(),
b69ab31194 });
b69ab31195 });
b69ab31196
b69ab31197 it('cannot drag with uncommitted changes', () => {
b69ab31198 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]}));
b69ab31199 dragAndDropCommits('d', '2');
b69ab31200
b69ab31201 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument();
b69ab31202 expect(screen.getByText('Cannot drag to rebase with uncommitted changes.')).toBeInTheDocument();
b69ab31203 });
b69ab31204
b69ab31205 it('cannot drag obsoleted commits', () => {
b69ab31206 dragAndDropCommits('ff1', 'e');
b69ab31207
b69ab31208 expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument();
b69ab31209 expect(screen.getByText('Cannot rebase obsoleted commits.')).toBeInTheDocument();
b69ab31210 });
b69ab31211
b69ab31212 it('can drag if uncommitted changes are optimistically removed', async () => {
b69ab31213 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: 'M'}]}));
b69ab31214 act(() => {
b69ab31215 fireEvent.click(screen.getByTestId('quick-commit-button'));
b69ab31216 });
b69ab31217 await waitFor(() => {
b69ab31218 expect(screen.queryByText(ignoreRTL('file1.txt'))).not.toBeInTheDocument();
b69ab31219 });
b69ab31220 dragAndDropCommits('d', '2');
b69ab31221
b69ab31222 expect(
b69ab31223 screen.queryByText('Cannot drag to rebase with uncommitted changes.'),
b69ab31224 ).not.toBeInTheDocument();
b69ab31225 });
b69ab31226
b69ab31227 it('can drag with untracked changes', () => {
b69ab31228 act(() => simulateUncommittedChangedFiles({value: [{path: 'file1.txt', status: '?'}]}));
b69ab31229 dragAndDropCommits('d', '2');
b69ab31230
b69ab31231 expect(screen.queryByText('Run Rebase')).toBeInTheDocument();
b69ab31232 });
b69ab31233
b69ab31234 it('handles partial rebase in optimistic dag', () => {
b69ab31235 const dag = new Dag().add(TEST_COMMIT_HISTORY.map(c => DagCommitInfo.fromCommitInfo(c)));
b69ab31236
b69ab31237 const type = 'succeedable-revset';
b69ab31238 // Rebase a-b-c-d-e to z
b69ab31239 const rebaseOp = new RebaseOperation({type, revset: 'a'}, {type, revset: 'z'});
b69ab31240 // Count commits with the given title in a dag.
b69ab31241 const count = (dag: Dag, title: string): number =>
b69ab31242 dag.getBatch([...dag]).filter(c => c.title === title).length;
b69ab31243 // Emulate partial rebased: a-b was rebased to z, but not c-d-e
b69ab31244 const partialRebased = dag.rebase(['a', 'b'], 'z');
b69ab31245 // There are 2 "Commit A"s in the partially rebased dag - one obsolsted.
b69ab31246 expect(count(partialRebased, 'Commit A')).toBe(2);
b69ab31247 expect(count(partialRebased, 'Commit B')).toBe(2);
b69ab31248 expect(partialRebased.descendants('z').size).toBe(dag.descendants('z').size + 2);
b69ab31249
b69ab31250 // Calculate the optimistic dag from a partial rebase state.
b69ab31251 const optimisticDag = rebaseOp.optimisticDag(partialRebased);
b69ab31252 // should be only 1 "Commit A"s.
b69ab31253 expect(count(optimisticDag, 'Commit A')).toBe(1);
b69ab31254 expect(count(optimisticDag, 'Commit B')).toBe(1);
b69ab31255 expect(count(optimisticDag, 'Commit E')).toBe(1);
b69ab31256 // check the Commit A..E branch is completed rebased.
b69ab31257 expect(dag.children(dag.parents('a')).size).toBe(
b69ab31258 optimisticDag.children(dag.parents('a')).size + 1,
b69ab31259 );
b69ab31260 expect(optimisticDag.descendants('z').size).toBe(dag.descendants('a').size + 1);
b69ab31261 });
b69ab31262
b69ab31263 describe('stacking optimistic state', () => {
b69ab31264 it('cannot drag and drop preview descendants', () => {
b69ab31265 dragAndDropCommits('d', 'a');
b69ab31266 expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']);
b69ab31267
b69ab31268 dragAndDropCommits(getCommitWithPreview('e', CommitPreview.REBASE_DESCENDANT), 'b');
b69ab31269
b69ab31270 // we still see same commit preview
b69ab31271 expect(scanForkedBranchHashes('a')).toEqual(['d', 'e']);
b69ab31272 });
b69ab31273
b69ab31274 it('can drag preview root again', () => {
b69ab31275 dragAndDropCommits('d', 'a');
b69ab31276
b69ab31277 dragAndDropCommits(getCommitWithPreview('d', CommitPreview.REBASE_ROOT), 'b');
b69ab31278
b69ab31279 // preview is updated to be based on b
b69ab31280 expect(scanForkedBranchHashes('b')).toEqual(['d', 'e']);
b69ab31281 });
b69ab31282
b69ab31283 it('can preview drag drop while previous rebase running', async () => {
b69ab31284 // c
b69ab31285 // c | e
b69ab31286 // e b |/
b69ab31287 // d | e b
b69ab31288 // c -> | d -> | d
b69ab31289 // b |/ |/
b69ab31290 // a a a
b69ab31291 dragAndDropCommits('d', 'a');
b69ab31292 fireEvent.click(screen.getByText('Run Rebase'));
b69ab31293 await waitFor(() => {
b69ab31294 expect(screen.getByText('rebasing...')).toBeInTheDocument();
b69ab31295 });
b69ab31296
b69ab31297 dragAndDropCommits(
b69ab31298 getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
b69ab31299 'b',
b69ab31300 );
b69ab31301
b69ab31302 // original optimistic is still there
b69ab31303 expect(scanForkedBranchHashes('a')).toContain('d');
b69ab31304 // also previewing new drag
b69ab31305 expect(scanForkedBranchHashes('b')).toEqual(['e']);
b69ab31306 });
b69ab31307
b69ab31308 it('can see optimistic drag drop while previous rebase running', async () => {
b69ab31309 // c
b69ab31310 // c | e
b69ab31311 // e b |/
b69ab31312 // d | e b
b69ab31313 // c -> | d -> | d
b69ab31314 // b |/ |/
b69ab31315 // a a a
b69ab31316 dragAndDropCommits('d', 'a');
b69ab31317 fireEvent.click(screen.getByText('Run Rebase'));
b69ab31318 await waitFor(() => {
b69ab31319 expect(screen.getByText('rebasing...')).toBeInTheDocument();
b69ab31320 });
b69ab31321 dragAndDropCommits(
b69ab31322 getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
b69ab31323 'b',
b69ab31324 );
b69ab31325 fireEvent.click(screen.getByText('Run Rebase'));
b69ab31326
b69ab31327 // original optimistic is still there
b69ab31328 expect(scanForkedBranchHashes('a')).toContain('d');
b69ab31329 // new optimistic state is also there
b69ab31330 expect(scanForkedBranchHashes('b')).toEqual(['e']);
b69ab31331 });
b69ab31332 });
b69ab31333});