| 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 {Set as ImSet} from 'immutable'; |
| 9 | import type {Repository} from 'isl-server/src/Repository'; |
| 10 | import {repositoryCache} from 'isl-server/src/RepositoryCache'; |
| 11 | import fs from 'node:fs'; |
| 12 | import {ComparisonType, type Comparison} from 'shared/Comparison'; |
| 13 | import * as vscode from 'vscode'; |
| 14 | import {vscodeCommands} from '../commands'; |
| 15 | import {shouldOpenBeside} from '../config'; |
| 16 | import {encodeDeletedFileUri} from '../DeletedFileContentProvider'; |
| 17 | import {encodeSaplingDiffUri} from '../DiffContentProvider'; |
| 18 | |
| 19 | // Mock vscode command |
| 20 | jest.mock('vscode', () => { |
| 21 | const actualVscode = jest.requireActual('../../__mocks__/vscode'); |
| 22 | return { |
| 23 | ...actualVscode, |
| 24 | commands: { |
| 25 | executeCommand: jest.fn(), |
| 26 | }, |
| 27 | }; |
| 28 | }); |
| 29 | const mockExecuteVSCodeCommand = vscode.commands.executeCommand as jest.MockedFunction< |
| 30 | typeof vscode.commands.executeCommand |
| 31 | >; |
| 32 | |
| 33 | // Mock fs access |
| 34 | jest.mock('node:fs', () => ({ |
| 35 | promises: { |
| 36 | access: jest.fn(), |
| 37 | }, |
| 38 | })); |
| 39 | const mockFsAccess = fs.promises.access as jest.MockedFunction<typeof fs.promises.access>; |
| 40 | |
| 41 | // Mock global config |
| 42 | jest.mock('../config', () => ({ |
| 43 | shouldOpenBeside: jest.fn(), |
| 44 | })); |
| 45 | const mockShouldOpenBeside = shouldOpenBeside as jest.MockedFunction<typeof shouldOpenBeside>; |
| 46 | |
| 47 | describe('open-file-diff', () => { |
| 48 | const openDiffView = vscodeCommands['sapling.open-file-diff']; |
| 49 | |
| 50 | const repoRoot = '/repo/root'; |
| 51 | const filePath = 'path/to/file'; |
| 52 | const submodulePath = 'path/to/submodule'; |
| 53 | const fileUri = vscode.Uri.file(`${repoRoot}/${filePath}`); |
| 54 | const submoduleUri = vscode.Uri.file(`${repoRoot}/${submodulePath}`); |
| 55 | |
| 56 | // Create a proper mock repository |
| 57 | const mockRepo = { |
| 58 | info: { |
| 59 | repoRoot, |
| 60 | }, |
| 61 | getSubmodulePathCache: jest.fn(), |
| 62 | } as unknown as jest.Mocked<Repository>; |
| 63 | |
| 64 | beforeEach(() => { |
| 65 | jest.clearAllMocks(); |
| 66 | |
| 67 | jest.spyOn(repositoryCache, 'cachedRepositoryForPath').mockReturnValue(mockRepo); |
| 68 | mockRepo.getSubmodulePathCache.mockReturnValue(ImSet([submodulePath])); |
| 69 | mockShouldOpenBeside.mockReturnValue(false); |
| 70 | }); |
| 71 | |
| 72 | it('uncommitted changes, regular file', async () => { |
| 73 | mockFsAccess.mockResolvedValue(undefined); // File exists |
| 74 | |
| 75 | const comparison: Comparison = {type: ComparisonType.UncommittedChanges}; |
| 76 | await openDiffView(fileUri, comparison); |
| 77 | |
| 78 | const expectedLeftRev = '.'; |
| 79 | const expectedLeftUri = encodeSaplingDiffUri(fileUri, expectedLeftRev); |
| 80 | |
| 81 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 82 | 'vscode.diff', |
| 83 | expectedLeftUri, |
| 84 | fileUri, |
| 85 | 'file (Uncommitted Changes)', |
| 86 | {viewColumn: undefined}, |
| 87 | ); |
| 88 | }); |
| 89 | |
| 90 | it('uncommitted changes, submodule', async () => { |
| 91 | mockFsAccess.mockRejectedValue(undefined); // Path exists |
| 92 | |
| 93 | const comparison: Comparison = {type: ComparisonType.UncommittedChanges}; |
| 94 | await openDiffView(submoduleUri, comparison); |
| 95 | |
| 96 | const expectedLeftRev = '.'; |
| 97 | const expectedLeftUri = encodeSaplingDiffUri(submoduleUri, expectedLeftRev); |
| 98 | const expectedRightRev = 'wdir()'; |
| 99 | const expectedRightUri = encodeSaplingDiffUri(submoduleUri, expectedRightRev); |
| 100 | |
| 101 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 102 | 'vscode.diff', |
| 103 | expectedLeftUri, |
| 104 | expectedRightUri, |
| 105 | 'submodule (Uncommitted Changes)', |
| 106 | {viewColumn: undefined}, |
| 107 | ); |
| 108 | }); |
| 109 | |
| 110 | it('uncommitted changes, file deleted', async () => { |
| 111 | mockFsAccess.mockRejectedValue(new Error('File not found')); // File doesn't exist |
| 112 | |
| 113 | const comparison: Comparison = {type: ComparisonType.UncommittedChanges}; |
| 114 | await openDiffView(fileUri, comparison); |
| 115 | |
| 116 | const expectedLeftRev = '.'; |
| 117 | const expectedLeftUri = encodeSaplingDiffUri(fileUri, expectedLeftRev); |
| 118 | const expectedRightUri = encodeDeletedFileUri(fileUri); |
| 119 | |
| 120 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 121 | 'vscode.diff', |
| 122 | expectedLeftUri, |
| 123 | expectedRightUri, |
| 124 | 'file (Uncommitted Changes)', |
| 125 | {viewColumn: undefined}, |
| 126 | ); |
| 127 | }); |
| 128 | |
| 129 | it('head changes, regular file', async () => { |
| 130 | mockFsAccess.mockResolvedValue(undefined); // File exists |
| 131 | |
| 132 | const comparison: Comparison = {type: ComparisonType.HeadChanges}; |
| 133 | await openDiffView(fileUri, comparison); |
| 134 | |
| 135 | const expectedLeftRev = '.^'; |
| 136 | const expectedLeftUri = encodeSaplingDiffUri(fileUri, expectedLeftRev); |
| 137 | |
| 138 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 139 | 'vscode.diff', |
| 140 | expectedLeftUri, |
| 141 | fileUri, |
| 142 | 'file (Head Changes)', |
| 143 | {viewColumn: undefined}, |
| 144 | ); |
| 145 | }); |
| 146 | |
| 147 | it('head changes, submodule', async () => { |
| 148 | mockFsAccess.mockRejectedValue(undefined); // Path exists |
| 149 | |
| 150 | const comparison: Comparison = {type: ComparisonType.HeadChanges}; |
| 151 | await openDiffView(submoduleUri, comparison); |
| 152 | |
| 153 | const expectedLeftRev = '.^'; |
| 154 | const expectedLeftUri = encodeSaplingDiffUri(submoduleUri, expectedLeftRev); |
| 155 | const expectedRightRev = 'wdir()'; |
| 156 | const expectedRightUri = encodeSaplingDiffUri(submoduleUri, expectedRightRev); |
| 157 | |
| 158 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 159 | 'vscode.diff', |
| 160 | expectedLeftUri, |
| 161 | expectedRightUri, |
| 162 | 'submodule (Head Changes)', |
| 163 | {viewColumn: undefined}, |
| 164 | ); |
| 165 | }); |
| 166 | |
| 167 | it('stack changes, regular file', async () => { |
| 168 | mockFsAccess.mockResolvedValue(undefined); // File exists |
| 169 | |
| 170 | const comparison: Comparison = {type: ComparisonType.StackChanges}; |
| 171 | await openDiffView(fileUri, comparison); |
| 172 | |
| 173 | const expectedLeftRev = 'ancestor(.,interestingmaster())'; |
| 174 | const expectedLeftUri = encodeSaplingDiffUri(fileUri, expectedLeftRev); |
| 175 | |
| 176 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 177 | 'vscode.diff', |
| 178 | expectedLeftUri, |
| 179 | fileUri, |
| 180 | 'file (Stack Changes)', |
| 181 | {viewColumn: undefined}, |
| 182 | ); |
| 183 | }); |
| 184 | |
| 185 | it('stack changes, submodule', async () => { |
| 186 | mockFsAccess.mockRejectedValue(undefined); // Path exists |
| 187 | |
| 188 | const comparison: Comparison = {type: ComparisonType.StackChanges}; |
| 189 | await openDiffView(submoduleUri, comparison); |
| 190 | |
| 191 | const expectedLeftRev = 'ancestor(.,interestingmaster())'; |
| 192 | const expectedLeftUri = encodeSaplingDiffUri(submoduleUri, expectedLeftRev); |
| 193 | const expectedRightRev = 'wdir()'; |
| 194 | const expectedRightUri = encodeSaplingDiffUri(submoduleUri, expectedRightRev); |
| 195 | |
| 196 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 197 | 'vscode.diff', |
| 198 | expectedLeftUri, |
| 199 | expectedRightUri, |
| 200 | 'submodule (Stack Changes)', |
| 201 | {viewColumn: undefined}, |
| 202 | ); |
| 203 | }); |
| 204 | |
| 205 | it('committed changes, regular file', async () => { |
| 206 | const comparison: Comparison = {type: ComparisonType.Committed, hash: 'abc123'}; |
| 207 | await openDiffView(fileUri, comparison); |
| 208 | |
| 209 | const expectedLeftRev = 'abc123^'; |
| 210 | const expectedLeftUri = encodeSaplingDiffUri(fileUri, expectedLeftRev); |
| 211 | const expectedRightRev = 'abc123'; |
| 212 | const expectedRightUri = encodeSaplingDiffUri(fileUri, expectedRightRev); |
| 213 | |
| 214 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 215 | 'vscode.diff', |
| 216 | expectedLeftUri, |
| 217 | expectedRightUri, |
| 218 | 'file (In abc123)', |
| 219 | {viewColumn: undefined}, |
| 220 | ); |
| 221 | }); |
| 222 | |
| 223 | it('committed changes, submodule', async () => { |
| 224 | const comparison: Comparison = {type: ComparisonType.Committed, hash: 'abc123'}; |
| 225 | await openDiffView(submoduleUri, comparison); |
| 226 | |
| 227 | const expectedLeftRev = 'abc123^'; |
| 228 | const expectedLeftUri = encodeSaplingDiffUri(submoduleUri, expectedLeftRev); |
| 229 | const expectedRightRev = 'abc123'; |
| 230 | const expectedRightUri = encodeSaplingDiffUri(submoduleUri, expectedRightRev); |
| 231 | |
| 232 | expect(mockExecuteVSCodeCommand).toHaveBeenCalledWith( |
| 233 | 'vscode.diff', |
| 234 | expectedLeftUri, |
| 235 | expectedRightUri, |
| 236 | 'submodule (In abc123)', |
| 237 | {viewColumn: undefined}, |
| 238 | ); |
| 239 | }); |
| 240 | }); |
| 241 | |