| 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 | |
| 8 | import type {Hash} from '../../types'; |
| 9 | |
| 10 | import {act, fireEvent, render, screen} from '@testing-library/react'; |
| 11 | import {nextTick} from 'shared/testUtils'; |
| 12 | import App from '../../App'; |
| 13 | import platform from '../../platform'; |
| 14 | import { |
| 15 | COMMIT, |
| 16 | closeCommitInfoSidebar, |
| 17 | expectMessageNOTSentToServer, |
| 18 | expectMessageSentToServer, |
| 19 | expectYouAreHerePointAt, |
| 20 | resetTestMessages, |
| 21 | simulateCommits, |
| 22 | } from '../../testUtils'; |
| 23 | import {CommandRunner, succeedableRevset} from '../../types'; |
| 24 | |
| 25 | describe('GotoOperation', () => { |
| 26 | beforeEach(() => { |
| 27 | resetTestMessages(); |
| 28 | render(<App />); |
| 29 | act(() => { |
| 30 | closeCommitInfoSidebar(); |
| 31 | expectMessageSentToServer({ |
| 32 | type: 'subscribe', |
| 33 | kind: 'smartlogCommits', |
| 34 | subscriptionID: expect.anything(), |
| 35 | }); |
| 36 | simulateCommits({ |
| 37 | value: [ |
| 38 | COMMIT('2', 'master', '00', {phase: 'public', remoteBookmarks: ['remote/master']}), |
| 39 | COMMIT('1', 'Commit 1', '0', {phase: 'public'}), |
| 40 | COMMIT('a', 'Commit A', '1'), |
| 41 | COMMIT('b', 'Commit B', 'a', {isDot: true}), |
| 42 | COMMIT('c', 'Commit C', 'b'), |
| 43 | ], |
| 44 | }); |
| 45 | }); |
| 46 | }); |
| 47 | |
| 48 | const clickGoto = async (commit: Hash) => { |
| 49 | const myCommit = screen.queryByTestId(`commit-${commit}`); |
| 50 | const gotoButton = myCommit?.querySelector('.goto-button button'); |
| 51 | expect(gotoButton).toBeDefined(); |
| 52 | await act(async () => { |
| 53 | fireEvent.click(gotoButton as Element); |
| 54 | await nextTick(); // async check if commit is too old |
| 55 | }); |
| 56 | }; |
| 57 | |
| 58 | it('goto button is accessible', () => { |
| 59 | expect(screen.getByLabelText('Go to commit "Commit A"')).toBeInTheDocument(); |
| 60 | expect(screen.queryByLabelText('Go to commit "Commit B"')).not.toBeInTheDocument(); // already head, no goto button |
| 61 | expect(screen.getByLabelText('Go to commit "Commit C"')).toBeInTheDocument(); |
| 62 | }); |
| 63 | |
| 64 | it('runs goto', async () => { |
| 65 | await clickGoto('a'); |
| 66 | |
| 67 | expectMessageSentToServer({ |
| 68 | type: 'runOperation', |
| 69 | operation: { |
| 70 | args: ['goto', '--rev', succeedableRevset('a')], |
| 71 | id: expect.anything(), |
| 72 | runner: CommandRunner.Sapling, |
| 73 | trackEventName: 'GotoOperation', |
| 74 | }, |
| 75 | }); |
| 76 | }); |
| 77 | |
| 78 | it('renders optimistic state while running', async () => { |
| 79 | await clickGoto('a'); |
| 80 | |
| 81 | expectYouAreHerePointAt('a'); |
| 82 | }); |
| 83 | |
| 84 | it('optimistic state resolves after goto completes', async () => { |
| 85 | await clickGoto('a'); |
| 86 | |
| 87 | act(() => { |
| 88 | simulateCommits({ |
| 89 | value: [ |
| 90 | COMMIT('1', 'Commit 1', '0', {phase: 'public'}), |
| 91 | COMMIT('a', 'Commit A', '1', {isDot: true}), |
| 92 | COMMIT('b', 'Commit B', 'a'), |
| 93 | COMMIT('c', 'Commit C', 'b'), |
| 94 | ], |
| 95 | }); |
| 96 | }); |
| 97 | |
| 98 | // With the DAG renderer, we no longer show old "were here" and new "moving here". |
| 99 | // The idea is that there would be a spinner on the "status" calculation to indicate |
| 100 | // the in-progress checkout. For now this test looks the same as the above. |
| 101 | expectYouAreHerePointAt('a'); |
| 102 | }); |
| 103 | |
| 104 | describe('bookmarks as destinations', () => { |
| 105 | it('runs goto with bookmark', async () => { |
| 106 | await clickGoto('2'); |
| 107 | |
| 108 | expectMessageSentToServer({ |
| 109 | type: 'runOperation', |
| 110 | operation: { |
| 111 | args: ['goto', '--rev', succeedableRevset('remote/master')], |
| 112 | id: expect.anything(), |
| 113 | runner: CommandRunner.Sapling, |
| 114 | trackEventName: 'GotoOperation', |
| 115 | }, |
| 116 | }); |
| 117 | }); |
| 118 | |
| 119 | it('renders optimistic state while running', async () => { |
| 120 | await clickGoto('2'); |
| 121 | |
| 122 | expectYouAreHerePointAt('2'); |
| 123 | }); |
| 124 | }); |
| 125 | |
| 126 | describe('succession', () => { |
| 127 | it('handles successions', async () => { |
| 128 | await clickGoto('c'); |
| 129 | |
| 130 | // get a new batch of commits from some other operation like rebase, which |
| 131 | // rewrites a,b,c into a1,b2,c2 |
| 132 | act(() => { |
| 133 | simulateCommits({ |
| 134 | value: [ |
| 135 | COMMIT('2', 'master', '00', {phase: 'public', remoteBookmarks: ['remote/master']}), |
| 136 | COMMIT('1', 'Commit 1', '0', {phase: 'public'}), |
| 137 | COMMIT('a2', 'Commit A', '1', {closestPredecessors: ['a']}), |
| 138 | COMMIT('b2', 'Commit B', 'a2', {isDot: true, closestPredecessors: ['b']}), |
| 139 | COMMIT('c2', 'Commit C', 'b2', {closestPredecessors: ['c']}), |
| 140 | ], |
| 141 | }); |
| 142 | }); |
| 143 | |
| 144 | // "c" becomes "c2" |
| 145 | expectYouAreHerePointAt('c2'); |
| 146 | }); |
| 147 | }); |
| 148 | |
| 149 | describe('age warning', () => { |
| 150 | let confirmSpy: jest.SpyInstance; |
| 151 | beforeEach(() => { |
| 152 | confirmSpy = jest.spyOn(platform, 'confirm').mockImplementation(() => Promise.resolve(true)); |
| 153 | act(() => { |
| 154 | simulateCommits({ |
| 155 | value: [ |
| 156 | COMMIT('b', 'Commit B', 'a', {isDot: true, date: new Date('2024-03-04')}), |
| 157 | COMMIT('a', 'Commit A', '3', {date: new Date('2024-03-03')}), |
| 158 | COMMIT('3', 'Commit 3', '003', {phase: 'public', date: new Date('2024-03-02')}), |
| 159 | COMMIT('2', 'Commit 2', '002', {phase: 'public', date: new Date('2024-03-01')}), |
| 160 | COMMIT('x', 'Commit X', '1', {date: new Date('2024-03-03')}), |
| 161 | COMMIT('1', 'Commit 1', '001', {phase: 'public', date: new Date('2020-01-01')}), |
| 162 | ], |
| 163 | }); |
| 164 | }); |
| 165 | }); |
| 166 | |
| 167 | it('warns if going to an old commit', async () => { |
| 168 | await clickGoto('1'); |
| 169 | expect(confirmSpy).toHaveBeenCalled(); |
| 170 | }); |
| 171 | |
| 172 | it("cancels goto if you don't confirm", async () => { |
| 173 | confirmSpy = jest.spyOn(platform, 'confirm').mockImplementation(() => Promise.resolve(false)); |
| 174 | await clickGoto('1'); |
| 175 | expect(confirmSpy).toHaveBeenCalled(); |
| 176 | expectMessageNOTSentToServer({ |
| 177 | type: 'runOperation', |
| 178 | operation: expect.objectContaining({ |
| 179 | args: expect.arrayContaining(['goto']), |
| 180 | }), |
| 181 | }); |
| 182 | }); |
| 183 | |
| 184 | it('does not warn for short goto', async () => { |
| 185 | await clickGoto('a'); |
| 186 | expect(confirmSpy).not.toHaveBeenCalled(); |
| 187 | }); |
| 188 | |
| 189 | it('compares base public commit, not destination itself', async () => { |
| 190 | await clickGoto('x'); // x is only 1 day old, but its parent is months older than b's public base. |
| 191 | expect(confirmSpy).toHaveBeenCalled(); |
| 192 | }); |
| 193 | |
| 194 | it('only warns going backwards, not forwards', async () => { |
| 195 | act(() => { |
| 196 | simulateCommits({ |
| 197 | value: [ |
| 198 | COMMIT('b', 'Commit B', 'a', {date: new Date('2024-03-04')}), |
| 199 | COMMIT('a', 'Commit A', '3', {date: new Date('2024-03-03')}), |
| 200 | COMMIT('3', 'Commit 3', '003', {phase: 'public', date: new Date('2024-03-02')}), |
| 201 | COMMIT('2', 'Commit 2', '002', {phase: 'public', date: new Date('2024-03-01')}), |
| 202 | COMMIT('x', 'Commit X', '1', {isDot: true, date: new Date('2024-03-03')}), |
| 203 | COMMIT('1', 'Commit 1', '001', {phase: 'public', date: new Date('2020-01-01')}), |
| 204 | ], |
| 205 | }); |
| 206 | }); |
| 207 | await clickGoto('b'); |
| 208 | expect(confirmSpy).not.toHaveBeenCalled(); |
| 209 | }); |
| 210 | }); |
| 211 | }); |
| 212 | |