Loading src/__mocks__/vscode.js +1 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ module.exports = { tabGroups: { activeTabGroup: {}, }, createTextEditorDecorationType: jest.fn(), }, commands: { executeCommand: jest.fn(), Loading src/browser/browser.ts +1 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,7 @@ export const activate = async (context: vscode.ExtensionContext) => { dependencyContainer.gitLabPlatformManager, ); } else { const codeSuggestions = new CodeSuggestions(dependencyContainer.gitLabPlatformManager); const codeSuggestions = new CodeSuggestions(context, dependencyContainer.gitLabPlatformManager); await codeSuggestions.init(); context.subscriptions.push(codeSuggestions); } Loading src/common/code_suggestions/code_suggestions.test.ts +4 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ jest.mock('./code_suggestions_state_manager', () => ({ }, })); jest.mock('./code_suggestions_gutter_icon'); jest.mock('./code_suggestions_provider'); jest.mock('./code_suggestions_status_bar_item'); Loading @@ -41,11 +42,13 @@ const manager: GitLabPlatformManager = createFakePartial<GitLabPlatformManager>( getForAllAccounts: jest.fn(), }); const context = createFakePartial<vscode.ExtensionContext>({}); describe('CodeSuggestions', () => { let codeSuggestions: CodeSuggestions; beforeEach(() => { codeSuggestions = new CodeSuggestions(manager); codeSuggestions = new CodeSuggestions(context, manager); }); afterEach(() => { Loading src/common/code_suggestions/code_suggestions.ts +6 −1 Original line number Diff line number Diff line Loading @@ -8,12 +8,15 @@ import { CodeSuggestionsStatusBarItem } from './code_suggestions_status_bar_item import { COMMAND_TOGGLE_CODE_SUGGESTIONS, toggleCodeSuggestions } from './commands/toggle'; import { LegacyApiFallbackConfig } from './legacy_api_fallback_config'; import { GitLabPlatformManagerForCodeSuggestions } from './gitlab_platform_manager_for_code_suggestions'; import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon'; export class CodeSuggestions { stateManager: CodeSuggestionsStateManager; statusBarItem: CodeSuggestionsStatusBarItem; #gutterIcon: CodeSuggestionsGutterIcon; toggleSuggestionsCommand?: vscode.Disposable; providerDisposable?: vscode.Disposable; Loading @@ -22,7 +25,7 @@ export class CodeSuggestions { legacyApiFallbackConfig: LegacyApiFallbackConfig; constructor(manager: GitLabPlatformManager) { constructor(context: vscode.ExtensionContext, manager: GitLabPlatformManager) { this.stateManager = new CodeSuggestionsStateManager(manager); this.toggleSuggestionsCommand = vscode.commands.registerCommand( COMMAND_TOGGLE_CODE_SUGGESTIONS, Loading @@ -32,6 +35,7 @@ export class CodeSuggestions { const platformManagerForCodeSuggestions = new GitLabPlatformManagerForCodeSuggestions(manager); this.statusBarItem = new CodeSuggestionsStatusBarItem(this.stateManager); this.#gutterIcon = new CodeSuggestionsGutterIcon(context, this.stateManager); this.legacyApiFallbackConfig = new LegacyApiFallbackConfig(platformManagerForCodeSuggestions); Loading Loading @@ -80,6 +84,7 @@ export class CodeSuggestions { dispose() { this.statusBarItem?.dispose(); this.#gutterIcon.dispose(); this.providerDisposable?.dispose(); this.activeTextEditorChangeDisposable?.dispose(); this.toggleSuggestionsCommand?.dispose(); Loading src/common/code_suggestions/code_suggestions_gutter_icon.test.ts 0 → 100644 +159 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon'; import { CodeSuggestionsStateManager, VisibleCodeSuggestionsState, } from './code_suggestions_state_manager'; import { createFakePartial } from '../test_utils/create_fake_partial'; import { createExtensionContext } from '../test_utils/entities'; describe('CodeSuggestionsGutterIcon', () => { let codeSuggestionsStateManager: CodeSuggestionsStateManager; let context: vscode.ExtensionContext; let mockStateChangeEmitter: vscode.EventEmitter<VisibleCodeSuggestionsState>; let setDecorationsSpy: jest.Mock; let subject: CodeSuggestionsGutterIcon; let textEditorDecorationTypeMap: Map<string, vscode.TextEditorDecorationType>; let activeTextEditorSelection: { start: { line: number } }; const triggerTextEditorSelectionChange = (e: vscode.TextEditorSelectionChangeEvent) => { jest .mocked(vscode.window.onDidChangeTextEditorSelection) .mock.calls.forEach(([listener]) => listener(e)); }; beforeEach(() => { textEditorDecorationTypeMap = new Map<string, vscode.TextEditorDecorationType>(); setDecorationsSpy = jest.fn(); mockStateChangeEmitter = new vscode.EventEmitter<VisibleCodeSuggestionsState>(); activeTextEditorSelection = { start: { line: 7, }, }; vscode.window.activeTextEditor = createFakePartial<vscode.TextEditor>({ setDecorations: setDecorationsSpy, selection: activeTextEditorSelection, document: {}, }); jest .mocked(vscode.window.createTextEditorDecorationType) .mockImplementation(({ gutterIconPath = '' }) => { const textEditorDecorationType: vscode.TextEditorDecorationType = { dispose: jest.fn(), key: gutterIconPath.toString(), }; textEditorDecorationTypeMap.set(textEditorDecorationType.key, textEditorDecorationType); return textEditorDecorationType; }); jest.mocked(vscode.window.onDidChangeTextEditorSelection).mockReturnValue({ dispose: jest.fn(), }); context = createExtensionContext(); codeSuggestionsStateManager = createFakePartial<CodeSuggestionsStateManager>({ onDidChangeVisibleState: mockStateChangeEmitter.event, }); }); afterEach(() => { jest.resetAllMocks(); subject.dispose(); }); describe('default', () => { beforeEach(() => { subject = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager); }); it.each` state | iconPath ${VisibleCodeSuggestionsState.LOADING} | ${'/test/abs/assets/icons/gitlab-code-suggestions-loading.svg'} ${VisibleCodeSuggestionsState.ERROR} | ${'/test/abs/assets/icons/gitlab-code-suggestions-error.svg'} `( 'when manager state changes to $state, updates decorations to $iconPath', ({ state, iconPath }) => { expect(setDecorationsSpy).not.toHaveBeenCalled(); mockStateChangeEmitter.fire(state); expect(setDecorationsSpy).toHaveBeenCalledTimes(1); expect(setDecorationsSpy).toHaveBeenCalledWith(textEditorDecorationTypeMap.get(iconPath), [ { range: { start: { line: 7 }, end: { line: 7 }, }, }, ]); }, ); it('when manager state changes to unhandled state, nothing happens', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY); expect(setDecorationsSpy).not.toHaveBeenCalled(); }); it('when manager state changes then changes again, clears old decoration', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); setDecorationsSpy.mockClear(); mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY); const loadingDecoration = textEditorDecorationTypeMap.get( '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg', ); expect(setDecorationsSpy).toHaveBeenCalledTimes(1); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []); }); it('when manager state changes then text editor selection changes, clears and reapplies old decoration', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); setDecorationsSpy.mockClear(); activeTextEditorSelection.start.line = 10; triggerTextEditorSelectionChange( createFakePartial<vscode.TextEditorSelectionChangeEvent>({}), ); const loadingDecoration = textEditorDecorationTypeMap.get( '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg', ); expect(setDecorationsSpy).toHaveBeenCalledTimes(2); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, [ { range: { start: { line: 10 }, end: { line: 10 }, }, }, ]); }); it('with not active editor, when manager state changes, nothing happens', () => { vscode.window.activeTextEditor = undefined; mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); expect(setDecorationsSpy).not.toHaveBeenCalled(); }); }); describe('dispose', () => { it('disposes subscriptions', () => { const icon = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager); icon.dispose(); const disposeTextEditorSelection = jest.mocked(vscode.window.onDidChangeTextEditorSelection) .mock.results[0].value.dispose; expect(disposeTextEditorSelection).toHaveBeenCalled(); }); }); }); Loading
src/__mocks__/vscode.js +1 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ module.exports = { tabGroups: { activeTabGroup: {}, }, createTextEditorDecorationType: jest.fn(), }, commands: { executeCommand: jest.fn(), Loading
src/browser/browser.ts +1 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,7 @@ export const activate = async (context: vscode.ExtensionContext) => { dependencyContainer.gitLabPlatformManager, ); } else { const codeSuggestions = new CodeSuggestions(dependencyContainer.gitLabPlatformManager); const codeSuggestions = new CodeSuggestions(context, dependencyContainer.gitLabPlatformManager); await codeSuggestions.init(); context.subscriptions.push(codeSuggestions); } Loading
src/common/code_suggestions/code_suggestions.test.ts +4 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ jest.mock('./code_suggestions_state_manager', () => ({ }, })); jest.mock('./code_suggestions_gutter_icon'); jest.mock('./code_suggestions_provider'); jest.mock('./code_suggestions_status_bar_item'); Loading @@ -41,11 +42,13 @@ const manager: GitLabPlatformManager = createFakePartial<GitLabPlatformManager>( getForAllAccounts: jest.fn(), }); const context = createFakePartial<vscode.ExtensionContext>({}); describe('CodeSuggestions', () => { let codeSuggestions: CodeSuggestions; beforeEach(() => { codeSuggestions = new CodeSuggestions(manager); codeSuggestions = new CodeSuggestions(context, manager); }); afterEach(() => { Loading
src/common/code_suggestions/code_suggestions.ts +6 −1 Original line number Diff line number Diff line Loading @@ -8,12 +8,15 @@ import { CodeSuggestionsStatusBarItem } from './code_suggestions_status_bar_item import { COMMAND_TOGGLE_CODE_SUGGESTIONS, toggleCodeSuggestions } from './commands/toggle'; import { LegacyApiFallbackConfig } from './legacy_api_fallback_config'; import { GitLabPlatformManagerForCodeSuggestions } from './gitlab_platform_manager_for_code_suggestions'; import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon'; export class CodeSuggestions { stateManager: CodeSuggestionsStateManager; statusBarItem: CodeSuggestionsStatusBarItem; #gutterIcon: CodeSuggestionsGutterIcon; toggleSuggestionsCommand?: vscode.Disposable; providerDisposable?: vscode.Disposable; Loading @@ -22,7 +25,7 @@ export class CodeSuggestions { legacyApiFallbackConfig: LegacyApiFallbackConfig; constructor(manager: GitLabPlatformManager) { constructor(context: vscode.ExtensionContext, manager: GitLabPlatformManager) { this.stateManager = new CodeSuggestionsStateManager(manager); this.toggleSuggestionsCommand = vscode.commands.registerCommand( COMMAND_TOGGLE_CODE_SUGGESTIONS, Loading @@ -32,6 +35,7 @@ export class CodeSuggestions { const platformManagerForCodeSuggestions = new GitLabPlatformManagerForCodeSuggestions(manager); this.statusBarItem = new CodeSuggestionsStatusBarItem(this.stateManager); this.#gutterIcon = new CodeSuggestionsGutterIcon(context, this.stateManager); this.legacyApiFallbackConfig = new LegacyApiFallbackConfig(platformManagerForCodeSuggestions); Loading Loading @@ -80,6 +84,7 @@ export class CodeSuggestions { dispose() { this.statusBarItem?.dispose(); this.#gutterIcon.dispose(); this.providerDisposable?.dispose(); this.activeTextEditorChangeDisposable?.dispose(); this.toggleSuggestionsCommand?.dispose(); Loading
src/common/code_suggestions/code_suggestions_gutter_icon.test.ts 0 → 100644 +159 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon'; import { CodeSuggestionsStateManager, VisibleCodeSuggestionsState, } from './code_suggestions_state_manager'; import { createFakePartial } from '../test_utils/create_fake_partial'; import { createExtensionContext } from '../test_utils/entities'; describe('CodeSuggestionsGutterIcon', () => { let codeSuggestionsStateManager: CodeSuggestionsStateManager; let context: vscode.ExtensionContext; let mockStateChangeEmitter: vscode.EventEmitter<VisibleCodeSuggestionsState>; let setDecorationsSpy: jest.Mock; let subject: CodeSuggestionsGutterIcon; let textEditorDecorationTypeMap: Map<string, vscode.TextEditorDecorationType>; let activeTextEditorSelection: { start: { line: number } }; const triggerTextEditorSelectionChange = (e: vscode.TextEditorSelectionChangeEvent) => { jest .mocked(vscode.window.onDidChangeTextEditorSelection) .mock.calls.forEach(([listener]) => listener(e)); }; beforeEach(() => { textEditorDecorationTypeMap = new Map<string, vscode.TextEditorDecorationType>(); setDecorationsSpy = jest.fn(); mockStateChangeEmitter = new vscode.EventEmitter<VisibleCodeSuggestionsState>(); activeTextEditorSelection = { start: { line: 7, }, }; vscode.window.activeTextEditor = createFakePartial<vscode.TextEditor>({ setDecorations: setDecorationsSpy, selection: activeTextEditorSelection, document: {}, }); jest .mocked(vscode.window.createTextEditorDecorationType) .mockImplementation(({ gutterIconPath = '' }) => { const textEditorDecorationType: vscode.TextEditorDecorationType = { dispose: jest.fn(), key: gutterIconPath.toString(), }; textEditorDecorationTypeMap.set(textEditorDecorationType.key, textEditorDecorationType); return textEditorDecorationType; }); jest.mocked(vscode.window.onDidChangeTextEditorSelection).mockReturnValue({ dispose: jest.fn(), }); context = createExtensionContext(); codeSuggestionsStateManager = createFakePartial<CodeSuggestionsStateManager>({ onDidChangeVisibleState: mockStateChangeEmitter.event, }); }); afterEach(() => { jest.resetAllMocks(); subject.dispose(); }); describe('default', () => { beforeEach(() => { subject = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager); }); it.each` state | iconPath ${VisibleCodeSuggestionsState.LOADING} | ${'/test/abs/assets/icons/gitlab-code-suggestions-loading.svg'} ${VisibleCodeSuggestionsState.ERROR} | ${'/test/abs/assets/icons/gitlab-code-suggestions-error.svg'} `( 'when manager state changes to $state, updates decorations to $iconPath', ({ state, iconPath }) => { expect(setDecorationsSpy).not.toHaveBeenCalled(); mockStateChangeEmitter.fire(state); expect(setDecorationsSpy).toHaveBeenCalledTimes(1); expect(setDecorationsSpy).toHaveBeenCalledWith(textEditorDecorationTypeMap.get(iconPath), [ { range: { start: { line: 7 }, end: { line: 7 }, }, }, ]); }, ); it('when manager state changes to unhandled state, nothing happens', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY); expect(setDecorationsSpy).not.toHaveBeenCalled(); }); it('when manager state changes then changes again, clears old decoration', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); setDecorationsSpy.mockClear(); mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY); const loadingDecoration = textEditorDecorationTypeMap.get( '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg', ); expect(setDecorationsSpy).toHaveBeenCalledTimes(1); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []); }); it('when manager state changes then text editor selection changes, clears and reapplies old decoration', () => { mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); setDecorationsSpy.mockClear(); activeTextEditorSelection.start.line = 10; triggerTextEditorSelectionChange( createFakePartial<vscode.TextEditorSelectionChangeEvent>({}), ); const loadingDecoration = textEditorDecorationTypeMap.get( '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg', ); expect(setDecorationsSpy).toHaveBeenCalledTimes(2); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []); expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, [ { range: { start: { line: 10 }, end: { line: 10 }, }, }, ]); }); it('with not active editor, when manager state changes, nothing happens', () => { vscode.window.activeTextEditor = undefined; mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING); expect(setDecorationsSpy).not.toHaveBeenCalled(); }); }); describe('dispose', () => { it('disposes subscriptions', () => { const icon = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager); icon.dispose(); const disposeTextEditorSelection = jest.mocked(vscode.window.onDidChangeTextEditorSelection) .mock.results[0].value.dispose; expect(disposeTextEditorSelection).toHaveBeenCalled(); }); }); });