From f9f4757fcf376249132634f33597fd341e81d183 Mon Sep 17 00:00:00 2001 From: Michael Hutchison Date: Sat, 30 May 2020 21:41:31 +1000 Subject: [PATCH] Significantly expanded the unit test coverage of the back-end. --- .gitignore | 1 + .vscodeignore | 1 + jest.config.js | 5 +- package.json | 3 +- src/extension.ts | 2 +- src/repoManager.ts | 12 +- src/statusBarItem.ts | 8 +- tests/config.test.ts | 3013 +++++++++++++++++++++++++++++++++ tests/diffDocProvider.test.ts | 222 +++ tests/event.test.ts | 6 +- tests/extensionState.test.ts | 1088 ++++++++++++ tests/logger.test.ts | 60 +- tests/mocks/date.ts | 8 +- tests/mocks/vscode.ts | 131 +- tests/repoFileWatcher.test.ts | 124 ++ tests/statusBarItem.test.ts | 155 ++ tests/utils.test.ts | 1066 +++++++++++- 17 files changed, 5813 insertions(+), 92 deletions(-) create mode 100644 tests/config.test.ts create mode 100644 tests/diffDocProvider.test.ts create mode 100644 tests/extensionState.test.ts create mode 100644 tests/repoFileWatcher.test.ts create mode 100644 tests/statusBarItem.test.ts diff --git a/.gitignore b/.gitignore index f85d7fea..975111a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +coverage media node_modules out diff --git a/.vscodeignore b/.vscodeignore index 583f0cc8..3c0c235e 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,5 +1,6 @@ .github/ .vscode/ +coverage/ node_modules/*/*.md out/**/*.d.ts out/**/*.js.map diff --git a/jest.config.js b/jest.config.js index ee164250..b5dd5c93 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,5 +9,8 @@ module.exports = { 'ts-jest': { tsConfig: './tests/tsconfig.json' } - } + }, + collectCoverageFrom: [ + "src/*.ts" + ] }; diff --git a/package.json b/package.json index 68a01701..e78cfe5c 100644 --- a/package.json +++ b/package.json @@ -819,7 +819,8 @@ "compile-web-debug": "tsc -p ./web && node ./.vscode/package-web.js debug", "package": "npm run clean && vsce package", "package-and-install": "npm run package && node ./.vscode/install-package.js", - "test": "jest --verbose" + "test": "jest --verbose", + "test-and-report-coverage": "jest --verbose --coverage" }, "dependencies": { "iconv-lite": "0.5.0" diff --git a/src/extension.ts b/src/extension.ts index 3427c296..97f61bf0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -42,7 +42,7 @@ export async function activate(context: vscode.ExtensionContext) { const dataSource = new DataSource(gitExecutable, onDidChangeConfiguration, onDidChangeGitExecutable, logger); const avatarManager = new AvatarManager(dataSource, extensionState, logger); const repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration, logger); - const statusBarItem = new StatusBarItem(repoManager, onDidChangeConfiguration, logger); + const statusBarItem = new StatusBarItem(repoManager.getNumRepos(), repoManager.onDidChangeRepos, onDidChangeConfiguration, logger); const commandManager = new CommandManager(context.extensionPath, avatarManager, dataSource, extensionState, repoManager, gitExecutable, onDidChangeGitExecutable, logger); context.subscriptions.push( diff --git a/src/repoManager.ts b/src/repoManager.ts index d8796027..317a025c 100644 --- a/src/repoManager.ts +++ b/src/repoManager.ts @@ -8,7 +8,7 @@ import { Logger } from './logger'; import { GitRepoSet, GitRepoState } from './types'; import { evalPromises, getPathFromUri, pathWithTrailingSlash, realpath } from './utils'; -interface RepoChangeEvent { +export interface RepoChangeEvent { repos: GitRepoSet; numRepos: number; loadRepo: string | null; @@ -199,6 +199,14 @@ export class RepoManager implements vscode.Disposable { return repos; } + /** + * Get the number of all known repositories in the current workspace. + * @returns The number of repositories. + */ + public getNumRepos() { + return Object.keys(this.repos).length; + } + /** * Get the repository that contains the specified file. * @param path The path of the file. @@ -318,7 +326,7 @@ export class RepoManager implements vscode.Disposable { private sendRepos(loadRepo: string | null = null) { this.repoEventEmitter.emit({ repos: this.getRepos(), - numRepos: Object.keys(this.repos).length, + numRepos: this.getNumRepos(), loadRepo: loadRepo }); } diff --git a/src/statusBarItem.ts b/src/statusBarItem.ts index ee124078..e67807d8 100644 --- a/src/statusBarItem.ts +++ b/src/statusBarItem.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { getConfig } from './config'; import { Event } from './event'; import { Logger } from './logger'; -import { RepoManager } from './repoManager'; +import { RepoChangeEvent } from './repoManager'; /** * Manages the Git Graph Status Bar Item, which allows users to open the Git Graph View from the Visual Studio Code Status Bar. @@ -19,7 +19,7 @@ export class StatusBarItem implements vscode.Disposable { * @param repoManager The Git Graph RepoManager instance. * @param logger The Git Graph Logger instance. */ - constructor(repoManager: RepoManager, onDidChangeConfiguration: Event, logger: Logger) { + constructor(initialNumRepos: number, onDidChangeRepos: Event, onDidChangeConfiguration: Event, logger: Logger) { this.logger = logger; const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); @@ -29,7 +29,7 @@ export class StatusBarItem implements vscode.Disposable { this.statusBarItem = statusBarItem; this.disposables.push(statusBarItem); - repoManager.onDidChangeRepos((event) => { + onDidChangeRepos((event) => { this.setNumRepos(event.numRepos); }, this.disposables); @@ -39,7 +39,7 @@ export class StatusBarItem implements vscode.Disposable { } }, this.disposables); - this.setNumRepos(Object.keys(repoManager.getRepos()).length); + this.setNumRepos(initialNumRepos); } /** diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 00000000..694f16ca --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,3013 @@ +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); + +import { getConfig } from '../src/config'; +import { CommitDetailsViewLocation, CommitOrdering, DateFormatType, DateType, FileViewType, GitResetMode, GraphStyle, RefLabelAlignment, TabIconColourTheme } from '../src/types'; + +let workspaceConfiguration = vscode.mocks.workspaceConfiguration; + +beforeEach(() => { + vscode.workspace.getConfiguration.mockClear(); + workspaceConfiguration.get.mockClear(); +}); + +describe('Config', () => { + let config: ReturnType; + beforeEach(() => { + config = getConfig(); + }); + + describe('autoCenterCommitDetailsView', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.autoCenterCommitDetailsView; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('autoCenterCommitDetailsView', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.autoCenterCommitDetailsView; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('autoCenterCommitDetailsView', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.autoCenterCommitDetailsView; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('autoCenterCommitDetailsView', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.autoCenterCommitDetailsView; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('autoCenterCommitDetailsView', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.autoCenterCommitDetailsView; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('autoCenterCommitDetailsView', true); + expect(value).toBe(true); + }); + }); + + describe('combineLocalAndRemoteBranchLabels', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.combineLocalAndRemoteBranchLabels; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('combineLocalAndRemoteBranchLabels', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.combineLocalAndRemoteBranchLabels; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('combineLocalAndRemoteBranchLabels', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.combineLocalAndRemoteBranchLabels; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('combineLocalAndRemoteBranchLabels', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.combineLocalAndRemoteBranchLabels; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('combineLocalAndRemoteBranchLabels', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.combineLocalAndRemoteBranchLabels; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('combineLocalAndRemoteBranchLabels', true); + expect(value).toBe(true); + }); + }); + + describe('commitDetailsViewLocation', () => { + it('Should return CommitDetailsViewLocation.Inline when the configuration value is "Inline"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Inline'); + + // Run + const value = config.commitDetailsViewLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitDetailsViewLocation', 'Inline'); + expect(value).toBe(CommitDetailsViewLocation.Inline); + }); + + it('Should return CommitDetailsViewLocation.DockedToBottom when the configuration value is "Docked to Bottom"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Docked to Bottom'); + + // Run + const value = config.commitDetailsViewLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitDetailsViewLocation', 'Inline'); + expect(value).toBe(CommitDetailsViewLocation.DockedToBottom); + }); + + it('Should return the default value (CommitDetailsViewLocation.Inline) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.commitDetailsViewLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitDetailsViewLocation', 'Inline'); + expect(value).toBe(CommitDetailsViewLocation.Inline); + }); + + it('Should return the default value (CommitDetailsViewLocation.Inline) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.commitDetailsViewLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitDetailsViewLocation', 'Inline'); + expect(value).toBe(CommitDetailsViewLocation.Inline); + }); + }); + + describe('commitOrdering', () => { + it('Should return CommitOrdering.Date when the configuration value is "date"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('date'); + + // Run + const value = config.commitOrdering; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitOrdering', 'date'); + expect(value).toBe(CommitOrdering.Date); + }); + + it('Should return CommitOrdering.AuthorDate when the configuration value is "author-date"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('author-date'); + + // Run + const value = config.commitOrdering; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitOrdering', 'date'); + expect(value).toBe(CommitOrdering.AuthorDate); + }); + + it('Should return CommitOrdering.Topological when the configuration value is "topo"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('topo'); + + // Run + const value = config.commitOrdering; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitOrdering', 'date'); + expect(value).toBe(CommitOrdering.Topological); + }); + + it('Should return the default value (CommitOrdering.Date) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.commitOrdering; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitOrdering', 'date'); + expect(value).toBe(CommitOrdering.Date); + }); + + it('Should return the default value (CommitOrdering.Date) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.commitOrdering; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('commitOrdering', 'date'); + expect(value).toBe(CommitOrdering.Date); + }); + }); + + describe('contextMenuActionsVisibility', () => { + it('Should return the default value (all items enabled) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(1); + + // Run + const value = config.contextMenuActionsVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('contextMenuActionsVisibility', {}); + expect(value.branch.checkout).toBe(true); + expect(value.branch.rename).toBe(true); + expect(value.branch.delete).toBe(true); + expect(value.branch.merge).toBe(true); + expect(value.branch.rebase).toBe(true); + expect(value.branch.push).toBe(true); + expect(value.branch.createPullRequest).toBe(true); + expect(value.branch.createArchive).toBe(true); + expect(value.branch.copyName).toBe(true); + expect(value.commit.addTag).toBe(true); + expect(value.commit.createBranch).toBe(true); + expect(value.commit.checkout).toBe(true); + expect(value.commit.cherrypick).toBe(true); + expect(value.commit.revert).toBe(true); + expect(value.commit.drop).toBe(true); + expect(value.commit.merge).toBe(true); + expect(value.commit.rebase).toBe(true); + expect(value.commit.reset).toBe(true); + expect(value.commit.copyHash).toBe(true); + expect(value.commit.copySubject).toBe(true); + expect(value.remoteBranch.checkout).toBe(true); + expect(value.remoteBranch.delete).toBe(true); + expect(value.remoteBranch.fetch).toBe(true); + expect(value.remoteBranch.pull).toBe(true); + expect(value.remoteBranch.createPullRequest).toBe(true); + expect(value.remoteBranch.createArchive).toBe(true); + expect(value.remoteBranch.copyName).toBe(true); + expect(value.stash.apply).toBe(true); + expect(value.stash.createBranch).toBe(true); + expect(value.stash.pop).toBe(true); + expect(value.stash.drop).toBe(true); + expect(value.stash.copyName).toBe(true); + expect(value.stash.copyHash).toBe(true); + expect(value.tag.viewDetails).toBe(true); + expect(value.tag.delete).toBe(true); + expect(value.tag.push).toBe(true); + expect(value.tag.createArchive).toBe(true); + expect(value.tag.copyName).toBe(true); + expect(value.uncommittedChanges.stash).toBe(true); + expect(value.uncommittedChanges.reset).toBe(true); + expect(value.uncommittedChanges.clean).toBe(true); + expect(value.uncommittedChanges.openSourceControlView).toBe(true); + }); + + it('Should return the default value (all items enabled) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.contextMenuActionsVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('contextMenuActionsVisibility', {}); + expect(value.branch.checkout).toBe(true); + expect(value.branch.rename).toBe(true); + expect(value.branch.delete).toBe(true); + expect(value.branch.merge).toBe(true); + expect(value.branch.rebase).toBe(true); + expect(value.branch.push).toBe(true); + expect(value.branch.createPullRequest).toBe(true); + expect(value.branch.createArchive).toBe(true); + expect(value.branch.copyName).toBe(true); + expect(value.commit.addTag).toBe(true); + expect(value.commit.createBranch).toBe(true); + expect(value.commit.checkout).toBe(true); + expect(value.commit.cherrypick).toBe(true); + expect(value.commit.revert).toBe(true); + expect(value.commit.drop).toBe(true); + expect(value.commit.merge).toBe(true); + expect(value.commit.rebase).toBe(true); + expect(value.commit.reset).toBe(true); + expect(value.commit.copyHash).toBe(true); + expect(value.commit.copySubject).toBe(true); + expect(value.remoteBranch.checkout).toBe(true); + expect(value.remoteBranch.delete).toBe(true); + expect(value.remoteBranch.fetch).toBe(true); + expect(value.remoteBranch.pull).toBe(true); + expect(value.remoteBranch.createPullRequest).toBe(true); + expect(value.remoteBranch.createArchive).toBe(true); + expect(value.remoteBranch.copyName).toBe(true); + expect(value.stash.apply).toBe(true); + expect(value.stash.createBranch).toBe(true); + expect(value.stash.pop).toBe(true); + expect(value.stash.drop).toBe(true); + expect(value.stash.copyName).toBe(true); + expect(value.stash.copyHash).toBe(true); + expect(value.tag.viewDetails).toBe(true); + expect(value.tag.delete).toBe(true); + expect(value.tag.push).toBe(true); + expect(value.tag.createArchive).toBe(true); + expect(value.tag.copyName).toBe(true); + expect(value.uncommittedChanges.stash).toBe(true); + expect(value.uncommittedChanges.reset).toBe(true); + expect(value.uncommittedChanges.clean).toBe(true); + expect(value.uncommittedChanges.openSourceControlView).toBe(true); + }); + + it('Should only affect the provided configuration overrides', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce({ + branch: { + rename: false + }, + commit: { + checkout: false + }, + remoteBranch: { + delete: true, + fetch: false, + pull: true + } + }); + + // Run + const value = config.contextMenuActionsVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('contextMenuActionsVisibility', {}); + expect(value.branch.checkout).toBe(true); + expect(value.branch.rename).toBe(false); + expect(value.branch.delete).toBe(true); + expect(value.branch.merge).toBe(true); + expect(value.branch.rebase).toBe(true); + expect(value.branch.push).toBe(true); + expect(value.branch.createPullRequest).toBe(true); + expect(value.branch.createArchive).toBe(true); + expect(value.branch.copyName).toBe(true); + expect(value.commit.addTag).toBe(true); + expect(value.commit.createBranch).toBe(true); + expect(value.commit.checkout).toBe(false); + expect(value.commit.cherrypick).toBe(true); + expect(value.commit.revert).toBe(true); + expect(value.commit.drop).toBe(true); + expect(value.commit.merge).toBe(true); + expect(value.commit.rebase).toBe(true); + expect(value.commit.reset).toBe(true); + expect(value.commit.copyHash).toBe(true); + expect(value.commit.copySubject).toBe(true); + expect(value.remoteBranch.checkout).toBe(true); + expect(value.remoteBranch.delete).toBe(true); + expect(value.remoteBranch.fetch).toBe(false); + expect(value.remoteBranch.pull).toBe(true); + expect(value.remoteBranch.createPullRequest).toBe(true); + expect(value.remoteBranch.createArchive).toBe(true); + expect(value.remoteBranch.copyName).toBe(true); + expect(value.stash.apply).toBe(true); + expect(value.stash.createBranch).toBe(true); + expect(value.stash.pop).toBe(true); + expect(value.stash.drop).toBe(true); + expect(value.stash.copyName).toBe(true); + expect(value.stash.copyHash).toBe(true); + expect(value.tag.viewDetails).toBe(true); + expect(value.tag.delete).toBe(true); + expect(value.tag.push).toBe(true); + expect(value.tag.createArchive).toBe(true); + expect(value.tag.copyName).toBe(true); + expect(value.uncommittedChanges.stash).toBe(true); + expect(value.uncommittedChanges.reset).toBe(true); + expect(value.uncommittedChanges.clean).toBe(true); + expect(value.uncommittedChanges.openSourceControlView).toBe(true); + }); + }); + + describe('customBranchGlobPatterns', () => { + it('Should return a filtered array of glob patterns based on the configuration value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce([ + { name: 'Name 1', glob: 'glob1' }, + { name: 'Name 2', glob: 'glob2' }, + { name: 'Name 3' }, + { name: 'Name 4', glob: 'glob4' } + ]); + + // Run + const value = config.customBranchGlobPatterns; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customBranchGlobPatterns', []); + expect(value).toHaveLength(3); + expect(value[0]).toStrictEqual({ name: 'Name 1', glob: '--glob=glob1' }); + expect(value[1]).toStrictEqual({ name: 'Name 2', glob: '--glob=glob2' }); + expect(value[2]).toStrictEqual({ name: 'Name 4', glob: '--glob=glob4' }); + }); + + it('Should return the default value ([]) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.customBranchGlobPatterns; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customBranchGlobPatterns', []); + expect(value).toHaveLength(0); + }); + }); + + describe('customEmojiShortcodeMappings', () => { + it('Should return a filtered array of emoji shortcode mappings based on the configuration value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce([ + { shortcode: 'dog', emoji: '🍎' }, + { shortcode: 'cat', emoji: '🎨' }, + { shortcode: 'bird' }, + { shortcode: 'fish', emoji: '🐛' } + ]); + + // Run + const value = config.customEmojiShortcodeMappings; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customEmojiShortcodeMappings', []); + expect(value).toStrictEqual([ + { shortcode: 'dog', emoji: '🍎' }, + { shortcode: 'cat', emoji: '🎨' }, + { shortcode: 'fish', emoji: '🐛' } + ]); + }); + + it('Should return the default value ([]) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.customEmojiShortcodeMappings; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customEmojiShortcodeMappings', []); + expect(value).toHaveLength(0); + }); + }); + + describe('customPullRequestProviders', () => { + it('Should return a filtered array of pull request providers based on the configuration value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce([ + { name: 'dog', templateUrl: '$1/$2' }, + { name: 'cat', templateUrl: '$1/$3' }, + { name: 'bird' }, + { name: 'fish', templateUrl: '$1/$4' }, + { templateUrl: '$1/$5' } + ]); + + // Run + const value = config.customPullRequestProviders; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customPullRequestProviders', []); + expect(value).toStrictEqual([ + { name: 'dog', templateUrl: '$1/$2' }, + { name: 'cat', templateUrl: '$1/$3' }, + { name: 'fish', templateUrl: '$1/$4' } + ]); + }); + + it('Should return the default value ([]) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.customPullRequestProviders; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customPullRequestProviders', []); + expect(value).toHaveLength(0); + }); + + it('Should return the default value ([]) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.customPullRequestProviders; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('customPullRequestProviders', []); + expect(value).toHaveLength(0); + }); + }); + + describe('dateFormat', () => { + it('Should successfully parse the configuration value "Date & Time"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Date & Time'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateAndTime, iso: false }); + }); + + it('Should successfully parse the configuration value "Date Only"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Date Only'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateOnly, iso: false }); + }); + + it('Should successfully parse the configuration value "ISO Date & Time"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('ISO Date & Time'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateAndTime, iso: true }); + }); + + it('Should successfully parse the configuration value "ISO Date Only"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('ISO Date Only'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateOnly, iso: true }); + }); + + it('Should successfully parse the configuration value "Relative"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Relative'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.Relative, iso: false }); + }); + + it('Should return the default value when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateAndTime, iso: false }); + }); + + it('Should return the default value when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.dateFormat; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateFormat', 'Date & Time'); + expect(value).toStrictEqual({ type: DateFormatType.DateAndTime, iso: false }); + }); + }); + + describe('dateType', () => { + it('Should return DateType.Author when the configuration value is "Author Date"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Author Date'); + + // Run + const value = config.dateType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateType', 'Author Date'); + expect(value).toBe(DateType.Author); + }); + + it('Should return DateType.Commit when the configuration value is "Commit Date"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Commit Date'); + + // Run + const value = config.dateType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateType', 'Author Date'); + expect(value).toBe(DateType.Commit); + }); + + it('Should return the default value (DateType.Author) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.dateType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateType', 'Author Date'); + expect(value).toBe(DateType.Author); + }); + + it('Should return the default value (DateType.Author) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.dateType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dateType', 'Author Date'); + expect(value).toBe(DateType.Author); + }); + }); + + describe('defaultColumnVisibility', () => { + it('Should successfully parse the configuration value (Date column disabled)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce({ Date: false, Author: true, Commit: true }); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: false, author: true, commit: true }); + }); + + it('Should successfully parse the configuration value (Author column disabled)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce({ Date: true, Author: false, Commit: true }); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: false, commit: true }); + }); + + it('Should successfully parse the configuration value (Commit column disabled)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce({ Date: true, Author: true, Commit: false }); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: true, commit: false }); + }); + + it('Should return the default value when the configuration value is invalid (not an object)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: true, commit: true }); + }); + + it('Should return the default value when the configuration value is invalid (NULL)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(null); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: true, commit: true }); + }); + + it('Should return the default value when the configuration value is invalid (column value is not a boolean)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce({ Date: true, Author: true, Commit: 5 }); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: true, commit: true }); + }); + + it('Should return the default value when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.defaultColumnVisibility; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultColumnVisibility', {}); + expect(value).toStrictEqual({ date: true, author: true, commit: true }); + }); + }); + + describe('defaultFileViewType', () => { + it('Should return FileViewType.Tree when the configuration value is "File Tree"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('File Tree'); + + // Run + const value = config.defaultFileViewType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultFileViewType', 'File Tree'); + expect(value).toBe(FileViewType.Tree); + }); + + it('Should return FileViewType.List when the configuration value is "File List"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('File List'); + + // Run + const value = config.defaultFileViewType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultFileViewType', 'File Tree'); + expect(value).toBe(FileViewType.List); + }); + + it('Should return the default value (FileViewType.Tree) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.defaultFileViewType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultFileViewType', 'File Tree'); + expect(value).toBe(FileViewType.Tree); + }); + + it('Should return the default value (FileViewType.Tree) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.defaultFileViewType; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('defaultFileViewType', 'File Tree'); + expect(value).toBe(FileViewType.Tree); + }); + }); + + describe('dialogDefaults', () => { + it('Should return TRUE values for boolean-based configuration values when they are TRUE', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type' || section === 'dialog.resetCurrentBranchToCommit.mode' || section === 'dialog.resetUncommittedChanges.mode') { + return defaultValue; + } else { + return true; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: true, + type: 'annotated' + }, + applyStash: { + reinstateIndex: true + }, + cherryPick: { + recordOrigin: true + }, + createBranch: { + checkout: true + }, + deleteBranch: { + forceDelete: true + }, + merge: { + noCommit: true, + noFastForward: true, + squash: true + }, + popStash: { + reinstateIndex: true + }, + rebase: { + ignoreDate: true, + interactive: true + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: true + } + }); + }); + + it('Should return FALSE values for boolean-based configuration values when they are FALSE', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type' || section === 'dialog.resetCurrentBranchToCommit.mode' || section === 'dialog.resetUncommittedChanges.mode') { + return defaultValue; + } else { + return false; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: false, + type: 'annotated' + }, + applyStash: { + reinstateIndex: false + }, + cherryPick: { + recordOrigin: false + }, + createBranch: { + checkout: false + }, + deleteBranch: { + forceDelete: false + }, + merge: { + noCommit: false, + noFastForward: false, + squash: false + }, + popStash: { + reinstateIndex: false + }, + rebase: { + ignoreDate: false, + interactive: false + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: false + } + }); + }); + + it('Should return TRUE values for boolean-based configuration values when they are truthy', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type' || section === 'dialog.resetCurrentBranchToCommit.mode' || section === 'dialog.resetUncommittedChanges.mode') { + return defaultValue; + } else { + return 1; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: true, + type: 'annotated' + }, + applyStash: { + reinstateIndex: true + }, + cherryPick: { + recordOrigin: true + }, + createBranch: { + checkout: true + }, + deleteBranch: { + forceDelete: true + }, + merge: { + noCommit: true, + noFastForward: true, + squash: true + }, + popStash: { + reinstateIndex: true + }, + rebase: { + ignoreDate: true, + interactive: true + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: true + } + }); + }); + + it('Should return FALSE values for boolean-based configuration values when they are falsy', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type' || section === 'dialog.resetCurrentBranchToCommit.mode' || section === 'dialog.resetUncommittedChanges.mode') { + return defaultValue; + } else { + return 0; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: false, + type: 'annotated' + }, + applyStash: { + reinstateIndex: false + }, + cherryPick: { + recordOrigin: false + }, + createBranch: { + checkout: false + }, + deleteBranch: { + forceDelete: false + }, + merge: { + noCommit: false, + noFastForward: false, + squash: false + }, + popStash: { + reinstateIndex: false + }, + rebase: { + ignoreDate: false, + interactive: false + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: false + } + }); + }); + + it('Should return the default values for text-based configuration values when they are invalid', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type' || section === 'dialog.resetCurrentBranchToCommit.mode' || section === 'dialog.resetUncommittedChanges.mode') { + return 'invalid'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: false, + type: 'annotated' + }, + applyStash: { + reinstateIndex: false + }, + cherryPick: { + recordOrigin: false + }, + createBranch: { + checkout: false + }, + deleteBranch: { + forceDelete: false + }, + merge: { + noCommit: false, + noFastForward: true, + squash: false + }, + popStash: { + reinstateIndex: false + }, + rebase: { + ignoreDate: true, + interactive: false + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: true + } + }); + }); + + it('Should return the default values when the configuration values are not set', () => { + // Setup + workspaceConfiguration.get.mockImplementation((_, defaultValue) => defaultValue); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.pushToRemote', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.addTag.type', 'Annotated'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.applyStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.cherryPick.recordOrigin', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.createBranch.checkOut', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.deleteBranch.forceDelete', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.popStash.reinstateIndex', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.ignoreDate', true); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.rebase.launchInteractiveRebase', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.resetUncommittedChanges.mode', 'Mixed'); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.stashUncommittedChanges.includeUntracked', true); + expect(value).toStrictEqual({ + addTag: { + pushToRemote: false, + type: 'annotated' + }, + applyStash: { + reinstateIndex: false + }, + cherryPick: { + recordOrigin: false + }, + createBranch: { + checkout: false + }, + deleteBranch: { + forceDelete: false + }, + merge: { + noCommit: false, + noFastForward: true, + squash: false + }, + popStash: { + reinstateIndex: false + }, + rebase: { + ignoreDate: true, + interactive: false + }, + resetCommit: { + mode: GitResetMode.Mixed + }, + resetUncommitted: { + mode: GitResetMode.Mixed + }, + stashUncommittedChanges: { + includeUntracked: true + } + }); + }); + + describe('dialogDefaults.addTag.type', () => { + it('Should return "annotated" the configuration value is "Annotated"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type') { + return 'Annotated'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.addTag.type).toBe('annotated'); + }); + + it('Should return "lightweight" the configuration value is "Annotated"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.addTag.type') { + return 'Lightweight'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.addTag.type).toBe('lightweight'); + }); + }); + + describe('dialogDefaults.resetCommit.mode', () => { + it('Should return GitResetMode.Hard the configuration value is "Hard"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.resetCurrentBranchToCommit.mode') { + return 'Hard'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.resetCommit.mode).toBe(GitResetMode.Hard); + }); + + it('Should return GitResetMode.Mixed the configuration value is "Mixed"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.resetCurrentBranchToCommit.mode') { + return 'Mixed'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.resetCommit.mode).toBe(GitResetMode.Mixed); + }); + + it('Should return GitResetMode.Soft the configuration value is "Soft"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.resetCurrentBranchToCommit.mode') { + return 'Soft'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.resetCommit.mode).toBe(GitResetMode.Soft); + }); + }); + + describe('dialogDefaults.resetUncommitted.mode', () => { + it('Should return GitResetMode.Hard the configuration value is "Hard"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.resetUncommittedChanges.mode') { + return 'Hard'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.resetUncommitted.mode).toBe(GitResetMode.Hard); + }); + + it('Should return GitResetMode.Mixed the configuration value is "Mixed"', () => { + // Setup + workspaceConfiguration.get.mockImplementation((section, defaultValue) => { + if (section === 'dialog.resetUncommittedChanges.mode') { + return 'Mixed'; + } else { + return defaultValue; + } + }); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.resetUncommitted.mode).toBe(GitResetMode.Mixed); + }); + }); + }); + + describe('fetchAndPrune', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.fetchAndPrune; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAndPrune', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.fetchAndPrune; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAndPrune', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.fetchAndPrune; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAndPrune', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.fetchAndPrune; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAndPrune', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.fetchAndPrune; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAndPrune', false); + expect(value).toBe(false); + }); + }); + + describe('fetchAvatars', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.fetchAvatars; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAvatars', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.fetchAvatars; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAvatars', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.fetchAvatars; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAvatars', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.fetchAvatars; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAvatars', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.fetchAvatars; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fetchAvatars', false); + expect(value).toBe(false); + }); + }); + + describe('fileEncoding', () => { + it('Should return the configured value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('file-encoding'); + + // Run + const value = config.fileEncoding; + + expect(workspaceConfiguration.get).toBeCalledWith('fileEncoding', 'utf8'); + expect(value).toBe('file-encoding'); + }); + + it('Should return the default configuration value ("utf8")', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.fileEncoding; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('fileEncoding', 'utf8'); + expect(value).toBe('utf8'); + }); + }); + + describe('graphColours', () => { + it('Should return a filtered array of colours based on the configuration value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(['#ff0000', '#0000000', '#00ff0088', 'rgb(1,2,3)', 'rgb(1,2,x)']); + + // Run + const value = config.graphColours; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphColours', []); + expect(value).toHaveLength(3); + expect(value[0]).toBe('#ff0000'); + expect(value[1]).toBe('#00ff0088'); + expect(value[2]).toBe('rgb(1,2,3)'); + }); + + it('Should return the default value when the configuration value is invalid (not an array)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.graphColours; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphColours', []); + expect(value).toStrictEqual(['#0085d9', '#d9008f', '#00d90a', '#d98500', '#a300d9', '#ff0000', '#00d9cc', '#e138e8', '#85d900', '#dc5b23', '#6f24d6', '#ffcc00']); + }); + + it('Should return the default value when the configuration value is invalid (an empty array)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce([]); + + // Run + const value = config.graphColours; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphColours', []); + expect(value).toStrictEqual(['#0085d9', '#d9008f', '#00d90a', '#d98500', '#a300d9', '#ff0000', '#00d9cc', '#e138e8', '#85d900', '#dc5b23', '#6f24d6', '#ffcc00']); + }); + + it('Should return the default value when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.graphColours; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphColours', []); + expect(value).toStrictEqual(['#0085d9', '#d9008f', '#00d90a', '#d98500', '#a300d9', '#ff0000', '#00d9cc', '#e138e8', '#85d900', '#dc5b23', '#6f24d6', '#ffcc00']); + }); + }); + + describe('graphStyle', () => { + it('Should return GraphStyle.Rounded when the configuration value is "rounded"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('rounded'); + + // Run + const value = config.graphStyle; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphStyle', 'rounded'); + expect(value).toBe(GraphStyle.Rounded); + }); + + it('Should return GraphStyle.Angular when the configuration value is "angular"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('angular'); + + // Run + const value = config.graphStyle; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphStyle', 'rounded'); + expect(value).toBe(GraphStyle.Angular); + }); + + it('Should return the default value (GraphStyle.Rounded) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.graphStyle; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphStyle', 'rounded'); + expect(value).toBe(GraphStyle.Rounded); + }); + + it('Should return the default value (GraphStyle.Rounded) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.graphStyle; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('graphStyle', 'rounded'); + expect(value).toBe(GraphStyle.Rounded); + }); + }); + + describe('includeCommitsMentionedByReflogs', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.includeCommitsMentionedByReflogs; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('includeCommitsMentionedByReflogs', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.includeCommitsMentionedByReflogs; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('includeCommitsMentionedByReflogs', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.includeCommitsMentionedByReflogs; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('includeCommitsMentionedByReflogs', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.includeCommitsMentionedByReflogs; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('includeCommitsMentionedByReflogs', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.includeCommitsMentionedByReflogs; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('includeCommitsMentionedByReflogs', false); + expect(value).toBe(false); + }); + }); + + describe('initialLoadCommits', () => { + it('Should return the configured value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(600); + + // Run + const value = config.initialLoadCommits; + + expect(workspaceConfiguration.get).toBeCalledWith('initialLoadCommits', 300); + expect(value).toBe(600); + }); + + it('Should return the default configuration value (300)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.initialLoadCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('initialLoadCommits', 300); + expect(value).toBe(300); + }); + }); + + describe('integratedTerminalShell', () => { + it('Should return the configured value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('/path/to/shell'); + + // Run + const value = config.integratedTerminalShell; + + expect(workspaceConfiguration.get).toBeCalledWith('integratedTerminalShell', ''); + expect(value).toBe('/path/to/shell'); + }); + + it('Should return the default configuration value ("")', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.integratedTerminalShell; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('integratedTerminalShell', ''); + expect(value).toBe(''); + }); + }); + + describe('loadMoreCommits', () => { + it('Should return the configured value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(200); + + // Run + const value = config.loadMoreCommits; + + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommits', 100); + expect(value).toBe(200); + }); + + it('Should return the default configuration value (100)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.loadMoreCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommits', 100); + expect(value).toBe(100); + }); + }); + + describe('loadMoreCommitsAutomatically', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.loadMoreCommitsAutomatically; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommitsAutomatically', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.loadMoreCommitsAutomatically; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommitsAutomatically', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.loadMoreCommitsAutomatically; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommitsAutomatically', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.loadMoreCommitsAutomatically; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommitsAutomatically', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.loadMoreCommitsAutomatically; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('loadMoreCommitsAutomatically', true); + expect(value).toBe(true); + }); + }); + + describe('muteCommitsThatAreNotAncestorsOfHead', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.muteCommitsThatAreNotAncestorsOfHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteCommitsThatAreNotAncestorsOfHead', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.muteCommitsThatAreNotAncestorsOfHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteCommitsThatAreNotAncestorsOfHead', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.muteCommitsThatAreNotAncestorsOfHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteCommitsThatAreNotAncestorsOfHead', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.muteCommitsThatAreNotAncestorsOfHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteCommitsThatAreNotAncestorsOfHead', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.muteCommitsThatAreNotAncestorsOfHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteCommitsThatAreNotAncestorsOfHead', false); + expect(value).toBe(false); + }); + }); + + describe('maxDepthOfRepoSearch', () => { + it('Should return the configured value', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.maxDepthOfRepoSearch; + + expect(workspaceConfiguration.get).toBeCalledWith('maxDepthOfRepoSearch', 0); + expect(value).toBe(5); + }); + + it('Should return the default configuration value (0)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.maxDepthOfRepoSearch; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('maxDepthOfRepoSearch', 0); + expect(value).toBe(0); + }); + }); + + describe('muteMergeCommits', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.muteMergeCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteMergeCommits', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.muteMergeCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteMergeCommits', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.muteMergeCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteMergeCommits', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.muteMergeCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteMergeCommits', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.muteMergeCommits; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('muteMergeCommits', true); + expect(value).toBe(true); + }); + }); + + describe('onlyFollowFirstParent', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.onlyFollowFirstParent; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('onlyFollowFirstParent', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.onlyFollowFirstParent; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('onlyFollowFirstParent', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.onlyFollowFirstParent; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('onlyFollowFirstParent', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.onlyFollowFirstParent; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('onlyFollowFirstParent', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.onlyFollowFirstParent; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('onlyFollowFirstParent', false); + expect(value).toBe(false); + }); + }); + + describe('openDiffTabLocation', () => { + it('Should return vscode.ViewColumn.Active when the configuration value is "Active"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Active'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Active); + }); + + it('Should return vscode.ViewColumn.Beside when the configuration value is "Beside"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Beside'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Beside); + }); + + it('Should return vscode.ViewColumn.One when the configuration value is "One"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('One'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.One); + }); + + it('Should return vscode.ViewColumn.Two when the configuration value is "Two"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Two'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Two); + }); + + it('Should return vscode.ViewColumn.Three when the configuration value is "Three"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Three'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Three); + }); + + it('Should return vscode.ViewColumn.Four when the configuration value is "Four"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Four'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Four); + }); + + it('Should return vscode.ViewColumn.Five when the configuration value is "Five"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Five'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Five); + }); + + it('Should return vscode.ViewColumn.Six when the configuration value is "Six"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Six'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Six); + }); + + it('Should return vscode.ViewColumn.Seven when the configuration value is "Seven"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Seven'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Seven); + }); + + it('Should return vscode.ViewColumn.Eight when the configuration value is "Eight"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Eight'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Eight); + }); + + it('Should return vscode.ViewColumn.Nine when the configuration value is "Nine"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Nine'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Nine); + }); + + it('Should return the default value (vscode.ViewColumn.Active) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Active); + }); + + it('Should return the default value (vscode.ViewColumn.Active) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.openDiffTabLocation; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openDiffTabLocation', 'Active'); + expect(value).toBe(vscode.ViewColumn.Active); + }); + }); + + describe('openRepoToHead', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.openRepoToHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openRepoToHead', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.openRepoToHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openRepoToHead', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.openRepoToHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openRepoToHead', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.openRepoToHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openRepoToHead', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.openRepoToHead; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openRepoToHead', false); + expect(value).toBe(false); + }); + }); + + describe('openToTheRepoOfTheActiveTextEditorDocument', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.openToTheRepoOfTheActiveTextEditorDocument; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openToTheRepoOfTheActiveTextEditorDocument', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.openToTheRepoOfTheActiveTextEditorDocument; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openToTheRepoOfTheActiveTextEditorDocument', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.openToTheRepoOfTheActiveTextEditorDocument; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openToTheRepoOfTheActiveTextEditorDocument', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.openToTheRepoOfTheActiveTextEditorDocument; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openToTheRepoOfTheActiveTextEditorDocument', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.openToTheRepoOfTheActiveTextEditorDocument; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('openToTheRepoOfTheActiveTextEditorDocument', false); + expect(value).toBe(false); + }); + }); + + describe('refLabelAlignment', () => { + it('Should return RefLabelAlignment.Normal when the configuration value is "Normal"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Normal'); + + // Run + const value = config.refLabelAlignment; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('referenceLabelAlignment', 'Normal'); + expect(value).toBe(RefLabelAlignment.Normal); + }); + + it('Should return RefLabelAlignment.BranchesOnLeftAndTagsOnRight when the configuration value is "Branches (on the left) & Tags (on the right)"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Branches (on the left) & Tags (on the right)'); + + // Run + const value = config.refLabelAlignment; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('referenceLabelAlignment', 'Normal'); + expect(value).toBe(RefLabelAlignment.BranchesOnLeftAndTagsOnRight); + }); + + it('Should return RefLabelAlignment.BranchesAlignedToGraphAndTagsOnRight when the configuration value is "Branches (aligned to the graph) & Tags (on the right)"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('Branches (aligned to the graph) & Tags (on the right)'); + + // Run + const value = config.refLabelAlignment; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('referenceLabelAlignment', 'Normal'); + expect(value).toBe(RefLabelAlignment.BranchesAlignedToGraphAndTagsOnRight); + }); + + it('Should return the default value (RefLabelAlignment.Normal) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.refLabelAlignment; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('referenceLabelAlignment', 'Normal'); + expect(value).toBe(RefLabelAlignment.Normal); + }); + + it('Should return the default value (RefLabelAlignment.Normal) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.refLabelAlignment; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('referenceLabelAlignment', 'Normal'); + expect(value).toBe(RefLabelAlignment.Normal); + }); + }); + + describe('retainContextWhenHidden', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.retainContextWhenHidden; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('retainContextWhenHidden', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.retainContextWhenHidden; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('retainContextWhenHidden', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.retainContextWhenHidden; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('retainContextWhenHidden', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.retainContextWhenHidden; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('retainContextWhenHidden', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.retainContextWhenHidden; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('retainContextWhenHidden', true); + expect(value).toBe(true); + }); + }); + + describe('showCommitsOnlyReferencedByTags', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showCommitsOnlyReferencedByTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCommitsOnlyReferencedByTags', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showCommitsOnlyReferencedByTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCommitsOnlyReferencedByTags', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showCommitsOnlyReferencedByTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCommitsOnlyReferencedByTags', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showCommitsOnlyReferencedByTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCommitsOnlyReferencedByTags', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showCommitsOnlyReferencedByTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCommitsOnlyReferencedByTags', true); + expect(value).toBe(true); + }); + }); + + describe('showCurrentBranchByDefault', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showCurrentBranchByDefault; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCurrentBranchByDefault', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showCurrentBranchByDefault; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCurrentBranchByDefault', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showCurrentBranchByDefault; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCurrentBranchByDefault', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showCurrentBranchByDefault; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCurrentBranchByDefault', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showCurrentBranchByDefault; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showCurrentBranchByDefault', false); + expect(value).toBe(false); + }); + }); + + describe('showSignatureStatus', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showSignatureStatus; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showSignatureStatus', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showSignatureStatus; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showSignatureStatus', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showSignatureStatus; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showSignatureStatus', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showSignatureStatus; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showSignatureStatus', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showSignatureStatus; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showSignatureStatus', false); + expect(value).toBe(false); + }); + }); + + describe('showStatusBarItem', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showStatusBarItem; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showStatusBarItem', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showStatusBarItem; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showStatusBarItem', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showStatusBarItem; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showStatusBarItem', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showStatusBarItem; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showStatusBarItem', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showStatusBarItem; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showStatusBarItem', true); + expect(value).toBe(true); + }); + }); + + describe('showTags', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showTags', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showTags', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showTags', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showTags', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showTags; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showTags', true); + expect(value).toBe(true); + }); + }); + + describe('showUncommittedChanges', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showUncommittedChanges; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUncommittedChanges', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showUncommittedChanges; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUncommittedChanges', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showUncommittedChanges; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUncommittedChanges', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showUncommittedChanges; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUncommittedChanges', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showUncommittedChanges; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUncommittedChanges', true); + expect(value).toBe(true); + }); + }); + + describe('showUntrackedFiles', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.showUntrackedFiles; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUntrackedFiles', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.showUntrackedFiles; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUntrackedFiles', true); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.showUntrackedFiles; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUntrackedFiles', true); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.showUntrackedFiles; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUntrackedFiles', true); + expect(value).toBe(false); + }); + + it('Should return the default value (TRUE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.showUntrackedFiles; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('showUntrackedFiles', true); + expect(value).toBe(true); + }); + }); + + describe('tabIconColourTheme', () => { + it('Should return TabIconColourTheme.Colour when the configuration value is "colour"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('colour'); + + // Run + const value = config.tabIconColourTheme; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('tabIconColourTheme', 'colour'); + expect(value).toBe(TabIconColourTheme.Colour); + }); + + it('Should return TabIconColourTheme.Grey when the configuration value is "grey"', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('grey'); + + // Run + const value = config.tabIconColourTheme; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('tabIconColourTheme', 'colour'); + expect(value).toBe(TabIconColourTheme.Grey); + }); + + it('Should return the default value (TabIconColourTheme.Colour) when the configuration value is invalid', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('invalid'); + + // Run + const value = config.tabIconColourTheme; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('tabIconColourTheme', 'colour'); + expect(value).toBe(TabIconColourTheme.Colour); + }); + + it('Should return the default value (TabIconColourTheme.Colour) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.tabIconColourTheme; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('tabIconColourTheme', 'colour'); + expect(value).toBe(TabIconColourTheme.Colour); + }); + }); + + describe('useMailmap', () => { + it('Should return TRUE when the configuration value is TRUE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const value = config.useMailmap; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('useMailmap', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is FALSE', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const value = config.useMailmap; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('useMailmap', false); + expect(value).toBe(false); + }); + + it('Should return TRUE when the configuration value is truthy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(5); + + // Run + const value = config.useMailmap; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('useMailmap', false); + expect(value).toBe(true); + }); + + it('Should return FALSE when the configuration value is falsy', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(0); + + // Run + const value = config.useMailmap; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('useMailmap', false); + expect(value).toBe(false); + }); + + it('Should return the default value (FALSE) when the configuration value is not set', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.useMailmap; + + // Assert + expect(workspaceConfiguration.get).toBeCalledWith('useMailmap', false); + expect(value).toBe(false); + }); + }); + + describe('gitPath', () => { + it('Should return the configured path', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('/path/to/git'); + + // Run + const value = config.gitPath; + + // Assert + expect(vscode.workspace.getConfiguration).toBeCalledWith('git'); + expect(workspaceConfiguration.get).toBeCalledWith('path', null); + expect(value).toBe('/path/to/git'); + }); + + it('Should return NULL when the configuration value is NULL', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(null); + + // Run + const value = config.gitPath; + + // Assert + expect(vscode.workspace.getConfiguration).toBeCalledWith('git'); + expect(workspaceConfiguration.get).toBeCalledWith('path', null); + expect(value).toBe(null); + }); + + it('Should return the default configuration value (NULL)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const value = config.gitPath; + + // Assert + expect(vscode.workspace.getConfiguration).toBeCalledWith('git'); + expect(workspaceConfiguration.get).toBeCalledWith('path', null); + expect(value).toBe(null); + }); + }); +}); diff --git a/tests/diffDocProvider.test.ts b/tests/diffDocProvider.test.ts new file mode 100644 index 00000000..6eeaa877 --- /dev/null +++ b/tests/diffDocProvider.test.ts @@ -0,0 +1,222 @@ +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('../src/dataSource'); +jest.mock('../src/logger'); + +import * as path from 'path'; +import { ConfigurationChangeEvent } from 'vscode'; +import { DataSource } from '../src/dataSource'; +import { decodeDiffDocUri, DiffDocProvider, DiffSide, encodeDiffDocUri } from '../src/diffDocProvider'; +import { EventEmitter } from '../src/event'; +import { Logger } from '../src/logger'; +import { GitFileStatus } from '../src/types'; +import { GitExecutable, UNCOMMITTED } from '../src/utils'; + +let onDidChangeConfiguration: EventEmitter; +let onDidChangeGitExecutable: EventEmitter; +let logger: Logger; +let dataSource: DataSource; + +beforeAll(() => { + onDidChangeConfiguration = new EventEmitter(); + onDidChangeGitExecutable = new EventEmitter(); + logger = new Logger(); + dataSource = new DataSource(null, onDidChangeConfiguration.subscribe, onDidChangeGitExecutable.subscribe, logger); +}); + +afterAll(() => { + dataSource.dispose(); + logger.dispose(); + onDidChangeConfiguration.dispose(); + onDidChangeGitExecutable.dispose(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('DiffDocProvider', () => { + it('Should construct a DiffDocProvider, provide a document, and be disposed', async () => { + // Setup + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Modified, DiffSide.New); + jest.spyOn(dataSource, 'getCommitFile').mockResolvedValueOnce('file-contents'); + + // Run + const diffDocProvider = new DiffDocProvider(dataSource); + const docContents = await diffDocProvider.provideTextDocumentContent(uri); + + // Assert + expect(docContents).toBe('file-contents'); + expect(diffDocProvider['docs'].size).toBe(1); + expect(diffDocProvider.onDidChange).toBeTruthy(); + + // Run + diffDocProvider.dispose(); + + // Assert + expect(diffDocProvider['closeDocSubscription'].dispose).toHaveBeenCalled(); + expect(diffDocProvider['docs'].size).toBe(0); + expect(diffDocProvider['onDidChangeEventEmitter'].dispose).toHaveBeenCalled(); + }); + + it('Should remove a cached document once it is closed', async () => { + // Setup + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Modified, DiffSide.New); + jest.spyOn(dataSource, 'getCommitFile').mockResolvedValueOnce('file-contents'); + + let closeTextDocument: (doc: { uri: vscode.Uri }) => void; + vscode.workspace.onDidCloseTextDocument.mockImplementationOnce((callback: (_: { uri: vscode.Uri }) => void) => { + closeTextDocument = callback; + return { dispose: jest.fn() }; + }); + + // Run + const diffDocProvider = new DiffDocProvider(dataSource); + const docContents = await diffDocProvider.provideTextDocumentContent(uri); + + // Assert + expect(docContents).toBe('file-contents'); + expect(diffDocProvider['docs'].size).toBe(1); + + // Run + closeTextDocument!({ uri: uri }); + + // Assert + expect(diffDocProvider['docs'].size).toBe(0); + + // Teardown + diffDocProvider.dispose(); + }); + + it('Should reuse a cached document if it exists', async () => { + // Setup + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Modified, DiffSide.New); + const spyOnGetCommitFile = jest.spyOn(dataSource, 'getCommitFile'); + spyOnGetCommitFile.mockResolvedValueOnce('file-contents'); + + // Run + const diffDocProvider = new DiffDocProvider(dataSource); + const docContents1 = await diffDocProvider.provideTextDocumentContent(uri); + const docContents2 = await diffDocProvider.provideTextDocumentContent(uri); + + // Assert + expect(docContents1).toBe('file-contents'); + expect(docContents2).toBe('file-contents'); + expect(spyOnGetCommitFile).toHaveBeenCalledTimes(1); + + // Teardown + diffDocProvider.dispose(); + }); + + it('Should return an empty document if requested', async () => { + // Setup + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', UNCOMMITTED, GitFileStatus.Deleted, DiffSide.New); + + // Run + const diffDocProvider = new DiffDocProvider(dataSource); + const docContents = await diffDocProvider.provideTextDocumentContent(uri); + + // Assert + expect(docContents).toBe(''); + + // Teardown + diffDocProvider.dispose(); + }); + + it('Should display an error message if an error occurred when fetching the file contents from the DataSource, and return an empty document', async () => { + // Setup + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Modified, DiffSide.New); + jest.spyOn(dataSource, 'getCommitFile').mockRejectedValueOnce('error-message'); + vscode.window.showErrorMessage.mockResolvedValue(null); + + // Run + const diffDocProvider = new DiffDocProvider(dataSource); + const docContents = await diffDocProvider.provideTextDocumentContent(uri); + + // Assert + expect(docContents).toBe(''); + expect(vscode.window.showErrorMessage).toBeCalledWith('Unable to retrieve file: error-message'); + + // Teardown + diffDocProvider.dispose(); + }); +}); + +describe('encodeDiffDocUri', () => { + it('Should return a file URI if requested on uncommitted changes and it is not deleted', () => { + // Run + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', UNCOMMITTED, GitFileStatus.Added, DiffSide.New); + + // Assert + expect(uri.scheme).toBe('file'); + expect(uri.fsPath).toBe(path.join('/repo', 'path/to/file.txt')); + }); + + it('Should return an empty file URI if requested on a file displayed on the old side of the diff, and it is added', () => { + // Run + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Added, DiffSide.Old); + + // Assert + expect(uri.scheme).toBe('git-graph'); + expect(uri.fsPath).toBe('file'); + expect(uri.query).toBe('bnVsbA=='); + }); + + it('Should return an empty file URI if requested on a file displayed on the new side of the diff, and it is deleted', () => { + // Run + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Deleted, DiffSide.New); + + // Assert + expect(uri.scheme).toBe('git-graph'); + expect(uri.fsPath).toBe('file'); + expect(uri.query).toBe('bnVsbA=='); + }); + + it('Should return a git-graph URI with the provided file extension', () => { + // Run + const uri = encodeDiffDocUri('/repo', 'path/to/file.txt', 'abcdef', GitFileStatus.Modified, DiffSide.New); + + // Assert + expect(uri.scheme).toBe('git-graph'); + expect(uri.fsPath).toBe('file.txt'); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiJhYmNkZWYiLCJyZXBvIjoiL3JlcG8ifQ=='); + }); + + it('Should return a git-graph URI with no file extension when it is not provided', () => { + // Run + const uri = encodeDiffDocUri('/repo', 'path/to/file', 'abcdef', GitFileStatus.Modified, DiffSide.New); + + // Assert + expect(uri.scheme).toBe('git-graph'); + expect(uri.fsPath).toBe('file'); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZSIsImNvbW1pdCI6ImFiY2RlZiIsInJlcG8iOiIvcmVwbyJ9'); + }); +}); + +describe('decodeDiffDocUri', () => { + it('Should return an null if requested on an empty file URI', () => { + // Run + const value = decodeDiffDocUri(vscode.Uri.file('file').with({ + scheme: 'git-graph', + query: 'bnVsbA==' + })); + + // Assert + expect(value).toBe(null); + }); + + it('Should return the parse DiffDocUriData if requested on a git-graph URI', () => { + // Run + const value = decodeDiffDocUri(vscode.Uri.file('file.txt').with({ + scheme: 'git-graph', + query: 'eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiJhYmNkZWYiLCJyZXBvIjoiL3JlcG8ifQ==' + })); + + // Assert + expect(value).toStrictEqual({ + filePath: 'path/to/file.txt', + commit: 'abcdef', + repo: '/repo' + }); + }); +}); diff --git a/tests/event.test.ts b/tests/event.test.ts index a4078041..92e04935 100644 --- a/tests/event.test.ts +++ b/tests/event.test.ts @@ -75,10 +75,8 @@ describe('Event Emitter', () => { emitter.emit(5); // Assert - expect(mockSubscriber1.mock.calls.length).toBe(1); - expect(mockSubscriber1.mock.calls[0][0]).toBe(5); - expect(mockSubscriber2.mock.calls.length).toBe(1); - expect(mockSubscriber2.mock.calls[0][0]).toBe(5); + expect(mockSubscriber1).toHaveBeenCalledWith(5); + expect(mockSubscriber2).toHaveBeenCalledWith(5); // Teardown emitter.dispose(); diff --git a/tests/extensionState.test.ts b/tests/extensionState.test.ts new file mode 100644 index 00000000..6613ddbe --- /dev/null +++ b/tests/extensionState.test.ts @@ -0,0 +1,1088 @@ +import './mocks/date'; +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('fs'); + +import * as fs from 'fs'; +import { EventEmitter } from '../src/event'; +import { ExtensionState } from '../src/extensionState'; +import { FileViewType, GitGraphViewGlobalState, IncludeCommitsMentionedByReflogs, OnlyFollowFirstParent, RepoCommitOrdering, ShowTags } from '../src/types'; +import { GitExecutable } from '../src/utils'; + +let extensionContext = vscode.mocks.extensionContext; +let onDidChangeGitExecutable: EventEmitter; + +beforeAll(() => { + onDidChangeGitExecutable = new EventEmitter(); +}); + +afterAll(() => { + onDidChangeGitExecutable.dispose(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('ExtensionState', () => { + let extensionState: ExtensionState; + beforeEach(() => { + extensionState = new ExtensionState(extensionContext, onDidChangeGitExecutable.subscribe); + }); + afterEach(() => { + extensionState.dispose(); + }); + + describe('GitExecutable Change Event Processing', () => { + it('Should subscribe to GitExecutable change events', () => { + // Assert + expect(onDidChangeGitExecutable['listeners']).toHaveLength(1); + }); + + it('Should unsubscribe from GitExecutable change events after disposal', () => { + // Run + extensionState.dispose(); + + // Assert + expect(onDidChangeGitExecutable['listeners']).toHaveLength(0); + }); + + it('Should save the last known git executable path received from GitExecutable change events', () => { + // Run + onDidChangeGitExecutable.emit({ path: '/path/to/git', version: '1.2.3' }); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('lastKnownGitPath', '/path/to/git'); + }); + }); + + describe('getRepos', () => { + it('Should return the stored repositories', () => { + // Setup + const repoState = { + columnWidths: null, + cdvDivider: 0.5, + cdvHeight: 250, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + includeCommitsMentionedByReflogs: IncludeCommitsMentionedByReflogs.Enabled, + onlyFollowFirstParent: OnlyFollowFirstParent.Disabled, + issueLinkingConfig: null, + pullRequestConfig: null, + showRemoteBranches: true, + showTags: ShowTags.Show, + hideRemotes: [] + }; + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': repoState + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': repoState + }); + }); + + it('Should assign missing repository state variables to their default values', () => { + // Setup + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': { + columnWidths: null, + hideRemotes: [] + } + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': { + columnWidths: null, + cdvDivider: 0.5, + cdvHeight: 250, + commitOrdering: RepoCommitOrdering.Default, + fileViewType: FileViewType.Default, + includeCommitsMentionedByReflogs: IncludeCommitsMentionedByReflogs.Default, + onlyFollowFirstParent: OnlyFollowFirstParent.Default, + issueLinkingConfig: null, + pullRequestConfig: null, + showRemoteBranches: true, + showTags: ShowTags.Default, + hideRemotes: [] + } + }); + }); + + it('Should return the default value if it is not defined', () => { + // Setup + extensionContext.workspaceState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({}); + }); + }); + + describe('saveRepos', () => { + it('Should store the provided repositories in the workspace state', () => { + // Setup + const repos = {}; + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.saveRepos(repos); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('repoStates', repos); + }); + }); + + describe('transferRepo', () => { + it('Should update the last active repo and code reviews with the new repository path', () => { + // Setup + extensionContext.workspaceState.get.mockReturnValueOnce('/path/to/repo'); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.transferRepo('/path/to/repo', '/new/path/to/repo'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenNthCalledWith(1, 'lastActiveRepo', '/new/path/to/repo'); + expect(extensionContext.workspaceState.update).toHaveBeenNthCalledWith(2, 'codeReviews', { + '/new/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + }); + + it('Shouldn\'t update the last active repo or code reviews when no match is found with the transfer repository', () => { + // Setup + extensionContext.workspaceState.get.mockReturnValueOnce('/path/to/repo'); + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + + // Run + extensionState.transferRepo('/path/to/repo1', '/new/path/to/repo'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledTimes(0); + }); + }); + + describe('getGlobalViewState', () => { + it('Should return the stored global view state', () => { + // Setup + const globalViewState = { + alwaysAcceptCheckoutCommit: true, + issueLinkingConfig: null + }; + extensionContext.globalState.get.mockReturnValueOnce(globalViewState); + + // Run + const result = extensionState.getGlobalViewState(); + + // Assert + expect(result).toStrictEqual(globalViewState); + }); + + it('Should assign missing global view state variables to their default values', () => { + // Setup + extensionContext.globalState.get.mockReturnValueOnce({ + issueLinkingConfig: null + }); + + // Run + const result = extensionState.getGlobalViewState(); + + // Assert + expect(result).toStrictEqual({ + alwaysAcceptCheckoutCommit: false, + issueLinkingConfig: null + }); + }); + + it('Should return the default global view state if it is not defined', () => { + // Setup + extensionContext.globalState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getGlobalViewState(); + + // Assert + expect(result).toStrictEqual({ + alwaysAcceptCheckoutCommit: false, + issueLinkingConfig: null + }); + }); + }); + + describe('setGlobalViewState', () => { + it('Should successfully store the global view state', async () => { + // Setup + const globalViewState = {} as GitGraphViewGlobalState; + extensionContext.globalState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.setGlobalViewState(globalViewState); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('globalViewState', globalViewState); + expect(result).toBe(null); + }); + + it('Should return an error message when vscode is unable to store the global view state', async () => { + // Setup + const globalViewState = {} as GitGraphViewGlobalState; + extensionContext.globalState.update.mockRejectedValueOnce(null); + + // Run + const result = await extensionState.setGlobalViewState(globalViewState); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('globalViewState', globalViewState); + expect(result).toBe('Visual Studio Code was unable to save the Git Graph Global State Memento.'); + }); + }); + + describe('getIgnoredRepos', () => { + it('Should return the stored ignored repositories', () => { + // Setup + const ignoredRepos = ['/ignored-repo1']; + extensionContext.workspaceState.get.mockReturnValueOnce(ignoredRepos); + + // Run + const result = extensionState.getIgnoredRepos(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('ignoredRepos', []); + expect(result).toBe(ignoredRepos); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.workspaceState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getIgnoredRepos(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('ignoredRepos', []); + expect(result).toStrictEqual([]); + }); + }); + + describe('setIgnoredRepos', () => { + it('Should successfully store the ignored repositories', async () => { + // Setup + const ignoreRepos = ['/path/to/ignore']; + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.setIgnoredRepos(ignoreRepos); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('ignoredRepos', ignoreRepos); + expect(result).toBe(null); + }); + + it('Should return an error message when vscode is unable to store the ignored repositories', async () => { + // Setup + const ignoreRepos = ['/path/to/ignore']; + extensionContext.workspaceState.update.mockRejectedValueOnce(null); + + // Run + const result = await extensionState.setIgnoredRepos(ignoreRepos); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('ignoredRepos', ignoreRepos); + expect(result).toBe('Visual Studio Code was unable to save the Git Graph Workspace State Memento.'); + }); + }); + + describe('getLastActiveRepo', () => { + it('Should return the stored last active repository', () => { + // Setup + extensionContext.workspaceState.get.mockReturnValueOnce('/last/active/repo'); + + // Run + const result = extensionState.getLastActiveRepo(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('lastActiveRepo', null); + expect(result).toBe('/last/active/repo'); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.workspaceState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getLastActiveRepo(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('lastActiveRepo', null); + expect(result).toBe(null); + }); + }); + + describe('setLastActiveRepo', () => { + it('Should store the last active repository', () => { + // Setup + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.setLastActiveRepo('/path/to/repo'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('lastActiveRepo', '/path/to/repo'); + }); + }); + + describe('getLastKnownGitPath', () => { + it('Should return the stored last active repository', () => { + // Setup + extensionContext.globalState.get.mockReturnValueOnce('/path/to/git'); + + // Run + const result = extensionState.getLastKnownGitPath(); + + // Assert + expect(extensionContext.globalState.get).toHaveBeenCalledWith('lastKnownGitPath', null); + expect(result).toBe('/path/to/git'); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.globalState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getLastKnownGitPath(); + + // Assert + expect(extensionContext.globalState.get).toHaveBeenCalledWith('lastKnownGitPath', null); + expect(result).toBe(null); + }); + }); + + describe('isAvatarStorageAvailable', () => { + it('Should return TRUE if the avatar storage folder existed on startup', () => { + // Setup + const spyOnStat = jest.spyOn(fs, 'stat'); + spyOnStat.mockImplementationOnce((_, callback) => callback(null, {} as fs.Stats)); + const extensionState = new ExtensionState(extensionContext, onDidChangeGitExecutable.subscribe); + + // Run + const result = extensionState.isAvatarStorageAvailable(); + + // Assert + expect(spyOnStat.mock.calls[0][0]).toBe('/path/to/globalStorage/avatars'); + expect(result).toBe(true); + + // Teardown + extensionState.dispose(); + }); + + it('Should return TRUE if the avatar storage folder was successfully created', () => { + // Setup + jest.spyOn(fs, 'stat').mockImplementationOnce((_, callback) => callback(new Error(), {} as fs.Stats)); + const spyOnMkdir = jest.spyOn(fs, 'mkdir'); + spyOnMkdir.mockImplementation((_, callback) => callback(null)); + const extensionState = new ExtensionState(extensionContext, onDidChangeGitExecutable.subscribe); + + // Run + const result = extensionState.isAvatarStorageAvailable(); + + // Assert + expect(spyOnMkdir.mock.calls[0][0]).toBe('/path/to/globalStorage'); + expect(spyOnMkdir.mock.calls[1][0]).toBe('/path/to/globalStorage/avatars'); + expect(result).toBe(true); + + // Teardown + extensionState.dispose(); + }); + + it('Should return TRUE if the avatar storage folder was created after the initial stat check', () => { + // Setup + jest.spyOn(fs, 'stat').mockImplementationOnce((_, callback) => callback(new Error(), {} as fs.Stats)); + const spyOnMkdir = jest.spyOn(fs, 'mkdir'); + spyOnMkdir.mockImplementation((_, callback) => callback({ code: 'EEXIST' } as NodeJS.ErrnoException)); + const extensionState = new ExtensionState(extensionContext, onDidChangeGitExecutable.subscribe); + + // Run + const result = extensionState.isAvatarStorageAvailable(); + + // Assert + expect(spyOnMkdir.mock.calls[0][0]).toBe('/path/to/globalStorage'); + expect(spyOnMkdir.mock.calls[1][0]).toBe('/path/to/globalStorage/avatars'); + expect(result).toBe(true); + + // Teardown + extensionState.dispose(); + }); + + it('Should return FALSE if the avatar storage folder could not be created', () => { + // Setup + jest.spyOn(fs, 'stat').mockImplementationOnce((_, callback) => callback(new Error(), {} as fs.Stats)); + const spyOnMkdir = jest.spyOn(fs, 'mkdir'); + spyOnMkdir.mockImplementation((_, callback) => callback({} as NodeJS.ErrnoException)); + const extensionState = new ExtensionState(extensionContext, onDidChangeGitExecutable.subscribe); + + // Run + const result = extensionState.isAvatarStorageAvailable(); + + // Assert + expect(spyOnMkdir.mock.calls[0][0]).toBe('/path/to/globalStorage'); + expect(spyOnMkdir.mock.calls[1][0]).toBe('/path/to/globalStorage/avatars'); + expect(result).toBe(false); + + // Teardown + extensionState.dispose(); + }); + }); + + describe('getAvatarStoragePath', () => { + it('Should return the avatar storage path', () => { + // Run + const result = extensionState.getAvatarStoragePath(); + + // Assert + expect(result).toBe('/path/to/globalStorage/avatars'); + }); + }); + + describe('getAvatarCache', () => { + it('Should return the stored avatar cache', () => { + // Setup + const cache = {}; + extensionContext.globalState.get.mockReturnValueOnce(cache); + + // Run + const result = extensionState.getAvatarCache(); + + // Assert + expect(extensionContext.globalState.get).toHaveBeenCalledWith('avatarCache', {}); + expect(result).toBe(cache); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.globalState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getAvatarCache(); + + // Assert + expect(extensionContext.globalState.get).toHaveBeenCalledWith('avatarCache', {}); + expect(result).toStrictEqual({}); + }); + }); + + describe('saveAvatar', () => { + it('Should save the avatar to the avatar cache', () => { + // Setup + const avatar = { image: 'name.jpg', timestamp: 0, identicon: false }; + extensionContext.globalState.get.mockReturnValueOnce({}); + extensionContext.globalState.update.mockResolvedValueOnce(null); + + // Run + extensionState.saveAvatar('test@example.com', avatar); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('avatarCache', { 'test@example.com': avatar }); + }); + }); + + describe('removeAvatarFromCache', () => { + it('Should remove an avatar from the cache', () => { + // Setup + const avatar = { image: 'name.jpg', timestamp: 0, identicon: false }; + extensionContext.globalState.get.mockReturnValueOnce({ + 'test1@example.com': avatar, + 'test2@example.com': avatar + }); + extensionContext.globalState.update.mockResolvedValueOnce(null); + + // Run + extensionState.removeAvatarFromCache('test1@example.com'); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('avatarCache', { 'test2@example.com': avatar }); + }); + }); + + describe('clearAvatarCache', () => { + it('Should clear all avatars from the cache and delete all avatars that are currently stored on the file system', () => { + extensionContext.globalState.update.mockResolvedValueOnce(null); + const spyOnReaddir = jest.spyOn(fs, 'readdir'); + spyOnReaddir.mockImplementationOnce((_, callback) => callback(null, ['file1.jpg', 'file2.jpg'])); + const spyOnUnlink = jest.spyOn(fs, 'unlink'); + spyOnUnlink.mockImplementation((_, callback) => callback(null)); + + // Run + extensionState.clearAvatarCache(); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('avatarCache', {}); + expect(spyOnReaddir).toHaveBeenCalledTimes(1); + expect(spyOnReaddir.mock.calls[0][0]).toBe('/path/to/globalStorage/avatars'); + expect(spyOnUnlink).toHaveBeenCalledTimes(2); + expect(spyOnUnlink.mock.calls[0][0]).toBe('/path/to/globalStorage/avatars/file1.jpg'); + expect(spyOnUnlink.mock.calls[1][0]).toBe('/path/to/globalStorage/avatars/file2.jpg'); + }); + + it('Should skip deleting avatars on the file system if they could not be listed from the file system', () => { + extensionContext.globalState.update.mockResolvedValueOnce(null); + const spyOnReaddir = jest.spyOn(fs, 'readdir'); + spyOnReaddir.mockImplementationOnce((_, callback) => callback(new Error(), ['file1.jpg', 'file2.jpg'])); + const spyOnUnlink = jest.spyOn(fs, 'unlink'); + spyOnUnlink.mockImplementation((_, callback) => callback(null)); + + // Run + extensionState.clearAvatarCache(); + + // Assert + expect(extensionContext.globalState.update).toHaveBeenCalledWith('avatarCache', {}); + expect(spyOnReaddir).toHaveBeenCalledTimes(1); + expect(spyOnReaddir.mock.calls[0][0]).toBe('/path/to/globalStorage/avatars'); + expect(spyOnUnlink).toHaveBeenCalledTimes(0); + }); + }); + + describe('startCodeReview', () => { + it('Should store the code review (in a repository with no prior code reviews)', async () => { + // Setup + const codeReviews = {}; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.startCodeReview('/path/to/repo', 'abcdef', ['file2.txt', 'file3.txt'], 'file1.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + expect(result).toStrictEqual({ + codeReview: { + id: 'abcdef', + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }, + error: null + }); + }); + + it('Should store the code review (in a repository with a prior code review)', async () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.startCodeReview('/path/to/repo', '123456', ['file5.txt', 'file6.txt'], 'file4.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }, + '123456': { + lastActive: 1587559258000, + lastViewedFile: 'file4.txt', + remainingFiles: ['file5.txt', 'file6.txt'] + } + } + }); + expect(result).toStrictEqual({ + codeReview: { + id: '123456', + lastActive: 1587559258000, + lastViewedFile: 'file4.txt', + remainingFiles: ['file5.txt', 'file6.txt'] + }, + error: null + }); + }); + + it('Should return an error message when vscode is unable to store the code reviews', async () => { + // Setup + const codeReviews = {}; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockRejectedValueOnce(null); + + // Run + const result = await extensionState.startCodeReview('/path/to/repo', 'abcdef', ['file2.txt', 'file3.txt'], 'file1.txt'); + + // Assert + expect(result.error).toBe('Visual Studio Code was unable to save the Git Graph Workspace State Memento.'); + }); + }); + + describe('endCodeReview', () => { + it('Should store the updated code reviews, without the code review that was ended (no more code reviews in repo)', async () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.endCodeReview('/path/to/repo', 'abcdef'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', {}); + expect(result).toBe(null); + }); + + it('Should store the updated code reviews, without the code review that was ended (more code reviews in repo)', async () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }, + '123456': { + lastActive: 1587559258000, + lastViewedFile: 'file4.txt', + remainingFiles: ['file5.txt', 'file6.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.endCodeReview('/path/to/repo', 'abcdef'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + '123456': { + lastActive: 1587559258000, + lastViewedFile: 'file4.txt', + remainingFiles: ['file5.txt', 'file6.txt'] + } + } + }); + expect(result).toBe(null); + }); + + it('Should not make changes to the stored code reviews if the code review that was ended no longer exists', async () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = await extensionState.endCodeReview('/path/to/repo', '123456'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + expect(result).toBe(null); + }); + + it('Should return an error message when vscode is unable to store the code reviews', async () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockRejectedValueOnce(null); + + // Run + const result = await extensionState.endCodeReview('/path/to/repo', 'abcdef'); + + // Assert + expect(result).toBe('Visual Studio Code was unable to save the Git Graph Workspace State Memento.'); + }); + }); + + describe('getCodeReview', () => { + it('Should return the code review, and update its last active timestamp', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + const result = extensionState.getCodeReview('/path/to/repo', 'abcdef'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + expect(result).toStrictEqual({ + id: 'abcdef', + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }); + }); + + it('Should return NULL if no code review could be found in the specified repository', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + const result = extensionState.getCodeReview('/path/to/repo1', 'abcdef'); + + // Assert + expect(result).toBe(null); + }); + + it('Should return NULL if no code review could be found with the specified id', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + const result = extensionState.getCodeReview('/path/to/repo', '123456'); + + // Assert + expect(result).toBe(null); + }); + }); + + describe('updateCodeReviewFileReviewed', () => { + it('Should remove the reviewed file, set it as the last viewed file, and update the last active time', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.updateCodeReviewFileReviewed('/path/to/repo', 'abcdef', 'file2.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file2.txt', + remainingFiles: ['file3.txt'] + } + } + }); + }); + + it('Should ignore removing reviewed files if it has already be stored as reviewed', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.updateCodeReviewFileReviewed('/path/to/repo', 'abcdef', 'file2.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file2.txt', + remainingFiles: ['file3.txt'] + } + } + }); + }); + + it('Should remove the code review the last file in it has been reviewed', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559257000, + lastViewedFile: 'file2.txt', + remainingFiles: ['file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.updateCodeReviewFileReviewed('/path/to/repo', 'abcdef', 'file3.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', {}); + }); + + it('Shouldn\'t change the state if no code review could be found in the specified repository', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + extensionState.updateCodeReviewFileReviewed('/path/to/repo1', 'abcdef', 'file2.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledTimes(0); + }); + + it('Shouldn\'t change the state if no code review could be found with the specified id', () => { + // Setup + const codeReviews = { + '/path/to/repo': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + extensionState.updateCodeReviewFileReviewed('/path/to/repo', '123456', 'file2.txt'); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledTimes(0); + }); + }); + + describe('expireOldCodeReviews', () => { + it('Should delete all code reviews that have expired', () => { + // Setup + const codeReviews = { + '/path/to/repo1': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }, + '123456': { + lastActive: 0, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + }, + '/path/to/repo2': { + 'abcdef': { + lastActive: 0, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.expireOldCodeReviews(); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', { + '/path/to/repo1': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }); + }); + + it('Shouldn\'t make any changes when no repositories have expired', () => { + // Setup + const codeReviews = { + '/path/to/repo1': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + }, + '123456': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + }, + '/path/to/repo2': { + 'abcdef': { + lastActive: 1587559258000, + lastViewedFile: 'file1.txt', + remainingFiles: ['file2.txt', 'file3.txt'] + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + extensionState.expireOldCodeReviews(); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledTimes(0); + }); + }); + + describe('endAllWorkspaceCodeReviews', () => { + it('Should store the last active repository', () => { + // Setup + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.endAllWorkspaceCodeReviews(); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('codeReviews', {}); + }); + }); + + describe('getCodeReviews', () => { + it('Should return the stored code reviews', () => { + // Setup + const codeReviews = {}; + extensionContext.workspaceState.get.mockReturnValueOnce(codeReviews); + + // Run + const result = extensionState.getCodeReviews(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('codeReviews', {}); + expect(result).toBe(codeReviews); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.workspaceState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getCodeReviews(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('codeReviews', {}); + expect(result).toStrictEqual({}); + }); + }); +}); diff --git a/tests/logger.test.ts b/tests/logger.test.ts index 5f3d9d18..7560603e 100644 --- a/tests/logger.test.ts +++ b/tests/logger.test.ts @@ -4,86 +4,58 @@ jest.mock('vscode', () => vscode, { virtual: true }); import { Logger, maskEmail } from '../src/logger'; +let outputChannel = vscode.mocks.outputChannel; + beforeEach(() => { jest.clearAllMocks(); - date.beforeEach(); -}); - -afterEach(() => { - date.afterEach(); }); describe('Logger', () => { - it('Should create an output channel, and dispose it on dispose', () => { - // Run - const logger = new Logger(); - - // Assert - expect(vscode.window.createOutputChannel).toHaveBeenCalledWith('Git Graph'); - - // Run + let logger: Logger; + beforeEach(() => { + logger = new Logger(); + }); + afterEach(() => { logger.dispose(); - - // Assert - expect(vscode.OutputChannel.dispose).toBeCalledTimes(1); }); - it('Should create an output channel, and dispose it on dispose', () => { - // Run - const logger = new Logger(); - - // Assert - expect(vscode.window.createOutputChannel).toHaveBeenCalledWith('Git Graph'); - + it('Should create and dispose an output channel', () => { // Run logger.dispose(); // Assert - expect(vscode.OutputChannel.dispose).toBeCalledTimes(1); + expect(vscode.window.createOutputChannel).toHaveBeenCalledWith('Git Graph'); + expect(outputChannel.dispose).toBeCalledTimes(1); }); - it('Should log message to the Output Channel', () => { - // Setup - const logger = new Logger(); - + it('Should log a message to the Output Channel', () => { // Run logger.log('Test'); // Assert - expect(vscode.OutputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.000] Test'); - - // Teardown - logger.dispose(); + expect(outputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.000] Test'); }); - it('Should log command to the Output Channel', () => { + it('Should log a command to the Output Channel', () => { // Setup - const logger = new Logger(); date.setCurrentTime(1587559258.1); // Run logger.logCmd('git', ['--arg1', '--arg2', '--format="format-string"', '--arg3', 'arg with spaces']); // Assert - expect(vscode.OutputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.100] > git --arg1 --arg2 --format=... --arg3 "arg with spaces"'); - - // Teardown - logger.dispose(); + expect(outputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.100] > git --arg1 --arg2 --format=... --arg3 "arg with spaces"'); }); - it('Should log error to the Output Channel', () => { + it('Should log an error to the Output Channel', () => { // Setup - const logger = new Logger(); date.setCurrentTime(1587559258.01); // Run logger.logError('Test'); // Assert - expect(vscode.OutputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.010] ERROR: Test'); - - // Teardown - logger.dispose(); + expect(outputChannel.appendLine).toHaveBeenCalledWith('[2020-04-22 12:40:58.010] ERROR: Test'); }); }); diff --git a/tests/mocks/date.ts b/tests/mocks/date.ts index 7ce92c5b..c286c444 100644 --- a/tests/mocks/date.ts +++ b/tests/mocks/date.ts @@ -37,7 +37,7 @@ class MockDate extends RealDate { } } -export function beforeEach() { +beforeEach(() => { // Reset now to its initial value now = InitialNow; @@ -48,11 +48,11 @@ export function beforeEach() { return new MockDate(now * 1000); } } as DateConstructor; -} +}); -export function afterEach() { +afterEach(() => { Date = RealDate; -} +}); export function setCurrentTime(newNow: number) { now = newNow; diff --git a/tests/mocks/vscode.ts b/tests/mocks/vscode.ts index 12ace4cc..b0e2d38d 100644 --- a/tests/mocks/vscode.ts +++ b/tests/mocks/vscode.ts @@ -7,22 +7,131 @@ export const commands = { export const env = { clipboard: { writeText: jest.fn() - } + }, + openExternal: jest.fn() }; -export const Uri = { - file: jest.fn((path) => ({ fsPath: path } as vscode.Uri)) -}; +export const EventEmitter = jest.fn(() => ({ + dispose: jest.fn(), + event: jest.fn() +})); -export const ViewColumn = {}; +export class Uri implements vscode.Uri { + public readonly scheme: string; + public readonly authority: string; + public readonly path: string; + public readonly query: string; + public readonly fragment: string; -export const OutputChannel = { - dispose: jest.fn(), - appendLine: jest.fn() -}; + protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string) { + this.scheme = scheme; + this.authority = authority || ''; + this.path = path || ''; + this.query = query || ''; + this.fragment = fragment || ''; + } + + get fsPath() { + return this.path; + } + + public with(change: { scheme?: string | undefined; authority?: string | undefined; path?: string | undefined; query?: string | undefined; fragment?: string | undefined; }): vscode.Uri { + return new Uri(change.scheme || this.scheme, change.authority || this.authority, change.path || this.path, change.query || this.query, change.fragment || this.fragment); + } + + public toString() { + return this.scheme + '://' + this.path + (this.query ? '?' + this.query : '') + (this.fragment ? '#' + this.fragment : ''); + } + + public toJSON() { + return this; + } + + public static file(path: string) { + return new Uri('file', '', path); + } + + public static parse(path: string) { + const comps = path.match(/([a-z]+):\/\/([^?#]+)(\?([^#]+)|())(#(.+)|())/)!; + return new Uri(comps[1], '', comps[2], comps[4], comps[6]); + } +} + +export enum StatusBarAlignment { + Left = 1, + Right = 2 +} + +export enum ViewColumn { + Active = -1, + Beside = -2, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9 +} export const window = { - createOutputChannel: jest.fn(() => OutputChannel), + createOutputChannel: jest.fn(() => mocks.outputChannel), + createStatusBarItem: jest.fn(() => mocks.statusBarItem), + createTerminal: jest.fn(() => mocks.terminal), showErrorMessage: jest.fn(), - showInformationMessage: jest.fn() + showInformationMessage: jest.fn(), + showSaveDialog: jest.fn() +}; + +export const workspace = { + createFileSystemWatcher: jest.fn(() => mocks.fileSystemWater), + getConfiguration: jest.fn(() => mocks.workspaceConfiguration), + onDidCloseTextDocument: jest.fn((_: () => void) => ({ dispose: jest.fn() })), + workspaceFolders: <{ uri: Uri }[] | undefined>undefined +}; + +export const mocks = { + extensionContext: { + asAbsolutePath: jest.fn(), + extensionPath: '/path/to/extension', + globalState: { + get: jest.fn(), + update: jest.fn() + }, + globalStoragePath: '/path/to/globalStorage', + logPath: '/path/to/logs', + storagePath: '/path/to/storage', + subscriptions: [], + workspaceState: { + get: jest.fn(), + update: jest.fn() + } + }, + fileSystemWater: { + onDidCreate: jest.fn(), + onDidChange: jest.fn(), + onDidDelete: jest.fn(), + dispose: jest.fn() + }, + outputChannel: { + appendLine: jest.fn(), + dispose: jest.fn() + }, + statusBarItem: { + text: '', + tooltip: '', + command: '', + show: jest.fn(), + hide: jest.fn(), + dispose: jest.fn() + }, + terminal: { + sendText: jest.fn(), + show: jest.fn() + }, + workspaceConfiguration: { + get: jest.fn() + } }; diff --git a/tests/repoFileWatcher.test.ts b/tests/repoFileWatcher.test.ts new file mode 100644 index 00000000..8aaecf20 --- /dev/null +++ b/tests/repoFileWatcher.test.ts @@ -0,0 +1,124 @@ +import * as date from './mocks/date'; +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('../src/logger'); + +import { Logger } from '../src/logger'; +import { RepoFileWatcher } from '../src/repoFileWatcher'; + +let fsWatcher = vscode.mocks.fileSystemWater; +let logger: Logger; + +beforeAll(() => { + logger = new Logger(); + jest.useFakeTimers(); +}); + +afterAll(() => { + logger.dispose(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('RepoFileWatcher', () => { + let repoFileWatcher: RepoFileWatcher; + let callback: jest.Mock; + beforeEach(() => { + callback = jest.fn(); + repoFileWatcher = new RepoFileWatcher(logger, callback); + }); + + it('Should start and receive file events', () => { + // Setup + let onCreateCallback: jest.Mock, onChangeCallback: jest.Mock, onDeleteCallback: jest.Mock; + fsWatcher.onDidCreate.mockImplementationOnce((callback) => onCreateCallback = callback); + fsWatcher.onDidChange.mockImplementationOnce((callback) => onChangeCallback = callback); + fsWatcher.onDidDelete.mockImplementationOnce((callback) => onDeleteCallback = callback); + + // Run + repoFileWatcher.start('/path/to/repo'); + onCreateCallback!(vscode.Uri.file('/path/to/repo/file')); + onChangeCallback!(vscode.Uri.file('/path/to/repo/file')); + onDeleteCallback!(vscode.Uri.file('/path/to/repo/file')); + jest.runOnlyPendingTimers(); + + // Assert + expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledWith('/path/to/repo/**'); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('Should stop a previous active File System Watcher before creating a new one', () => { + // Setup + let onCreateCallback: jest.Mock; + fsWatcher.onDidCreate.mockImplementationOnce((callback) => onCreateCallback = callback); + + // Run + repoFileWatcher.start('/path/to/repo1'); + onCreateCallback!(vscode.Uri.file('/path/to/repo1/file')); + repoFileWatcher.start('/path/to/repo2'); + jest.runOnlyPendingTimers(); + + // Assert + expect(fsWatcher.dispose).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(0); + }); + + it('Should only dispose the active File System Watcher if it exists', () => { + // Run + repoFileWatcher.stop(); + + // Assert + expect(fsWatcher.dispose).toHaveBeenCalledTimes(0); + }); + + it('Should ignore file system events while muted', () => { + // Setup + let onCreateCallback: jest.Mock; + fsWatcher.onDidCreate.mockImplementationOnce((callback) => onCreateCallback = callback); + + // Run + repoFileWatcher.start('/path/to/repo'); + repoFileWatcher.mute(); + onCreateCallback!(vscode.Uri.file('/path/to/repo/file')); + jest.runOnlyPendingTimers(); + + // Assert + expect(callback).toHaveBeenCalledTimes(0); + }); + + it('Should resume reporting file events after 1.5 seconds', () => { + // Setup + let onCreateCallback: jest.Mock, onChangeCallback: jest.Mock; + fsWatcher.onDidCreate.mockImplementationOnce((callback) => onCreateCallback = callback); + fsWatcher.onDidChange.mockImplementationOnce((callback) => onChangeCallback = callback); + date.setCurrentTime(1587559258); + + // Run + repoFileWatcher.start('/path/to/repo'); + repoFileWatcher.mute(); + repoFileWatcher.unmute(); + onCreateCallback!(vscode.Uri.file('/path/to/repo/file')); + date.setCurrentTime(1587559260); + onChangeCallback!(vscode.Uri.file('/path/to/repo/file')); + jest.runOnlyPendingTimers(); + + // Assert + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('Should ignore file system events on files ignored within .git directory', () => { + // Setup + let onCreateCallback: jest.Mock; + fsWatcher.onDidCreate.mockImplementationOnce((callback) => onCreateCallback = callback); + + // Run + repoFileWatcher.start('/path/to/repo'); + onCreateCallback!(vscode.Uri.file('/path/to/repo/.git/config-x')); + jest.runOnlyPendingTimers(); + + // Assert + expect(callback).toHaveBeenCalledTimes(0); + }); +}); diff --git a/tests/statusBarItem.test.ts b/tests/statusBarItem.test.ts new file mode 100644 index 00000000..91e4c7b0 --- /dev/null +++ b/tests/statusBarItem.test.ts @@ -0,0 +1,155 @@ +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('../src/logger'); + +import { ConfigurationChangeEvent } from 'vscode'; +import { EventEmitter } from '../src/event'; +import { Logger } from '../src/logger'; +import { RepoChangeEvent } from '../src/repoManager'; +import { StatusBarItem } from '../src/statusBarItem'; + +let vscodeStatusBarItem = vscode.mocks.statusBarItem; +let workspaceConfiguration = vscode.mocks.workspaceConfiguration; +let onDidChangeRepos: EventEmitter; +let onDidChangeConfiguration: EventEmitter; +let logger: Logger; + +beforeAll(() => { + onDidChangeRepos = new EventEmitter(); + onDidChangeConfiguration = new EventEmitter(); + logger = new Logger(); +}); + +afterAll(() => { + logger.dispose(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('StatusBarItem', () => { + it('Should show the Status Bar Item on vscode startup', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const statusBarItem = new StatusBarItem(1, onDidChangeRepos.subscribe, onDidChangeConfiguration.subscribe, logger); + + // Assert + expect(vscodeStatusBarItem.text).toBe('Git Graph'); + expect(vscodeStatusBarItem.tooltip).toBe('View Git Graph'); + expect(vscodeStatusBarItem.command).toBe('git-graph.view'); + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Teardown + statusBarItem.dispose(); + + // Asset + expect(vscodeStatusBarItem.dispose).toHaveBeenCalledTimes(1); + }); + + it('Should hide the Status Bar Item after the number of repositories becomes zero', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const statusBarItem = new StatusBarItem(1, onDidChangeRepos.subscribe, onDidChangeConfiguration.subscribe, logger); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Run + onDidChangeRepos.emit({ + repos: {}, + numRepos: 0, + loadRepo: null + }); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(1); + + // Teardown + statusBarItem.dispose(); + }); + + it('Should show the Status Bar Item after the number of repositories increases above zero', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + workspaceConfiguration.get.mockReturnValueOnce(true); + + // Run + const statusBarItem = new StatusBarItem(0, onDidChangeRepos.subscribe, onDidChangeConfiguration.subscribe, logger); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(0); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Run + onDidChangeRepos.emit({ + repos: {}, + numRepos: 1, + loadRepo: null + }); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Teardown + statusBarItem.dispose(); + }); + + it('Should hide the Status Bar Item the extension setting git-graph.showStatusBarItem becomes disabled', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const statusBarItem = new StatusBarItem(1, onDidChangeRepos.subscribe, onDidChangeConfiguration.subscribe, logger); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Run + onDidChangeConfiguration.emit({ + affectsConfiguration: () => true + }); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(1); + + // Teardown + statusBarItem.dispose(); + }); + + it('Should ignore extension setting changes unrelated to git-graph.showStatusBarItem', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce(true); + workspaceConfiguration.get.mockReturnValueOnce(false); + + // Run + const statusBarItem = new StatusBarItem(1, onDidChangeRepos.subscribe, onDidChangeConfiguration.subscribe, logger); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Run + onDidChangeConfiguration.emit({ + affectsConfiguration: () => false + }); + + // Assert + expect(vscodeStatusBarItem.show).toHaveBeenCalledTimes(1); + expect(vscodeStatusBarItem.hide).toHaveBeenCalledTimes(0); + + // Teardown + statusBarItem.dispose(); + }); +}); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index cb0f5b83..21528fa4 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,16 +1,41 @@ import * as date from './mocks/date'; import * as vscode from './mocks/vscode'; jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('../src/dataSource'); +jest.mock('../src/logger'); + +import * as cp from 'child_process'; +import * as fs from 'fs'; +import { ConfigurationChangeEvent } from 'vscode'; +import { DataSource } from '../src/dataSource'; +import { EventEmitter } from '../src/event'; +import { Logger } from '../src/logger'; +import { GitFileStatus, PullRequestProvider } from '../src/types'; +import { abbrevCommit, abbrevText, archive, constructIncompatibleGitVersionMessage, copyFilePathToClipboard, copyToClipboard, createPullRequest, evalPromises, getGitExecutable, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, GitExecutable, isGitAtLeastVersion, isPathInWorkspace, openExtensionSettings, openFile, pathWithTrailingSlash, realpath, resolveToSymbolicPath, runGitCommandInNewTerminal, showErrorMessage, showInformationMessage, UNCOMMITTED, viewDiff, viewFileAtRevision, viewScm } from '../src/utils'; + +let terminal = vscode.mocks.terminal; +let workspaceConfiguration = vscode.mocks.workspaceConfiguration; +let onDidChangeConfiguration: EventEmitter; +let onDidChangeGitExecutable: EventEmitter; +let logger: Logger; +let dataSource: DataSource; + +beforeAll(() => { + onDidChangeConfiguration = new EventEmitter(); + onDidChangeGitExecutable = new EventEmitter(); + logger = new Logger(); + dataSource = new DataSource(null, onDidChangeConfiguration.subscribe, onDidChangeGitExecutable.subscribe, logger); +}); -import { abbrevCommit, abbrevText, constructIncompatibleGitVersionMessage, copyFilePathToClipboard, copyToClipboard, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, isGitAtLeastVersion, openExtensionSettings, pathWithTrailingSlash, showErrorMessage, showInformationMessage, viewScm } from '../src/utils'; +afterAll(() => { + dataSource.dispose(); + logger.dispose(); + onDidChangeConfiguration.dispose(); + onDidChangeGitExecutable.dispose(); +}); beforeEach(() => { jest.clearAllMocks(); - date.beforeEach(); -}); - -afterEach(() => { - date.afterEach(); }); describe('getPathFromUri', () => { @@ -67,6 +92,176 @@ describe('pathWithTrailingSlash', () => { }); }); +describe('realpath', () => { + it('Should return the normalised canonical absolute path', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementationOnce((path, callback) => callback(null, path as string)); + + // Run + const path = await realpath('\\a\\b'); + + // Assert + expect(path).toBe('/a/b'); + }); + + it('Should return the original path if fs.realpath returns an error', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementationOnce((_, callback) => callback(new Error('message'), '')); + + // Run + const path = await realpath('/a/b'); + + // Assert + expect(path).toBe('/a/b'); + }); +}); + +describe('isPathInWorkspace', () => { + it('Should return TRUE if a path is a workspace folder', () => { + // Setup + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/workspace-folder1') }, { uri: vscode.Uri.file('/path/to/workspace-folder2') }]; + + // Run + const result = isPathInWorkspace('/path/to/workspace-folder1'); + + // Assert + expect(result).toBe(true); + }); + + it('Should return TRUE if a path is within a workspace folder', () => { + // Setup + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/workspace-folder1') }, { uri: vscode.Uri.file('/path/to/workspace-folder2') }]; + + // Run + const result = isPathInWorkspace('/path/to/workspace-folder1/subfolder'); + + // Assert + expect(result).toBe(true); + }); + + it('Should return FALSE if a path is not within a workspace folder', () => { + // Setup + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/workspace-folder1') }, { uri: vscode.Uri.file('/path/to/workspace-folder2') }]; + + // Run + const result = isPathInWorkspace('/path/to/workspace-folder3/file'); + + // Assert + expect(result).toBe(false); + }); + + it('Should return FALSE if vscode is not running in a workspace', () => { + // Setup + vscode.workspace.workspaceFolders = undefined; + + // Run + const result = isPathInWorkspace('/path/to/workspace-folder1'); + + // Assert + expect(result).toBe(false); + }); +}); + +describe('resolveToSymbolicPath', () => { + it('Should return the original path if it matches a vscode workspace folder', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, path as string)); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/workspace-folder1') }]; + + // Run + const result = await resolveToSymbolicPath('/path/to/workspace-folder1'); + + // Assert + expect(result).toBe('/path/to/workspace-folder1'); + }); + + it('Should return the symbolic path if a vscode workspace folder resolves to it', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, (path as string).replace('symbolic', 'workspace'))); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/symbolic-folder1') }]; + + // Run + const result = await resolveToSymbolicPath('/path/to/workspace-folder1'); + + // Assert + expect(result).toBe('/path/to/symbolic-folder1'); + }); + + it('Should return the original path if it is within a vscode workspace folder', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, path as string)); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/workspace-folder1') }]; + + // Run + const result = await resolveToSymbolicPath('/path/to/workspace-folder1/subfolder/file.txt'); + + // Assert + expect(result).toBe('/path/to/workspace-folder1/subfolder/file.txt'); + }); + + it('Should return the symbolic path if a vscode workspace folder resolves to contain it', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, (path as string).replace('symbolic', 'workspace'))); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/symbolic-folder1') }]; + + // Run + const result = await resolveToSymbolicPath('/path/to/workspace-folder1/subfolder/file.txt'); + + // Assert + expect(result).toBe('/path/to/symbolic-folder1/subfolder/file.txt'); + }); + + it('Should return the symbolic path if the vscode workspace folder resolves to be contained within it', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, (path as string).replace('symbolic', 'workspace'))); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/symbolic-folder/dir') }]; + + // Run + const result = await resolveToSymbolicPath('/path/to/workspace-folder'); + + // Assert + expect(result).toBe('/path/to/symbolic-folder'); + }); + + it('Should return the original path if the vscode workspace folder resolves to be contained within it, when it was unable to find the path correspondence', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => { + path = path as string; + callback(null, path === '/symbolic-folder/path/to/dir' ? path.replace('symbolic', 'workspace') : path); + }); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/symbolic-folder/path/to/dir') }]; + + // Run + const result = await resolveToSymbolicPath('/workspace-folder/path'); + + // Assert + expect(result).toBe('/workspace-folder/path'); + }); + + it('Should return the original path if it is unrelated to the vscode workspace folders', async () => { + // Setup + jest.spyOn(fs, 'realpath').mockImplementation((path, callback) => callback(null, (path as string).replace('symbolic', 'workspace'))); + vscode.workspace.workspaceFolders = [{ uri: vscode.Uri.file('/path/to/symbolic-folder/dir') }]; + + // Run + const result = await resolveToSymbolicPath('/an/unrelated/directory'); + + // Assert + expect(result).toBe('/an/unrelated/directory'); + }); + + it('Should return the original path if vscode is not running in a workspace', async () => { + // Setup + vscode.workspace.workspaceFolders = undefined; + + // Run + const result = await resolveToSymbolicPath('/a/b'); + + // Assert + expect(result).toBe('/a/b'); + }); +}); + describe('abbrevCommit', () => { it('Truncates a commit hash to eight characters', () => { // Run @@ -269,6 +464,97 @@ describe('getRepoName', () => { }); }); +describe('archive', () => { + it('Should trigger the creation of the archive (tar)', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(vscode.Uri.file('/archive/file/destination.tar')); + const spyOnArchive = jest.spyOn(dataSource, 'archive'); + spyOnArchive.mockResolvedValueOnce(null); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe(null); + expect(spyOnArchive).toBeCalledWith('/repo/path', 'abcdef', '/archive/file/destination.tar', 'tar'); + }); + + it('Should trigger the creation of the archive (TAR)', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(vscode.Uri.file('/archive/file/destination.TAR')); + const spyOnArchive = jest.spyOn(dataSource, 'archive'); + spyOnArchive.mockResolvedValueOnce(null); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe(null); + expect(spyOnArchive).toBeCalledWith('/repo/path', 'abcdef', '/archive/file/destination.TAR', 'tar'); + }); + + it('Should trigger the creation of the archive (zip)', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(vscode.Uri.file('/archive/file/destination.zip')); + const spyOnArchive = jest.spyOn(dataSource, 'archive'); + spyOnArchive.mockResolvedValueOnce(null); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe(null); + expect(spyOnArchive).toBeCalledWith('/repo/path', 'abcdef', '/archive/file/destination.zip', 'zip'); + }); + + it('Should trigger the creation of the archive (ZIP)', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(vscode.Uri.file('/archive/file/destination.ZIP')); + const spyOnArchive = jest.spyOn(dataSource, 'archive'); + spyOnArchive.mockResolvedValueOnce(null); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe(null); + expect(spyOnArchive).toBeCalledWith('/repo/path', 'abcdef', '/archive/file/destination.ZIP', 'zip'); + }); + + it('Should return an error message when the specified archive destination has an invalid file extension', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(vscode.Uri.file('/archive/file/destination.txt')); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe('Invalid file extension "*.txt". The archive file must have a *.tar or *.zip extension.'); + }); + + it('Should return an error message when no file is specified for the archive', async () => { + // Setup + vscode.window.showSaveDialog.mockResolvedValueOnce(undefined); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe('No file name was provided for the archive.'); + }); + + it('Should return an error message when vscode fails to show the Save Dialog', async () => { + // Setup + vscode.window.showSaveDialog.mockRejectedValueOnce(undefined); + + // Run + const result = await archive('/repo/path', 'abcdef', dataSource); + + // Assert + expect(result).toBe('Visual Studio Code was unable to display the save dialog.'); + }); +}); + describe('copyFilePathToClipboard', () => { it('Appends the file path to the repository path, and copies the result to the clipboard', async () => { // Setup @@ -319,6 +605,154 @@ describe('copyToClipboard', () => { }); }); +describe('createPullRequest', () => { + it('Should construct and open a BitBucket Pull Request Creation Url', async () => { + // Setup + vscode.env.openExternal.mockResolvedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.Bitbucket, + custom: null, + hostRootUrl: 'https://bitbucket.org', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: 'destProjectId' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe(null); + expect(vscode.env.openExternal.mock.calls[0][0].toString()).toBe('https://bitbucket.org/sourceOwner/sourceRepo/pull-requests/new?source=sourceOwner/sourceRepo::sourceBranch&dest=destOwner/destRepo::destBranch'); + }); + + it('Should construct and open a Custom Providers Pull Request Creation Url', async () => { + // Setup + vscode.env.openExternal.mockResolvedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.Custom, + custom: { + name: 'custom', + templateUrl: '$1/$2/$3/$4/$5/$6/$8' + }, + hostRootUrl: 'https://example.com', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: 'destProjectId' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe(null); + expect(vscode.env.openExternal.mock.calls[0][0].toString()).toBe('https://example.com/sourceOwner/sourceRepo/sourceBranch/destOwner/destRepo/destBranch'); + }); + + it('Should construct and open a GitHub Pull Request Creation Url', async () => { + // Setup + vscode.env.openExternal.mockResolvedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.GitHub, + custom: null, + hostRootUrl: 'https://github.com', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: 'destProjectId' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe(null); + expect(vscode.env.openExternal.mock.calls[0][0].toString()).toBe('https://github.com/destOwner/destRepo/compare/destBranch...sourceOwner:sourceBranch'); + }); + + it('Should construct and open a GitLab Pull Request Creation Url', async () => { + // Setup + vscode.env.openExternal.mockResolvedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.GitLab, + custom: null, + hostRootUrl: 'https://gitlab.com', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: 'destProjectId' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe(null); + expect(vscode.env.openExternal.mock.calls[0][0].toString()).toBe('https://gitlab.com/sourceOwner/sourceRepo/-/merge_requests/new?merge_request[source_branch]=sourceBranch&merge_request[target_branch]=destBranch&merge_request[target_project_id]=destProjectId'); + }); + + it('Should construct and open a GitLab Pull Request Creation Url (without destProjectId)', async () => { + // Setup + vscode.env.openExternal.mockResolvedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.GitLab, + custom: null, + hostRootUrl: 'https://gitlab.com', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: '' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe(null); + expect(vscode.env.openExternal.mock.calls[0][0].toString()).toBe('https://gitlab.com/sourceOwner/sourceRepo/-/merge_requests/new?merge_request[source_branch]=sourceBranch&merge_request[target_branch]=destBranch'); + }); + + it('Should return an error message if vscode was unable to open the url', async () => { + // Setup + vscode.env.openExternal.mockRejectedValueOnce(null); + + // Run + const result = await createPullRequest({ + provider: PullRequestProvider.GitHub, + custom: null, + hostRootUrl: 'https://github.com', + sourceOwner: 'sourceOwner', + sourceRepo: 'sourceRepo', + sourceRemote: 'sourceRemote', + destOwner: 'destOwner', + destRepo: 'destRepo', + destBranch: 'destBranch', + destRemote: 'destRemote', + destProjectId: 'destProjectId' + }, 'sourceOwner', 'sourceRepo', 'sourceBranch'); + + // Assert + expect(result).toBe('Visual Studio Code was unable to open the Pull Request URL: https://github.com/destOwner/destRepo/compare/destBranch...sourceOwner:sourceBranch'); + }); +}); + describe('openExtensionSettings', () => { it('Executes workbench.action.openSettings', async () => { // Setup @@ -328,10 +762,8 @@ describe('openExtensionSettings', () => { const result = await openExtensionSettings(); // Assert - const receivedArgs: any[] = vscode.commands.executeCommand.mock.calls[0]; + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('workbench.action.openSettings', '@ext:mhutchie.git-graph'); expect(result).toBe(null); - expect(receivedArgs[0]).toBe('workbench.action.openSettings'); - expect(receivedArgs[1]).toBe('@ext:mhutchie.git-graph'); }); it('Returns an error message when executeCommand fails', async () => { @@ -346,6 +778,353 @@ describe('openExtensionSettings', () => { }); }); +describe('openFile', () => { + it('Should open the file in vscode', async () => { + // Setup + jest.spyOn(fs, 'access').mockImplementationOnce((...args) => ((args as unknown) as [fs.PathLike, number | undefined, (x: NodeJS.ErrnoException | null) => void])[2](null)); + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await openFile('/path/to/repo', 'file.txt'); + + // Assert + const [command, uri, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.open'); + expect(getPathFromUri(uri)).toBe('/path/to/repo/file.txt'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should return an error message if vscode was unable to open the file', async () => { + // Setup + jest.spyOn(fs, 'access').mockImplementationOnce((...args) => ((args as unknown) as [fs.PathLike, number | undefined, (x: NodeJS.ErrnoException | null) => void])[2](null)); + vscode.commands.executeCommand.mockRejectedValueOnce(null); + + // Run + const result = await openFile('/path/to/repo', 'file.txt'); + + // Assert + expect(result).toBe('Visual Studio Code was unable to open file.txt.'); + }); + + it('Should return an error message if the file doesn\'t exist in the repository', async () => { + // Setup + jest.spyOn(fs, 'access').mockImplementationOnce((...args) => ((args as unknown) as [fs.PathLike, number | undefined, (x: NodeJS.ErrnoException | null) => void])[2](new Error())); + + // Run + const result = await openFile('/path/to/repo', 'file.txt'); + + // Assert + expect(result).toBe('The file file.txt doesn\'t currently exist in this repository.'); + }); +}); + +describe('viewDiff', () => { + it('Should load the vscode diff view (single commit, file added)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', 'abcdef123456', 'abcdef123456', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9hZGRlZC50eHQiLCJjb21taXQiOiJhYmNkZWYxMjM0NTYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(title).toBe('added.txt (Added in abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (single commit, file modified)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', 'abcdef123456', 'abcdef123456', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJhYmNkZWYxMjM0NTZeIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); + expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJhYmNkZWYxMjM0NTYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(title).toBe('modified.txt (abcdef12^ ↔ abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (single commit, file deleted)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', 'abcdef123456', 'abcdef123456', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6ImFiY2RlZjEyMzQ1Nl4iLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(title).toBe('deleted.txt (Deleted in abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commits, file added)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', 'abcdef123456', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9hZGRlZC50eHQiLCJjb21taXQiOiJhYmNkZWYxMjM0NTYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(title).toBe('added.txt (Added between 123456ab & abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commits, file modified)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', 'abcdef123456', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxMjM0NTZhYmNkZWYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJhYmNkZWYxMjM0NTYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(title).toBe('modified.txt (123456ab ↔ abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commits, file deleted)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', 'abcdef123456', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjEyMzQ1NmFiY2RlZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); + expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(title).toBe('deleted.txt (Deleted between 123456ab & abcdef12)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commit and uncommitted changes, file added)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', UNCOMMITTED, 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/added.txt'); + expect(title).toBe('added.txt (Added between 123456ab & Present)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commit and uncommitted changes, file modified)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', UNCOMMITTED, 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxMjM0NTZhYmNkZWYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/modified.txt'); + expect(title).toBe('modified.txt (123456ab ↔ Present)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (between commit and uncommitted changes, file deleted)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', '123456abcdef', UNCOMMITTED, 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjEyMzQ1NmFiY2RlZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); + expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(title).toBe('deleted.txt (Deleted between 123456ab & Present)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (uncommitted changes, file added)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', UNCOMMITTED, UNCOMMITTED, 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/added.txt'); + expect(title).toBe('added.txt (Uncommitted)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (uncommitted changes, file modified)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', UNCOMMITTED, UNCOMMITTED, 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJIRUFEIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); + expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/modified.txt'); + expect(title).toBe('modified.txt (Uncommitted)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should load the vscode diff view (uncommitted changes, file deleted)', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', UNCOMMITTED, UNCOMMITTED, 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + + // Assert + const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.diff'); + expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IkhFQUQiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(title).toBe('deleted.txt (Uncommitted)'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should return an error message when vscode was unable to load the diff view', async () => { + // Setup + vscode.commands.executeCommand.mockRejectedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', 'abcdef123456', 'abcdef123456', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + + // Assert + expect(result).toBe('Visual Studio Code was unable load the diff editor for subfolder/modified.txt.'); + }); + + it('Should open an untracked file in vscode', async () => { + // Setup + jest.spyOn(fs, 'access').mockImplementationOnce((...args) => ((args as unknown) as [fs.PathLike, number | undefined, (x: NodeJS.ErrnoException | null) => void])[2](null)); + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewDiff('/path/to/repo', UNCOMMITTED, UNCOMMITTED, 'subfolder/untracked.txt', 'subfolder/untracked.txt', GitFileStatus.Untracked); + + // Assert + const [command, uri, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.open'); + expect(getPathFromUri(uri)).toBe('/path/to/repo/subfolder/untracked.txt'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); +}); + +describe('viewFileAtRevision', () => { + it('Should open the file in vscode', async () => { + // Setup + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await viewFileAtRevision('/path/to/repo', 'abcdef123456', 'subfolder/file.txt'); + + // Assert + const [command, uri, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.open'); + expect(uri.toString()).toBe('git-graph://abcdef12: file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9maWxlLnR4dCIsImNvbW1pdCI6ImFiY2RlZjEyMzQ1NiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Active + }); + expect(result).toBe(null); + }); + + it('Should return an error message if vscode was unable to open the file', async () => { + // Setup + vscode.commands.executeCommand.mockRejectedValueOnce(null); + + // Run + const result = await viewFileAtRevision('/path/to/repo', 'abcdef123456', 'subfolder/file.txt'); + + // Assert + expect(result).toBe('Visual Studio Code was unable to open subfolder/file.txt at commit abcdef12.'); + }); +}); + describe('viewScm', () => { it('Executes workbench.view.scm', async () => { // Setup @@ -355,9 +1134,8 @@ describe('viewScm', () => { const result = await viewScm(); // Assert - const receivedArgs: any[] = vscode.commands.executeCommand.mock.calls[0]; + expect(vscode.commands.executeCommand).toHaveBeenCalledWith('workbench.view.scm'); expect(result).toBe(null); - expect(receivedArgs[0]).toBe('workbench.view.scm'); }); it('Returns an error message when executeCommand fails', async () => { @@ -372,6 +1150,142 @@ describe('viewScm', () => { }); }); +describe('runGitCommandInNewTerminal', () => { + let ostype: string | undefined, path: string | undefined, platform: NodeJS.Platform; + beforeEach(() => { + ostype = process.env.OSTYPE; + path = process.env.PATH; + platform = process.platform; + process.env.OSTYPE = 'x'; + process.env.PATH = '/path/to/executable'; + Object.defineProperty(process, 'platform', { value: 'y' }); + }); + afterEach(() => { + process.env.OSTYPE = ostype; + process.env.PATH = path; + Object.defineProperty(process, 'platform', { value: platform }); + }); + + it('Should open a new terminal and run the git command', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/executable:/path/to/git' + }, + name: 'Name' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); + + it('Should open a new terminal and run the git command (with initially empty PATH)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + process.env.PATH = ''; + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/git' + }, + name: 'Name' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); + + it('Should open a new terminal and run the git command (with specific shell path)', () => { + // Setup + workspaceConfiguration.get.mockReturnValueOnce('/path/to/shell'); + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/executable:/path/to/git' + }, + name: 'Name', + shellPath: '/path/to/shell' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); + + it('Should open a new terminal and run the git command (platform: win32)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + Object.defineProperty(process, 'platform', { value: 'win32' }); + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/executable;/path/to/git' + }, + name: 'Name' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); + + it('Should open a new terminal and run the git command (ostype: cygwin)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + process.env.OSTYPE = 'cygwin'; + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/executable;/path/to/git' + }, + name: 'Name' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); + + it('Should open a new terminal and run the git command (ostype: msys)', () => { + // Setup + workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue); + process.env.OSTYPE = 'msys'; + + // Run + runGitCommandInNewTerminal('/path/to/repo', '/path/to/git/git', 'rebase', 'Name'); + + // Assert + expect(vscode.window.createTerminal).toHaveBeenCalledWith({ + cwd: '/path/to/repo', + env: { + PATH: '/path/to/executable;/path/to/git' + }, + name: 'Name' + }); + expect(terminal.sendText).toHaveBeenCalledWith('git rebase'); + expect(terminal.show).toHaveBeenCalled(); + }); +}); + describe('showInformationMessage', () => { it('Should show an information message (resolves)', async () => { // Setup @@ -420,13 +1334,125 @@ describe('showErrorMessage', () => { }); }); +describe('evalPromises', () => { + it('Should evaluate promises in parallel (one item in array)', async () => { + // Run + const result = await evalPromises([1], 2, (x) => Promise.resolve(x * 2)); + + // Assert + expect(result).toStrictEqual([2]); + }); + + it('Should evaluate promises in parallel (one item in array that rejects)', async () => { + // Setup + let rejected = false; + + // Run + await evalPromises([1], 2, (x) => Promise.reject(x * 2)).catch(() => rejected = true); + + // Assert + expect(rejected).toBe(true); + }); + + it('Should evaluate promises in parallel (empty array)', async () => { + // Run + const result = await evalPromises([], 2, (x) => Promise.resolve(x * 2)); + + // Assert + expect(result).toStrictEqual([]); + }); + + it('Should evaluate promises in parallel', async () => { + // Run + const result = await evalPromises([1, 2, 3, 4], 2, (x) => Promise.resolve(x * 2)); + + // Assert + expect(result).toStrictEqual([2, 4, 6, 8]); + }); + + it('Should evaluate promises in parallel that reject', async () => { + // Setup + let rejected = false; + + // Run + await evalPromises([1, 2, 3, 4], 2, (x) => Promise.reject(x * 2)).catch(() => rejected = true); + + // Assert + expect(rejected).toBe(true); + }); + + it('Should evaluate promises in parallel (first rejects)', async () => { + // Setup + const prom1 = new Promise((_, reject) => setTimeout(reject, 1)); + const prom2 = prom1.catch(() => 1); + + // Run + const result = await evalPromises([1, 2, 3, 4], 2, (x) => x === 1 ? prom1 : prom2).catch(() => -1); + + // Assert + expect(result).toBe(-1); + }); +}); + +describe('getGitExecutable', () => { + let child: cp.ChildProcess; + let onCallbacks: { [event: string]: (...args: any[]) => void } = {}, stdoutOnCallbacks: { [event: string]: (...args: any[]) => void } = {}; + beforeEach(() => { + child = { + on: (event: string, callback: (...args: any[]) => void) => onCallbacks[event] = callback, + stdout: { + on: (event: string, callback: (...args: any[]) => void) => stdoutOnCallbacks[event] = callback, + } + } as unknown as cp.ChildProcess; + jest.spyOn(cp, 'spawn').mockReturnValueOnce(child); + }); + + it('Should return the git version information', async () => { + // Run + const resultPromise = getGitExecutable('/path/to/git'); + stdoutOnCallbacks['data']('git '); + stdoutOnCallbacks['data']('version 1.2.3'); + onCallbacks['exit'](0); + const result = await resultPromise; + + expect(result).toStrictEqual({ + path: '/path/to/git', + version: '1.2.3' + }); + }); + + it('Should reject when an error is thrown', async () => { + // Setup + let rejected = false; + + // Run + const resultPromise = getGitExecutable('/path/to/git'); + onCallbacks['error'](); + await resultPromise.catch(() => rejected = true); + + expect(rejected).toBe(true); + }); + + it('Should reject when the command exits with a non-zero exit code', async () => { + // Setup + let rejected = false; + + // Run + const resultPromise = getGitExecutable('/path/to/git'); + onCallbacks['exit'](1); + await resultPromise.catch(() => rejected = true); + + expect(rejected).toBe(true); + }); +}); + describe('isGitAtLeastVersion', () => { it('Should correctly determine major newer', () => { // Run const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '1.4.6'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); it('Should correctly determine major older', () => { @@ -434,7 +1460,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '3.4.6'); // Assert - expect(result).toBeFalsy(); + expect(result).toBe(false); }); it('Should correctly determine minor newer', () => { @@ -442,7 +1468,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.3.6'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); it('Should correctly determine minor older', () => { @@ -450,7 +1476,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.5.6'); // Assert - expect(result).toBeFalsy(); + expect(result).toBe(false); }); it('Should correctly determine patch newer', () => { @@ -458,7 +1484,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.5'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); it('Should correctly determine patch older', () => { @@ -466,7 +1492,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.7'); // Assert - expect(result).toBeFalsy(); + expect(result).toBe(false); }); it('Should correctly determine same version', () => { @@ -474,7 +1500,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.6'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); it('Should correctly determine major newer if missing patch version', () => { @@ -482,7 +1508,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2.4', path: '' }, '1.4'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); it('Should correctly determine major newer if missing minor & patch versions', () => { @@ -490,7 +1516,7 @@ describe('isGitAtLeastVersion', () => { const result = isGitAtLeastVersion({ version: '2', path: '' }, '1'); // Assert - expect(result).toBeTruthy(); + expect(result).toBe(true); }); });