From f2b7522dd797c2db7f5b8ed9d6a92723fd2e31bc Mon Sep 17 00:00:00 2001 From: Vasileios Naskos Date: Wed, 16 Oct 2024 13:23:28 +0200 Subject: [PATCH] OpenInIdeNotification: Use the NotificationService --- .../OpenInIDE/OpenInIdeFailureInfoBarTests.cs | 371 ------------------ .../OpenInIDE/OpenInIdeNotificationTests.cs | 270 +++++++++++-- .../OpenInIde/OpenInIdeFailureInfoBar.cs | 132 ------- .../OpenInIde/OpenInIdeNotification.cs | 50 ++- 4 files changed, 273 insertions(+), 550 deletions(-) delete mode 100644 src/IssueViz.UnitTests/OpenInIDE/OpenInIdeFailureInfoBarTests.cs delete mode 100644 src/IssueViz/OpenInIde/OpenInIdeFailureInfoBar.cs diff --git a/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeFailureInfoBarTests.cs b/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeFailureInfoBarTests.cs deleted file mode 100644 index 0e98394181..0000000000 --- a/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeFailureInfoBarTests.cs +++ /dev/null @@ -1,371 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.InfoBar; -using SonarLint.VisualStudio.IssueVisualization.OpenInIde; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.OpenInIDE -{ - [TestClass] - public class OpenInIdeFailureInfoBarTests - { - private static readonly Guid ValidToolWindowId = Guid.NewGuid(); - - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public async Task Clear_NoPreviousInfoBar_NoException() - { - var infoBarManager = new Mock(); - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - await testSubject.ClearAsync(); - - infoBarManager.Invocations.Should().BeEmpty(); - } - - [TestMethod] - public async Task Clear_HasPreviousInfoBar_InfoBarCleared() - { - var infoBar = new Mock(); - var infoBarManager = new Mock(); - var testSubject = await CreateTestSubjectWithPreviousInfoBar(infoBarManager, infoBar); - - SetupInfoBarEvents(infoBar); - - // Act - await testSubject.ClearAsync(); - - CheckInfoBarWithEventsRemoved(infoBarManager, infoBar); - } - - [TestMethod] - public async Task Show_NoPreviousInfoBar_InfoBarIsShown() - { - var infoBarText = "info bar text"; - var infoBar = new Mock(); - SetupInfoBarEvents(infoBar); - - var infoBarManager = new Mock(); - infoBarManager - .Setup(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(infoBar.Object); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - await testSubject.ShowAsync(ValidToolWindowId, infoBarText, default); - - CheckInfoBarWithEventsAdded(infoBarManager, infoBar, ValidToolWindowId, infoBarText, default); - infoBar.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Show_NoMoreInfo_InfoBarHasOnlyShowLogsButton() - { - const bool hasMoreInfo = false; - var infoBar = new Mock(); - SetupInfoBarEvents(infoBar); - - var infoBarManager = new Mock(); - infoBarManager - .Setup(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(infoBar.Object); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - await testSubject.ShowAsync(ValidToolWindowId, default, hasMoreInfo); - - CheckInfoBarWithEventsAdded(infoBarManager, infoBar, ValidToolWindowId, default, [OpenInIdeResources.InfoBar_Button_ShowLogs]); - infoBar.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Show_HasMoreInfo_InfoBarHasMoreInfoAndShowLogsButtons() - { - const bool hasMoreInfo = true; - var infoBar = new Mock(); - SetupInfoBarEvents(infoBar); - - var infoBarManager = new Mock(); - infoBarManager - .Setup(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(infoBar.Object); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - await testSubject.ShowAsync(ValidToolWindowId, default, hasMoreInfo); - - CheckInfoBarWithEventsAdded(infoBarManager, infoBar, ValidToolWindowId, default, [OpenInIdeResources.InfoBar_Button_MoreInfo, OpenInIdeResources.InfoBar_Button_ShowLogs]); - infoBar.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Show_NoCustomText_InfoBarWithDefaultTextIsShown() - { - var infoBar = new Mock(); - SetupInfoBarEvents(infoBar); - - var infoBarManager = new Mock(); - infoBarManager - .Setup(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(infoBar.Object); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - await testSubject.ShowAsync(ValidToolWindowId, default, default); - - CheckInfoBarWithEventsAdded(infoBarManager, infoBar, ValidToolWindowId, OpenInIdeResources.DefaultInfoBarMessage, default); - infoBar.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Show_HasPreviousInfoBar_InfoBarReplaced() - { - var firstInfoBar = new Mock(); - SetupInfoBarEvents(firstInfoBar); - - var secondInfoBar = new Mock(); - SetupInfoBarEvents(secondInfoBar); - - var infoBarManager = new Mock(); - infoBarManager - .SetupSequence(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(firstInfoBar.Object) - .Returns(secondInfoBar.Object); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - // Act - var text1 = "text1"; - await testSubject.ShowAsync(ValidToolWindowId, text1, default); // show first bar - - CheckInfoBarWithEventsAdded(infoBarManager, firstInfoBar, ValidToolWindowId, text1, default); - infoBarManager.Invocations.Clear(); - - var text2 = "text2"; - await testSubject.ShowAsync(ValidToolWindowId, text2, default); // show second bar - - CheckInfoBarWithEventsRemoved(infoBarManager, firstInfoBar); - CheckInfoBarWithEventsAdded(infoBarManager, secondInfoBar, ValidToolWindowId, text2, default); - - firstInfoBar.VerifyNoOtherCalls(); - secondInfoBar.VerifyNoOtherCalls(); - infoBarManager.VerifyAll(); - infoBarManager.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task Dispose_HasPreviousInfoBar_InfoBarRemoved() - { - var infoBar = new Mock(); - var infoBarManager = new Mock(); - var testSubject = await CreateTestSubjectWithPreviousInfoBar(infoBarManager, infoBar); - - SetupInfoBarEvents(infoBar); - - // Act - testSubject.Dispose(); - - CheckInfoBarWithEventsRemoved(infoBarManager, infoBar); - infoBarManager.VerifyNoOtherCalls(); - } - - [TestMethod] - public void Dispose_NoPreviousInfoBar_NoException() - { - var infoBarManager = new Mock(); - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), new NoOpThreadHandler()); - - testSubject.Dispose(); - - infoBarManager.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task InfoBarIsManuallyClosed_InfoBarDetachedFromToolWindow() - { - var infoBar = new Mock(); - var infoBarManager = new Mock(); - var testSubject = await CreateTestSubjectWithPreviousInfoBar(infoBarManager, infoBar); - - SetupInfoBarEvents(infoBar); - - // Act - infoBar.Raise(x => x.Closed += null, EventArgs.Empty); - - CheckInfoBarWithEventsRemoved(infoBarManager, infoBar); - infoBarManager.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task InfoBarShowLogsButtonClicked_OutputWindowIsShown() - { - var infoBarManager = new Mock(); - var infoBar = new Mock(); - var outputWindowService = new Mock(); - var testSubject = await CreateTestSubjectWithPreviousInfoBar(infoBarManager, infoBar, outputWindow: outputWindowService); - - outputWindowService.VerifyNoOtherCalls(); - - // Act - infoBar.Raise(x => x.ButtonClick += null, new InfoBarButtonClickedEventArgs(OpenInIdeResources.InfoBar_Button_ShowLogs)); - - CheckInfoBarNotRemoved(infoBarManager, infoBar); - outputWindowService.Verify(x => x.Show(), Times.Once); - outputWindowService.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task InfoBarMoreInfoButtonClicked_DocumentationOpenInBrowser() - { - var infoBarManager = new Mock(); - var infoBar = new Mock(); - var browserService = new Mock(); - var testSubject = await CreateTestSubjectWithPreviousInfoBar(infoBarManager, infoBar, browserService: browserService); - - browserService.VerifyNoOtherCalls(); - - // Act - infoBar.Raise(x => x.ButtonClick += null, new InfoBarButtonClickedEventArgs(OpenInIdeResources.InfoBar_Button_MoreInfo)); - - CheckInfoBarNotRemoved(infoBarManager, infoBar); - browserService.Verify(x => x.Navigate(DocumentationLinks.OpenInIdeIssueLocation)); - browserService.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task ShowAsync_VerifySwitchesToUiThreadIsCalled() - { - var infoBarManager = new Mock(); - infoBarManager.Setup(x => - x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)).Returns(Mock.Of()); - - var threadHandling = new Mock(); - threadHandling.Setup(x => x.RunOnUIThreadAsync(It.IsAny())).Callback(op => - { - infoBarManager.Verify(x - => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default), Times.Never); - op(); - infoBarManager.Verify(x - => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default), Times.Once); - }); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, Mock.Of(), Mock.Of(), threadHandling.Object); - - await testSubject.ShowAsync(ValidToolWindowId, "some text", default); - threadHandling.Verify(x => x.RunOnUIThreadAsync(It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task ClearAsync_VerifySwitchesToUiThreadIsCalled() - { - var threadHandling = new Mock(); - - var testSubject = new OpenInIdeFailureInfoBar(Mock.Of(), Mock.Of(), Mock.Of(), threadHandling.Object); - - await testSubject.ClearAsync(); - - threadHandling.Verify(x => x.RunOnUIThreadAsync(It.IsAny()), Times.Once); - } - - private async static Task CreateTestSubjectWithPreviousInfoBar( - Mock infoBarManager = null, - Mock infoBar = null, - Mock browserService = null, - Mock outputWindow = null) - { - infoBar ??= new Mock(); - SetupInfoBarEvents(infoBar); - - infoBarManager ??= new Mock(); - infoBarManager - .Setup(x => x.AttachInfoBarWithButtons(ValidToolWindowId, It.IsAny(), It.IsAny>(), default)) - .Returns(infoBar.Object); - - browserService ??= new Mock(); - outputWindow ??= new Mock(); - - var testSubject = new OpenInIdeFailureInfoBar(infoBarManager.Object, outputWindow.Object, browserService.Object, new NoOpThreadHandler()); - - // Call "Show" to create an infobar and check it was added - var someText = "some text"; - await testSubject.ShowAsync(ValidToolWindowId, someText, default); - - CheckInfoBarWithEventsAdded(infoBarManager, infoBar, ValidToolWindowId, someText, default); - - infoBarManager.VerifyNoOtherCalls(); - outputWindow.Invocations.Should().BeEmpty(); - - return testSubject; - } - - private static void SetupInfoBarEvents(Mock infoBar) - { - infoBar.SetupAdd(x => x.Closed += (sender, args) => { }); - infoBar.SetupAdd(x => x.ButtonClick += (sender, args) => { }); - - infoBar.SetupRemove(x => x.Closed -= (sender, args) => { }); - infoBar.SetupRemove(x => x.ButtonClick -= (sender, args) => { }); - } - - private static void CheckInfoBarWithEventsRemoved(Mock infoBarManager, Mock infoBar) - { - infoBarManager.Verify(x => x.CloseInfoBar(infoBar.Object), Times.Once); - - infoBar.VerifyRemove(x => x.Closed -= It.IsAny(), Times.Once); - infoBar.VerifyRemove(x => x.ButtonClick -= It.IsAny>(), Times.Once); - } - - private static void CheckInfoBarNotRemoved(Mock infoBarManager, Mock infoBar) - { - infoBarManager.Verify(x => x.CloseInfoBar(infoBar.Object), Times.Never); - } - - private static void CheckInfoBarWithEventsAdded(Mock infoBarManager, Mock infoBar, Guid toolWindowId, string text, List buttonTexts) - { - text ??= OpenInIdeResources.DefaultInfoBarMessage; - buttonTexts ??= [OpenInIdeResources.InfoBar_Button_ShowLogs]; - infoBarManager.Verify(x => x.AttachInfoBarWithButtons(toolWindowId, - text, - It.Is>(actualButtons => actualButtons.SequenceEqual(buttonTexts)), - default), - Times.Once); - - infoBar.VerifyAdd(x => x.Closed += It.IsAny(), Times.Once); - infoBar.VerifyAdd(x => x.ButtonClick += It.IsAny>(), Times.Once); - } - } -} diff --git a/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeNotificationTests.cs b/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeNotificationTests.cs index 2b570b5465..5bdc8485f9 100644 --- a/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeNotificationTests.cs +++ b/src/IssueViz.UnitTests/OpenInIDE/OpenInIdeNotificationTests.cs @@ -18,11 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.Windows; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using NSubstitute; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Notifications; using SonarLint.VisualStudio.IssueVisualization.OpenInIde; using SonarLint.VisualStudio.TestInfrastructure; @@ -31,12 +28,35 @@ namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.OpenInIDE; [TestClass] public class OpenInIdeNotificationTests { + private const string AFilePath = @"file\path\123.cs"; + + private static readonly Guid AToolWindowId = Guid.NewGuid(); + + private OpenInIdeNotification testSubject; + private IToolWindowService toolWindowService; + private INotificationService notificationService; + private IOutputWindowService outputWindowService; + private IBrowserService browserService; + + [TestInitialize] + public void TestInitialize() + { + toolWindowService = Substitute.For(); + notificationService = Substitute.For(); + outputWindowService = Substitute.For(); + browserService = Substitute.For(); + + testSubject = new OpenInIdeNotification(toolWindowService, notificationService, outputWindowService, browserService); + } + [TestMethod] public void MefCtor_CheckIsExported() { MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); } [TestMethod] @@ -44,52 +64,236 @@ public void MefCtor_CheckIsSingleton() { MefTestHelpers.CheckIsSingletonMefComponent(); } + + [TestMethod] + public void UnableToLocateIssue_DisplaysNotification_WithMessage() + { + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + AssertReceivedNotificationWithMessage(string.Format(OpenInIdeResources.Notification_UnableToLocateIssue, AFilePath), AToolWindowId); + } + + [TestMethod] + public void UnableToLocateIssue_ShouldNotShowOnlyOncePerSession() + { + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + + AssertNotificationNotShownOnlyOncePerSession(AToolWindowId); + } + [TestMethod] - public void UnableToLocateIssue_ShowsMessageBox() + public void UnableToLocateIssue_DisplaysNotification_WithActionToOpenDocsInBrowser() { - var filePath = "file/path/123"; - var toolWindowId = Guid.NewGuid(); - var infoBar = Substitute.For(); - var toolWindowService = Substitute.For(); + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + + SimulateNotificationActionInvoked(0); - new OpenInIdeNotification(infoBar, toolWindowService).UnableToLocateIssue(filePath, toolWindowId); + AssertOpenedDocsPageInBrowser(); + } + + [TestMethod] + public void UnableToLocateIssue_DisplaysNotification_WithActionToShowLogs() + { + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); - VerifyToolWindowOpened(toolWindowService, toolWindowId); - VerifyInfoBarShown(infoBar, "Open in IDE. Could not locate the issue in the file. Please ensure the file (file/path/123) has not been modified.", toolWindowId, true); + SimulateNotificationActionInvoked(1); + + AssertShownLogsWindow(); } + + [TestMethod] + public void UnableToLocateIssue_NotificationActions_ShouldNotDismissNotification() + { + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + AssertActionsAreNotDismissingNotification(AToolWindowId); + } + [TestMethod] - public void UnableToOpenFile_ShowsMessageBox() + public void UnableToLocateIssue_NotificationActions_ShouldBeExactlyTwo() { - var filePath = "file/path/123"; - var toolWindowId = Guid.NewGuid(); - var infoBar = Substitute.For(); - var toolWindowService = Substitute.For(); + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + + AssertActionButtonsAreExactly(2); + } + + [TestMethod] + public void UnableToLocateIssue_BringsToolWindowToFocus() + { + testSubject.UnableToLocateIssue(AFilePath, AToolWindowId); + + AssertBroughtToolWindowToFocus(AToolWindowId); + } + + [TestMethod] + public void UnableToOpenFile_DisplaysNotification_WithMessage() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + AssertReceivedNotificationWithMessage(string.Format(OpenInIdeResources.Notification_UnableToOpenFile, AFilePath), AToolWindowId); + } + + [TestMethod] + public void UnableToOpenFile_ShouldNotShowOnlyOncePerSession() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); - new OpenInIdeNotification(infoBar, toolWindowService).UnableToOpenFile(filePath, toolWindowId); + AssertNotificationNotShownOnlyOncePerSession(AToolWindowId); + } + + [TestMethod] + public void UnableToOpenFile_DisplaysNotification_WithActionToOpenDocsInBrowser() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + SimulateNotificationActionInvoked(0); - VerifyToolWindowOpened(toolWindowService, toolWindowId); - VerifyInfoBarShown(infoBar, "Open in IDE. Could not open File: file/path/123. Please ensure that you're on the correct branch and the file has not been deleted locally.", toolWindowId, true); + AssertOpenedDocsPageInBrowser(); + } + + [TestMethod] + public void UnableToOpenFile_DisplaysNotification_WithActionToShowLogs() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + SimulateNotificationActionInvoked(1); + + AssertShownLogsWindow(); + } + + [TestMethod] + public void UnableToOpenFile_NotificationActions_ShouldNotDismissNotification() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + AssertActionsAreNotDismissingNotification(AToolWindowId); + } + + [TestMethod] + public void UnableToOpenFile_NotificationActions_ShouldBeExactlyTwo() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + AssertActionButtonsAreExactly(2); + } + + [TestMethod] + public void UnableToOpenFile_BringsToolWindowToFocus() + { + testSubject.UnableToOpenFile(AFilePath, AToolWindowId); + + AssertBroughtToolWindowToFocus(AToolWindowId); + } + + [TestMethod] + public void InvalidRequest_DisplaysNotification_WithMessage() + { + testSubject.InvalidRequest(AFilePath, AToolWindowId); + + AssertReceivedNotificationWithMessage(string.Format(OpenInIdeResources.Notification_InvalidConfiguration, AFilePath), AToolWindowId); } [TestMethod] - public void InvalidConfiguration_ShowsMessageBox() + public void InvalidRequest_ShouldNotShowOnlyOncePerSession() { - var reason = "reason 123"; - var infoBar = Substitute.For(); - var toolWindowId = Guid.NewGuid(); - var toolWindowService = Substitute.For(); + testSubject.InvalidRequest(AFilePath, AToolWindowId); - new OpenInIdeNotification(infoBar, toolWindowService).InvalidRequest(reason, toolWindowId); + AssertNotificationNotShownOnlyOncePerSession(AToolWindowId); + } + + [TestMethod] + public void InvalidRequest_DisplaysNotification_WithActionToShowLogs() + { + testSubject.InvalidRequest(AFilePath, AToolWindowId); - VerifyToolWindowOpened(toolWindowService, toolWindowId); - VerifyInfoBarShown(infoBar, $"Unable to process Open in IDE request. Reason: reason 123", toolWindowId, false); + SimulateNotificationActionInvoked(0); + + AssertShownLogsWindow(); } - private void VerifyToolWindowOpened(IToolWindowService toolWindowService, Guid toolWindowId) => + [TestMethod] + public void InvalidRequest_NotificationActions_ShouldNotDismissNotification() + { + testSubject.InvalidRequest(AFilePath, AToolWindowId); + + AssertActionsAreNotDismissingNotification(AToolWindowId); + } + + [TestMethod] + public void InvalidRequest_NotificationActions_ShouldBeExactlyOne() + { + testSubject.InvalidRequest(AFilePath, AToolWindowId); + + AssertActionButtonsAreExactly(1); + } + + [TestMethod] + public void InvalidRequest_BringsToolWindowToFocus() + { + testSubject.InvalidRequest(AFilePath, AToolWindowId); + + AssertBroughtToolWindowToFocus(AToolWindowId); + } + + [TestMethod] + public void Clear_ClosesAnyOpenNotification() + { + testSubject.Clear(); + + notificationService.Received(1).CloseNotification(); + } + + [TestMethod] + public void Dispose_ClosesAnyOpenNotification() + { + testSubject.Dispose(); + + notificationService.Received(1).CloseNotification(); + } + + private void AssertReceivedNotificationWithMessage(string message, Guid toolWindowId) + { + notificationService.Received(1).ShowNotification(Arg.Is(x => x.Message == message), toolWindowId); + } + + private void AssertNotificationNotShownOnlyOncePerSession(Guid toolWindowId) + { + notificationService.Received(1).ShowNotification(Arg.Is(x => !x.ShowOncePerSession), toolWindowId); + } + + private void AssertOpenedDocsPageInBrowser() + { + browserService.Received(1).Navigate(DocumentationLinks.OpenInIdeIssueLocation); + } + + private void AssertShownLogsWindow() + { + outputWindowService.Received(1).Show(); + } + + private void AssertActionsAreNotDismissingNotification(Guid toolWindowId) + { + notificationService.Received(1).ShowNotification(Arg.Is(n => + n.Actions.All(a => !a.ShouldDismissAfterAction) + ), toolWindowId); + } + + private void AssertActionButtonsAreExactly(int count) + { + var notification = notificationService.ReceivedCalls().Single().GetArguments()[0] as Notification; + notification.Should().NotBeNull(); + notification!.Actions.Count().Should().Be(count); + } + + private void AssertBroughtToolWindowToFocus(Guid toolWindowId) + { toolWindowService.Show(toolWindowId); + } - private void VerifyInfoBarShown(IOpenInIdeFailureInfoBar infoBar, string message, Guid toolWindowId, bool hasMoreInfo) => - infoBar.Received(1).ShowAsync(toolWindowId, message, hasMoreInfo); + private void SimulateNotificationActionInvoked(int actionIndex) + { + var notification = notificationService.ReceivedCalls().Single().GetArguments()[0] as Notification; + notification.Should().NotBeNull(); + notification!.Actions.ToList()[actionIndex].Action.Invoke(notification); + } } diff --git a/src/IssueViz/OpenInIde/OpenInIdeFailureInfoBar.cs b/src/IssueViz/OpenInIde/OpenInIdeFailureInfoBar.cs deleted file mode 100644 index dcfaba82d5..0000000000 --- a/src/IssueViz/OpenInIde/OpenInIdeFailureInfoBar.cs +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.ComponentModel.Composition; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.InfoBar; - -namespace SonarLint.VisualStudio.IssueVisualization.OpenInIde; - -public interface IOpenInIdeFailureInfoBar -{ - Task ShowAsync(Guid toolWindowId, string text, bool showMoreInfoButton); - - Task ClearAsync(); -} - -[Export(typeof(IOpenInIdeFailureInfoBar))] -[PartCreationPolicy(CreationPolicy.Shared)] -internal sealed class OpenInIdeFailureInfoBar : IOpenInIdeFailureInfoBar, IDisposable -{ - private readonly IInfoBarManager infoBarManager; - private readonly IOutputWindowService outputWindowService; - private readonly IBrowserService browService; - private readonly IThreadHandling threadHandling; - private readonly object lockObject = new(); - private IInfoBar currentInfoBar; - - [ImportingConstructor] - public OpenInIdeFailureInfoBar(IInfoBarManager infoBarManager, - IOutputWindowService outputWindowService, - IBrowserService browService, - IThreadHandling threadHandling) - { - this.infoBarManager = infoBarManager; - this.outputWindowService = outputWindowService; - this.browService = browService; - this.threadHandling = threadHandling; - } - - public async Task ShowAsync(Guid toolWindowId, string text, bool showMoreInfoButton) - { - await threadHandling.RunOnUIThreadAsync(() => - { - lock (lockObject) - { - RemoveExistingInfoBar(); - AddInfoBar(toolWindowId, text, showMoreInfoButton); - } - }); - } - - public async Task ClearAsync() - { - await threadHandling.RunOnUIThreadAsync(() => - { - lock (lockObject) - { - RemoveExistingInfoBar(); - } - }); - } - - private void AddInfoBar(Guid toolWindowId, string text, bool hasMoreInfo) - { - List buttonTexts = hasMoreInfo - ? [OpenInIdeResources.InfoBar_Button_MoreInfo, OpenInIdeResources.InfoBar_Button_ShowLogs] - : [OpenInIdeResources.InfoBar_Button_ShowLogs]; - currentInfoBar = infoBarManager.AttachInfoBarWithButtons(toolWindowId, - text ?? OpenInIdeResources.DefaultInfoBarMessage, buttonTexts, default); - Debug.Assert(currentInfoBar != null, "currentInfoBar != null"); - - currentInfoBar.ButtonClick += HandleInfoBarAction; - currentInfoBar.Closed += CurrentInfoBar_Closed; - } - - private void HandleInfoBarAction(object sender, InfoBarButtonClickedEventArgs e) - { - if (e.ClickedButtonText == OpenInIdeResources.InfoBar_Button_MoreInfo) - { - browService.Navigate(DocumentationLinks.OpenInIdeIssueLocation); - } - - if (e.ClickedButtonText == OpenInIdeResources.InfoBar_Button_ShowLogs) - { - outputWindowService.Show(); - } - } - - private void RemoveExistingInfoBar() - { - if (currentInfoBar != null) - { - currentInfoBar.ButtonClick -= HandleInfoBarAction; - currentInfoBar.Closed -= CurrentInfoBar_Closed; - infoBarManager.CloseInfoBar(currentInfoBar); - currentInfoBar = null; - } - } - - private void CurrentInfoBar_Closed(object sender, EventArgs e) - { - lock (lockObject) - { - RemoveExistingInfoBar(); - } - } - - public void Dispose() - { - lock (lockObject) - { - RemoveExistingInfoBar(); - } - } -} diff --git a/src/IssueViz/OpenInIde/OpenInIdeNotification.cs b/src/IssueViz/OpenInIde/OpenInIdeNotification.cs index b9bf638f83..f4ef2153ea 100644 --- a/src/IssueViz/OpenInIde/OpenInIdeNotification.cs +++ b/src/IssueViz/OpenInIde/OpenInIdeNotification.cs @@ -18,12 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.ComponentModel.Composition; -using System.Windows; -using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.Core.Notifications; namespace SonarLint.VisualStudio.IssueVisualization.OpenInIde; @@ -37,35 +34,60 @@ internal interface IOpenInIdeNotification [Export(typeof(IOpenInIdeNotification))] [PartCreationPolicy(CreationPolicy.Shared)] -internal class OpenInIdeNotification : IOpenInIdeNotification +internal sealed class OpenInIdeNotification : IOpenInIdeNotification, IDisposable { private readonly IToolWindowService toolWindowService; - private readonly IOpenInIdeFailureInfoBar infoBar; + private readonly INotificationService notificationService; + private readonly IOutputWindowService outputWindowService; + private readonly IBrowserService browserService; [ImportingConstructor] - public OpenInIdeNotification(IOpenInIdeFailureInfoBar infoBar, IToolWindowService toolWindowService) + public OpenInIdeNotification( + IToolWindowService toolWindowService, + INotificationService notificationService, + IOutputWindowService outputWindowService, + IBrowserService browserService) { this.toolWindowService = toolWindowService; - this.infoBar = infoBar; + this.notificationService = notificationService; + this.outputWindowService = outputWindowService; + this.browserService = browserService; } public void UnableToLocateIssue(string filePath, Guid toolWindowId) => - Show(string.Format(OpenInIdeResources.Notification_UnableToLocateIssue, filePath), toolWindowId, true); + Show(toolWindowId, string.Format(OpenInIdeResources.Notification_UnableToLocateIssue, filePath), true); public void UnableToOpenFile(string filePath, Guid toolWindowId) => - Show(string.Format(OpenInIdeResources.Notification_UnableToOpenFile, filePath), toolWindowId, true); + Show(toolWindowId, string.Format(OpenInIdeResources.Notification_UnableToOpenFile, filePath), true); public void InvalidRequest(string reason, Guid toolWindowId) => - Show(string.Format(OpenInIdeResources.Notification_InvalidConfiguration, reason), toolWindowId, false); + Show(toolWindowId, string.Format(OpenInIdeResources.Notification_InvalidConfiguration, reason), false); public void Clear() { - infoBar.ClearAsync().Forget(); + notificationService.CloseNotification(); } - private void Show(string message, Guid toolWindowId, bool hasMoreInfo) + public void Dispose() + { + Clear(); + } + + private void Show(Guid toolWindowId, string message, bool hasMoreInfo) { toolWindowService.Show(toolWindowId); - infoBar.ShowAsync(toolWindowId, message, hasMoreInfo).Forget(); + + var moreInfoAction = new NotificationAction(OpenInIdeResources.InfoBar_Button_MoreInfo, _ => browserService.Navigate(DocumentationLinks.OpenInIdeIssueLocation), false); + var showLogsAction = new NotificationAction(OpenInIdeResources.InfoBar_Button_ShowLogs, _ => outputWindowService.Show(), false); + List actions = hasMoreInfo + ? [moreInfoAction, showLogsAction] + : [showLogsAction]; + + var notification = new Notification( + id: "OpenInIdeNotification", + showOncePerSession: false, + message: message, + actions: actions); + notificationService.ShowNotification(notification, toolWindowId); } }