diff --git a/GitHubExtension.Test/Controls/GitHubQueryValidationTests.cs b/GitHubExtension.Test/Controls/GitHubQueryValidationTests.cs index 58f1a6a..6f374a3 100644 --- a/GitHubExtension.Test/Controls/GitHubQueryValidationTests.cs +++ b/GitHubExtension.Test/Controls/GitHubQueryValidationTests.cs @@ -12,12 +12,19 @@ namespace GitHubExtension.Test.Controls; [TestClass] public class GitHubQueryValidationTests { + public SaveSearchForm CreateSaveSearchForm(Mock mockSearchRepository) + { + var mockResources = new Mock(); + var savedSearchesMediator = new SavedSearchesMediator(); + return new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); + } + [TestMethod] public async Task ValidateSearch_SupportsIsOpenKeyword() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:open"; var search = new SearchCandidate(searchString, "Test Search"); @@ -35,7 +42,7 @@ public async Task ValidateSearch_SupportsIsIssueKeyword() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:issue"; var search = new SearchCandidate(searchString, "Test Search"); @@ -53,7 +60,7 @@ public async Task ValidateSearch_SupportsIsPullRequestKeyword() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:pr"; var search = new SearchCandidate(searchString, "Test Search"); @@ -70,7 +77,7 @@ public async Task ValidateSearch_SupportsMultipleKeywords() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:open is:issue"; var search = new SearchCandidate(searchString, "Test Search"); @@ -87,7 +94,7 @@ public async Task ValidateSearch_SupportsRepoQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:issue repo:microsoft/PowerToys"; @@ -103,7 +110,7 @@ public async Task ValidateSearch_SupportsAuthorQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:pr author:octocat"; @@ -119,7 +126,7 @@ public async Task ValidateSearch_SupportsStateAndLabelQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "state:open label:bug"; @@ -135,7 +142,7 @@ public async Task ValidateSearch_SupportsInvolvesAndLanguageQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "involves:defunkt language:javascript"; @@ -151,7 +158,7 @@ public async Task ValidateSearch_SupportsOrgAndCreatedQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "org:github created:>2022-01-01"; @@ -167,7 +174,7 @@ public async Task ValidateSearch_SupportsAssigneeAndMilestoneQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:issue assignee:@me milestone:v1.0"; @@ -183,7 +190,7 @@ public async Task ValidateSearch_SupportsReviewQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:pr review:approved"; @@ -199,7 +206,7 @@ public async Task ValidateSearch_SupportsInQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:issue in:title error"; @@ -215,7 +222,7 @@ public async Task ValidateSearch_SupportsMergedDateQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:pr merged:>=2023-01-01"; @@ -231,7 +238,7 @@ public async Task ValidateSearch_SupportsSingleLabelQualifier() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "label:enhancement"; @@ -247,7 +254,7 @@ public async Task ValidateSearch_SupportsMultipleLabelQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "label:bug label:help-wanted label:documentation"; @@ -263,7 +270,7 @@ public async Task ValidateSearch_SupportsExcludingLabel() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "-label:wontfix"; @@ -279,7 +286,7 @@ public async Task ValidateSearch_SupportsExcludingMultipleLabels() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "-label:wontfix -label:duplicate -label:invalid"; @@ -295,7 +302,7 @@ public async Task ValidateSearch_SupportsExcludingOtherQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:issue -is:closed -author:bot"; @@ -312,7 +319,7 @@ public async Task ValidateSearch_SupportsMixOfIncludeAndExcludeQualifiers() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:pr label:enhancement -label:wontfix repo:microsoft/PowerToys -is:draft"; @@ -329,7 +336,7 @@ public async Task ValidateSearch_SupportsBooleanOperators() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "is:open AND (is:issue OR is:pr) NOT author:bot devhome"; var search = new SearchCandidate(searchString, "Test Search"); @@ -346,7 +353,7 @@ public async Task ValidateSearch_SupportsMultipleRepositories() { var mockSearchRepository = new Mock(); var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var saveSearchForm = CreateSaveSearchForm(mockSearchRepository); var searchString = "repo:microsoft/terminal repo:microsoft/PowerToys repo:microsoft/vscode is:open is:issue"; var search = new SearchCandidate(searchString, "Test Search"); diff --git a/GitHubExtension.Test/Controls/SaveSearchFormTest.cs b/GitHubExtension.Test/Controls/SaveSearchFormTest.cs index 1213106..9b05441 100644 --- a/GitHubExtension.Test/Controls/SaveSearchFormTest.cs +++ b/GitHubExtension.Test/Controls/SaveSearchFormTest.cs @@ -51,7 +51,8 @@ public void SubmitForm_ShouldSaveIssueSearch_WhenIssueSearchIsProvided() .Returns(Task.CompletedTask); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -88,7 +89,8 @@ public void SubmitForm_ShouldSavePullRequestSearch_WhenPRSearchIsProvided() .Returns(Task.CompletedTask); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -125,7 +127,8 @@ public void SubmitForm_ShouldSaveCombinedSearch_WhenNoTypeIsProvided() .Returns(Task.CompletedTask); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -159,7 +162,8 @@ public void SubmitForm_ShouldEditSearchString_WhenUpdatingExistingSearch() var existingSearch = new SearchCandidate("old search", "My Search", false); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -205,7 +209,8 @@ public void SubmitForm_ShouldEditSearchName_WhenUpdatingExistingSearch() var existingSearch = new SearchCandidate("my search", "Old Name", false); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -251,7 +256,8 @@ public void SubmitForm_ShouldEditBothNameAndString_WhenUpdatingExistingSearch() var existingSearch = new SearchCandidate("old search", "Old Name", false); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -297,7 +303,8 @@ public void SubmitForm_ShouldOnlyUpdateTopLevel_WhenNothingElseChanges() var existingSearch = new SearchCandidate("my search", "My Search", false); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(existingSearch, mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -341,7 +348,8 @@ public async Task GetSearchAsync_WithGitHubQueryUrl_ParsesAndReturnsSearchString var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -363,7 +371,8 @@ public async Task GetSearchAsync_WithRepositoryIssuesUrl_ParsesAndReturnsFormatt var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -384,7 +393,8 @@ public async Task GetSearchAsync_WithRepositoryIssuesWithoutQuery_ParsesAndRetur var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -405,7 +415,8 @@ public async Task GetSearchAsync_WithRepositoryClosedIssuesUrl_ParsesAndReturnsC var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -426,7 +437,8 @@ public async Task GetSearchAsync_WithPullRequestsUrl_ParsesAndReturnsPrSearchStr var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -447,7 +459,8 @@ public async Task GetSearchAsync_WithSearchPagesUrl_ParsesAndReturnsBasicSearchS var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -468,7 +481,8 @@ public async Task GetSearchAsync_WithInvalidUrl_UsesOriginalString() var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(invalidUrl, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -489,7 +503,8 @@ public async Task GetSearchAsync_WithEmptyUrl_ReturnsEmptySearchString() var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(emptyUrl, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -510,7 +525,8 @@ public async Task GetSearchAsync_WithMultipleQualifiers_ParsesAndPreservesAllQua var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -531,7 +547,8 @@ public async Task GetSearchAsync_WithNegatedQualifiers_ParsesAndPreservesNegatio var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -552,7 +569,8 @@ public async Task GetSearchAsync_WithMultipleRepositories_ParsesAndReturnsCorrec var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -573,7 +591,8 @@ public async Task GetSearchAsync_WithMultipleStates_ParsesAndReturnsCorrectSearc var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -594,7 +613,8 @@ public async Task GetSearchAsync_WithMultipleSortDirections_ParsesAndReturnsCorr var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); @@ -615,7 +635,8 @@ public async Task GetSearchAsync_WithMultipleLanguagesMilestonesDates_ParsesAndR var mockResources = new Mock(); - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, mockResources.Object, savedSearchesMediator); var payload = CreatePayload(url, "Test Search"); var result = await saveSearchForm.GetSearchAsync(payload); diff --git a/GitHubExtension.Test/Controls/SavedSearchesPageTest.cs b/GitHubExtension.Test/Controls/SavedSearchesPageTest.cs index 41bbe63..d7ab0cf 100644 --- a/GitHubExtension.Test/Controls/SavedSearchesPageTest.cs +++ b/GitHubExtension.Test/Controls/SavedSearchesPageTest.cs @@ -21,7 +21,8 @@ public void SavedSearchesPageCreate() var stubSearchRepository = new Mock().Object; var stubAddSearchListItem = new Mock().Object; var stubResources = new Mock().Object; - var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory, stubSearchRepository, stubResources, stubAddSearchListItem); + var savedSearchesMediator = new Mock().Object; + var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory, stubSearchRepository, stubResources, stubAddSearchListItem, savedSearchesMediator); Assert.IsNotNull(savedSearchesPage); } @@ -33,7 +34,8 @@ public void GetItemsFromSavedSearchesPage() var stubSearchRepository = new Mock(); var stubAddSearchListItem = new Mock(); var stubResources = new Mock(); - var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object); + var savedSearchesMediator = new Mock().Object; + var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object, savedSearchesMediator); var savedSearches = new List { new Mock().Object, @@ -52,25 +54,12 @@ public void GetItemsFromSavedSearchesPageWhenNoSearches() var stubSearchRepository = new Mock(); var stubAddSearchListItem = new Mock(); var stubResources = new Mock(); - var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object); + var savedSearchesMediator = new Mock().Object; + var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object, savedSearchesMediator); var items = savedSearchesPage.GetItems(); Assert.AreEqual(1, items.Length); } - [TestMethod] - [TestCategory("Unit")] - public void AddingSearchSaved() - { - var stubSearchPageFactory = new Mock(); - var stubSearchRepository = new Mock(); - var stubAddSearchListItem = new Mock(); - var stubResources = new Mock(); - var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object); - - savedSearchesPage.OnSearchSaved(this, new SearchCandidate()); - stubSearchRepository.Verify(x => x.GetSavedSearches(), Times.Once); - } - [TestMethod] [TestCategory("Unit")] public void RemoveSavedSearch() @@ -79,12 +68,12 @@ public void RemoveSavedSearch() var stubSearchRepository = new Mock(); var stubAddSearchListItem = new Mock(); var stubResources = new Mock(); - var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object); + var savedSearchesMediator = new SavedSearchesMediator(); + var savedSearchesPage = new SavedSearchesPage(stubSearchPageFactory.Object, stubSearchRepository.Object, stubResources.Object, stubAddSearchListItem.Object, savedSearchesMediator); var search = new Mock().Object; var savedSearchesPostRemove = new List(); - savedSearchesPage.OnSearchRemoved(this, true); - stubSearchRepository.Verify(x => x.GetSavedSearches(), Times.Once); + savedSearchesMediator.RemoveSearch(search); stubSearchRepository.Setup(x => x.GetSavedSearches()).ReturnsAsync(savedSearchesPostRemove); var items = savedSearchesPage.GetItems(); diff --git a/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs b/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs index 2756304..b326f25 100644 --- a/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs +++ b/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs @@ -51,7 +51,8 @@ public async Task SaveSearchForm_ShouldRetainIsTopLevel_WhenSearchIsSaved() }); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(mockSearchRepository.Object, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -78,7 +79,7 @@ public async Task SaveSearchForm_ShouldRetainIsTopLevel_WhenSearchIsSaved() Assert.IsNotNull(capturedSearch); - var saveSearchForm2 = new SaveSearchForm(capturedSearch, mockSearchRepository.Object, stubResources); + var saveSearchForm2 = new SaveSearchForm(capturedSearch, mockSearchRepository.Object, stubResources, savedSearchesMediator); mockSearchRepository .Setup(repo => repo.IsTopLevel(capturedSearch)) .Returns(Task.FromResult(true)); @@ -99,7 +100,8 @@ public async Task SaveSearchForm_ShouldRemoveFromTopLevel_WhenIsTopLevelUnchecke await dataManager.UpdateSearchTopLevelStatus(dummySearch, true); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(dummySearch, dataManager, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(dummySearch, dataManager, stubResources, savedSearchesMediator); var initialTopLevelSearches = await dataManager.GetTopLevelSearches(); Assert.IsTrue(initialTopLevelSearches.Any(s => s.Name == "Dummy Search" && s.SearchString == "dummy search 2")); @@ -147,7 +149,8 @@ public async Task Integration_ShouldAddTopLevelCommand_FromSavedSearchesPage() using var dataManager = new PersistentDataManager(stubValidator, dataStoreOptions); var stubResources = new Mock().Object; - var saveSearchForm = new SaveSearchForm(dataManager, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var saveSearchForm = new SaveSearchForm(dataManager, stubResources, savedSearchesMediator); var jsonPayload = JsonNode.Parse(@" { @@ -200,13 +203,15 @@ public async Task Integration_ShouldAddAndEditSearch_ToBeTopLevel() using var dataManager = new PersistentDataManager(stubValidator, dataStoreOptions); var stubResources = new Mock().Object; + var savedSearchesMediator = new SavedSearchesMediator(); var savedSearchesPage = new SavedSearchesPage( mockSearchPageFactory.Object, dataManager, stubResources, - mockAddSearchListItem.Object); + mockAddSearchListItem.Object, + savedSearchesMediator); - var initialSaveSearchForm = new SaveSearchForm(dataManager, stubResources); + var initialSaveSearchForm = new SaveSearchForm(dataManager, stubResources, savedSearchesMediator); var initialJsonPayload = JsonNode.Parse(@" { @@ -226,7 +231,7 @@ public async Task Integration_ShouldAddAndEditSearch_ToBeTopLevel() var savedSearch = savedSearches.FirstOrDefault(s => s.Name == "My Regular Search"); Assert.IsNotNull(savedSearch, "Saved search should exist"); - var editSearchForm = new SaveSearchForm(savedSearch, dataManager, stubResources); + var editSearchForm = new SaveSearchForm(savedSearch, dataManager, stubResources, savedSearchesMediator); var editJsonPayload = JsonNode.Parse(@" { @@ -284,6 +289,8 @@ public async Task Integration_ShouldRemoveTopLevelCommand_FromSavedSearchesPage( var initialSavedSearches = await dataManager.GetSavedSearches(); var initialTopLevelSearches = await dataManager.GetTopLevelSearches(); + var savedSearchesMediator = new SavedSearchesMediator(); + Assert.IsTrue( initialSavedSearches.Any(s => s.Name == "Top Level Search" && @@ -301,9 +308,10 @@ public async Task Integration_ShouldRemoveTopLevelCommand_FromSavedSearchesPage( mockSearchPageFactory.Object, dataManager, stubResources, - mockAddSearchListItem.Object); + mockAddSearchListItem.Object, + savedSearchesMediator); - var removeCommand = new RemoveSavedSearchCommand(topLevelSearch, dataManager, stubResources); + var removeCommand = new RemoveSavedSearchCommand(topLevelSearch, dataManager, stubResources, savedSearchesMediator); removeCommand.Invoke(); await Task.Delay(1000); @@ -366,8 +374,8 @@ public async Task Integration_ShouldRemoveTopLevelCommand_FromTopLevel() "Search should be in top level searches initially"); var stubResources = new Mock().Object; - - var removeCommand = new RemoveSavedSearchCommand(topLevelSearch, dataManager, stubResources); + var savedSearchesMediator = new SavedSearchesMediator(); + var removeCommand = new RemoveSavedSearchCommand(topLevelSearch, dataManager, stubResources, savedSearchesMediator); removeCommand.Invoke(); await Task.Delay(1000); @@ -390,7 +398,8 @@ public async Task Integration_ShouldRemoveTopLevelCommand_FromTopLevel() mockSearchPageFactory.Object, dataManager, stubResources, - mockAddSearchListItem.Object); + mockAddSearchListItem.Object, + savedSearchesMediator); var savedSearchesItems = savedSearchesPage.GetItems(); var containsRemovedSearch = false; @@ -449,18 +458,20 @@ public async Task Integration_ShouldEditNonTopLevelSearch_ToBeTopLevel() s.SearchString == "is:issue label:bug"), "Search should not be in top level searches initially"); + var savedSearchesMediator = new SavedSearchesMediator(); var stubResources = new Mock().Object; var savedSearchesPage = new SavedSearchesPage( mockSearchPageFactory.Object, dataManager, stubResources, - mockAddSearchListItem.Object); + mockAddSearchListItem.Object, + savedSearchesMediator); var savedSearch = initialSavedSearches.First(s => s.Name == "Bug Reports" && s.SearchString == "is:issue label:bug"); - var editSearchForm = new SaveSearchForm(savedSearch, dataManager, stubResources); + var editSearchForm = new SaveSearchForm(savedSearch, dataManager, stubResources, savedSearchesMediator); var editJsonPayload = JsonNode.Parse(@" { diff --git a/GitHubExtension/Controls/AuthenticationMediator.cs b/GitHubExtension/Controls/AuthenticationMediator.cs new file mode 100644 index 0000000..db19be7 --- /dev/null +++ b/GitHubExtension/Controls/AuthenticationMediator.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using GitHubExtension.Helpers; + +namespace GitHubExtension.Controls; + +public class AuthenticationMediator +{ + public event EventHandler? SignInAction; + + public event EventHandler? SignOutAction; + + public AuthenticationMediator() + { + } + + public void SignIn(SignInStatusChangedEventArgs args) + { + SignInAction?.Invoke(this, args); + } + + public void SignOut(SignInStatusChangedEventArgs args) + { + SignOutAction?.Invoke(this, args); + } +} diff --git a/GitHubExtension/Controls/Commands/RemoveSavedSearchCommand.cs b/GitHubExtension/Controls/Commands/RemoveSavedSearchCommand.cs index 6335596..a193584 100644 --- a/GitHubExtension/Controls/Commands/RemoveSavedSearchCommand.cs +++ b/GitHubExtension/Controls/Commands/RemoveSavedSearchCommand.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using GitHubExtension.Controls.Pages; using GitHubExtension.Helpers; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Foundation; @@ -12,18 +11,15 @@ namespace GitHubExtension.Controls.Commands; public partial class RemoveSavedSearchCommand : InvokableCommand { private readonly ISearch savedSearch; - private readonly ISearchRepository _searchRepository; private readonly IResources _resources; + private readonly SavedSearchesMediator _savedSearchesMediator; - public static event TypedEventHandler? SearchRemoving; - - public static event TypedEventHandler? SearchRemoved; - - public RemoveSavedSearchCommand(ISearch search, ISearchRepository searchRepository, IResources resources) + public RemoveSavedSearchCommand(ISearch search, ISearchRepository searchRepository, IResources resources, SavedSearchesMediator savedSearchesMediator) { _searchRepository = searchRepository; _resources = resources; + _savedSearchesMediator = savedSearchesMediator; savedSearch = new SearchCandidate(search.SearchString, search.Name); Name = _resources.GetResource("Commands_Remove_Saved_Search"); @@ -32,17 +28,17 @@ public RemoveSavedSearchCommand(ISearch search, ISearchRepository searchReposito public override CommandResult Invoke() { - SearchRemoving?.Invoke(this, null); Task.Run(async () => await RemoveSavedSearch()) .ContinueWith(task => { if (task.IsFaulted) { - SearchRemoved?.Invoke(this, task.Exception); + ExtensionHost.LogMessage(new LogMessage() { Message = $"Error removing saved search: {task.Exception?.GetBaseException().Message}" }); + _savedSearchesMediator.RemoveSearch(task.Exception!); } else { - SearchRemoved?.Invoke(this, task.Result); + _savedSearchesMediator.RemoveSearch(task.Result); } }); diff --git a/GitHubExtension/Controls/Forms/SaveSearchForm.cs b/GitHubExtension/Controls/Forms/SaveSearchForm.cs index ea8b6b3..c57ca72 100644 --- a/GitHubExtension/Controls/Forms/SaveSearchForm.cs +++ b/GitHubExtension/Controls/Forms/SaveSearchForm.cs @@ -14,19 +14,22 @@ namespace GitHubExtension.Controls.Forms; public sealed partial class SaveSearchForm : FormContent, IGitHubForm { - public static event EventHandler? SearchSaved; - private readonly ISearch _savedSearch; private readonly ISearchRepository _searchRepository; + private readonly IResources _resources; + private readonly SavedSearchesMediator _savedSearchesMediator; + private string IsTopLevelChecked => GetIsTopLevel().Result.ToString().ToLower(CultureInfo.InvariantCulture); public event EventHandler? LoadingStateChanged; public event EventHandler? FormSubmitted; + public event EventHandler? SearchSaved; + public Dictionary TemplateSubstitutions => new() { { "{{SaveSearchFormTitle}}", _resources.GetResource(string.IsNullOrEmpty(_savedSearch.Name) ? "Forms_Save_Search" : "Forms_Edit_Search") }, @@ -42,19 +45,21 @@ public sealed partial class SaveSearchForm : FormContent, IGitHubForm }; // for saving a new query - public SaveSearchForm(ISearchRepository searchRepository, IResources resources) + public SaveSearchForm(ISearchRepository searchRepository, IResources resources, SavedSearchesMediator savedSearchesMediator) { _resources = resources; _savedSearch = new SearchCandidate(); _searchRepository = searchRepository; + _savedSearchesMediator = savedSearchesMediator; } // for editing an existing query - public SaveSearchForm(ISearch savedSearch, ISearchRepository searchRepository, IResources resources) + public SaveSearchForm(ISearch savedSearch, ISearchRepository searchRepository, IResources resources, SavedSearchesMediator savedSearchesMediator) { _resources = resources; _savedSearch = savedSearch; _searchRepository = searchRepository; + _savedSearchesMediator = savedSearchesMediator; } public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("SaveSearch", TemplateSubstitutions); diff --git a/GitHubExtension/Controls/Forms/SignInForm.cs b/GitHubExtension/Controls/Forms/SignInForm.cs index cfa6ea0..814aa6b 100644 --- a/GitHubExtension/Controls/Forms/SignInForm.cs +++ b/GitHubExtension/Controls/Forms/SignInForm.cs @@ -12,26 +12,26 @@ namespace GitHubExtension.Controls.Forms; public partial class SignInForm : FormContent, IGitHubForm { - public static event EventHandler? SignInAction; - public event EventHandler? LoadingStateChanged; public event EventHandler? FormSubmitted; private readonly IDeveloperIdProvider _developerIdProvider; private readonly IResources _resources; + private readonly AuthenticationMediator _authenticationMediator; private bool _isButtonEnabled = true; private string IsButtonEnabled => _isButtonEnabled.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture); - public SignInForm(IDeveloperIdProvider developerIdProvider, IResources resources) + public SignInForm(IDeveloperIdProvider developerIdProvider, IResources resources, AuthenticationMediator authenticationMediator) { _resources = resources; _developerIdProvider = developerIdProvider; _developerIdProvider.OAuthRedirected += DeveloperIdProvider_OAuthRedirected; - SignOutForm.SignOutAction += SignOutForm_SignOutAction; + _authenticationMediator = authenticationMediator; + _authenticationMediator.SignOutAction += SignOutForm_SignOutAction; } private void SignOutForm_SignOutAction(object? sender, SignInStatusChangedEventArgs e) @@ -45,7 +45,7 @@ private void DeveloperIdProvider_OAuthRedirected(object? sender, Exception? e) { SetButtonEnabled(true); LoadingStateChanged?.Invoke(this, false); - SignInAction?.Invoke(this, new SignInStatusChangedEventArgs(false, e)); + _authenticationMediator.SignIn(new SignInStatusChangedEventArgs(false, e)); FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, e)); return; } @@ -80,14 +80,14 @@ public override ICommandResult SubmitForm(string inputs, string data) { var signInSucceeded = HandleSignIn().Result; LoadingStateChanged?.Invoke(this, false); - SignInAction?.Invoke(this, new SignInStatusChangedEventArgs(signInSucceeded, null)); + _authenticationMediator.SignIn(new SignInStatusChangedEventArgs(signInSucceeded, null)); FormSubmitted?.Invoke(this, new FormSubmitEventArgs(signInSucceeded, null)); } catch (Exception ex) { LoadingStateChanged?.Invoke(this, false); SetButtonEnabled(true); - SignInAction?.Invoke(this, new SignInStatusChangedEventArgs(false, ex)); + _authenticationMediator.SignIn(new SignInStatusChangedEventArgs(false, ex)); FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); } }); diff --git a/GitHubExtension/Controls/Forms/SignOutForm.cs b/GitHubExtension/Controls/Forms/SignOutForm.cs index 683bf6c..2965a28 100644 --- a/GitHubExtension/Controls/Forms/SignOutForm.cs +++ b/GitHubExtension/Controls/Forms/SignOutForm.cs @@ -11,19 +11,19 @@ namespace GitHubExtension.Controls.Forms; public sealed partial class SignOutForm : FormContent, IGitHubForm { - public static event EventHandler? SignOutAction; - public event EventHandler? LoadingStateChanged; public event EventHandler? FormSubmitted; private readonly IDeveloperIdProvider _developerIdProvider; private readonly IResources _resources; + private readonly AuthenticationMediator _authenticationMediator; - public SignOutForm(IDeveloperIdProvider developerIdProvider, IResources resources) + public SignOutForm(IDeveloperIdProvider developerIdProvider, IResources resources, AuthenticationMediator authenticationMediator) { _developerIdProvider = developerIdProvider; _resources = resources; + _authenticationMediator = authenticationMediator; } public Dictionary TemplateSubstitutions => new() @@ -54,7 +54,7 @@ public override ICommandResult SubmitForm(string inputs, string data) var signOutSucceeded = !_developerIdProvider.GetLoggedInDeveloperIdsInternal().Any(); LoadingStateChanged?.Invoke(this, false); - SignOutAction?.Invoke(this, new SignInStatusChangedEventArgs(!signOutSucceeded, null)); + _authenticationMediator.SignOut(new SignInStatusChangedEventArgs(!signOutSucceeded, null)); FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null)); } catch (Exception ex) @@ -62,7 +62,7 @@ public override ICommandResult SubmitForm(string inputs, string data) LoadingStateChanged?.Invoke(this, false); // if sign out fails, the user is still signed in (true) - SignOutAction?.Invoke(this, new SignInStatusChangedEventArgs(true, ex)); + _authenticationMediator.SignOut(new SignInStatusChangedEventArgs(true, ex)); FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); } }); diff --git a/GitHubExtension/Controls/ListItems/AddSearchListItem.cs b/GitHubExtension/Controls/ListItems/AddSearchListItem.cs index 24280cf..8179a8d 100644 --- a/GitHubExtension/Controls/ListItems/AddSearchListItem.cs +++ b/GitHubExtension/Controls/ListItems/AddSearchListItem.cs @@ -11,8 +11,6 @@ namespace GitHubExtension.Controls.ListItems; public partial class AddSearchListItem : ListItem { public AddSearchListItem(SaveSearchPage page, IResources resources) - - // : base(new SaveSearchPage(new SaveSearchForm(SearchInput.SearchString), new StatusMessage(), "Search saved successfully!", "Error in saving search")) : base(page) { Title = resources.GetResource("ListItems_AddSearch"); diff --git a/GitHubExtension/Controls/Pages/SaveSearchPage.cs b/GitHubExtension/Controls/Pages/SaveSearchPage.cs index 49bffc0..0466ad3 100644 --- a/GitHubExtension/Controls/Pages/SaveSearchPage.cs +++ b/GitHubExtension/Controls/Pages/SaveSearchPage.cs @@ -1,41 +1,41 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Resources; -using GitHubExtension.Controls.Forms; -using GitHubExtension.Helpers; -using Microsoft.CommandPalette.Extensions; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace GitHubExtension.Controls.Pages; - -public sealed partial class SaveSearchPage : ContentPage -{ - private readonly SaveSearchForm _saveSearchForm; - private readonly StatusMessage _statusMessage; - private readonly string _successMessage; - private readonly string _errorMessage; - - public SaveSearchPage(SaveSearchForm saveSearchForm, StatusMessage statusMessage, string successMessage, string errorMessage, string saveSearchPageTitle) - { - _saveSearchForm = saveSearchForm; - _statusMessage = statusMessage; - _successMessage = successMessage; +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Resources; +using GitHubExtension.Controls.Forms; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Pages; + +public sealed partial class SaveSearchPage : ContentPage +{ + private readonly SaveSearchForm _saveSearchForm; + private readonly StatusMessage _statusMessage; + private readonly string _successMessage; + private readonly string _errorMessage; + + public SaveSearchPage(SaveSearchForm saveSearchForm, StatusMessage statusMessage, string successMessage, string errorMessage, string saveSearchPageTitle) + { + _saveSearchForm = saveSearchForm; + _statusMessage = statusMessage; + _successMessage = successMessage; _errorMessage = errorMessage; Icon = new IconInfo("\uecc8"); - Title = saveSearchPageTitle; - - // Wire up events using the helper - FormEventHelper.WireFormEvents(_saveSearchForm, this, _statusMessage, _successMessage, _errorMessage); - - // Hide status message initially - ExtensionHost.HideStatus(_statusMessage); - } - - public override IContent[] GetContent() - { - ExtensionHost.HideStatus(_statusMessage); - return [_saveSearchForm]; - } -} + Title = saveSearchPageTitle; + + // Wire up events using the helper + FormEventHelper.WireFormEvents(_saveSearchForm, this, _statusMessage, _successMessage, _errorMessage); + + // Hide status message initially + ExtensionHost.HideStatus(_statusMessage); + } + + public override IContent[] GetContent() + { + ExtensionHost.HideStatus(_statusMessage); + return [_saveSearchForm]; + } +} diff --git a/GitHubExtension/Controls/Pages/SavedSearchesPage.cs b/GitHubExtension/Controls/Pages/SavedSearchesPage.cs index 2aa03ec..cba20b0 100644 --- a/GitHubExtension/Controls/Pages/SavedSearchesPage.cs +++ b/GitHubExtension/Controls/Pages/SavedSearchesPage.cs @@ -19,58 +19,32 @@ public partial class SavedSearchesPage : ListPage private readonly ISearchPageFactory _searchPageFactory; private readonly ISearchRepository _searchRepository; + private readonly IResources _resources; + private readonly SavedSearchesMediator _savedSearchesMediator; + public SavedSearchesPage( ISearchPageFactory searchPageFactory, ISearchRepository searchRepository, IResources resources, - IListItem addSearchListItem) + IListItem addSearchListItem, + SavedSearchesMediator savedSearchesMediator) { _resources = resources; Icon = new IconInfo("\ue721"); Name = _resources.GetResource("Pages_Saved_Searches"); - SaveSearchForm.SearchSaved += OnSearchSaved; - RemoveSavedSearchCommand.SearchRemoved += OnSearchRemoved; - RemoveSavedSearchCommand.SearchRemoving += OnSearchRemoving; + _savedSearchesMediator = savedSearchesMediator; + _savedSearchesMediator.SearchRemoved += OnSearchRemoved; + _savedSearchesMediator.SearchRemoving += OnSearchRemoving; _searchPageFactory = searchPageFactory; _searchRepository = searchRepository; _addSearchListItem = addSearchListItem; + _savedSearchesMediator.SearchSaved += OnSearchSaved; } - public override IListItem[] GetItems() - { - var savedSearches = _searchRepository.GetSavedSearches().Result; - if (savedSearches.Any()) - { - var searchPages = savedSearches.Select(savedSearch => _searchPageFactory.CreateItemForSearch(savedSearch)).ToList(); - - searchPages.Add(_addSearchListItem); - - return searchPages.ToArray(); - } - else - { - return [_addSearchListItem]; - } - } - - // Change this to public to facilitate tests. As the event handler is - // listening to a static event, it is not possible to mock the event. - public void OnSearchSaved(object? sender, object? args) - { - IsLoading = false; - - if (args != null && args is SearchCandidate) - { - RaiseItemsChanged(_searchRepository.GetSavedSearches().Result.Count()); - } - - // errors are handled in SaveSearchPage - } - - public void OnSearchRemoved(object sender, object? args) + private void OnSearchRemoved(object? sender, object? args) { IsLoading = false; @@ -86,7 +60,7 @@ public void OnSearchRemoved(object sender, object? args) } else if (args is true) { - RaiseItemsChanged(_searchRepository.GetSavedSearches().Result.Count()); + RaiseItemsChanged(0); } else if (args is false) { @@ -100,8 +74,39 @@ public void OnSearchRemoved(object sender, object? args) } } - private void OnSearchRemoving(object sender, object? args) + private void OnSearchRemoving(object? sender, object? e) { IsLoading = true; } + + public override IListItem[] GetItems() + { + var savedSearches = _searchRepository.GetSavedSearches().Result; + if (savedSearches.Any()) + { + var searchPages = savedSearches.Select(savedSearch => _searchPageFactory.CreateItemForSearch(savedSearch)).ToList(); + + searchPages.Add(_addSearchListItem); + + return searchPages.ToArray(); + } + else + { + return [_addSearchListItem]; + } + } + + // Change this to public to facilitate tests. As the event handler is + // listening to a static event, it is not possible to mock the event. + public void OnSearchSaved(object? sender, object? args) + { + IsLoading = false; + + if (args != null && args is SearchCandidate) + { + RaiseItemsChanged(0); + } + + // errors are handled in SaveSearchPage + } } diff --git a/GitHubExtension/Controls/Pages/SearchPages/SearchPageFactory.cs b/GitHubExtension/Controls/Pages/SearchPages/SearchPageFactory.cs index 85ab653..5101e1c 100644 --- a/GitHubExtension/Controls/Pages/SearchPages/SearchPageFactory.cs +++ b/GitHubExtension/Controls/Pages/SearchPages/SearchPageFactory.cs @@ -15,13 +15,15 @@ public class SearchPageFactory : ISearchPageFactory { private readonly ICacheDataManager _cacheDataManager; private readonly ISearchRepository _searchRepository; - private readonly IResources _resources; + private readonly IResources _resources; + private readonly SavedSearchesMediator _savedSearchesMediator; - public SearchPageFactory(ICacheDataManager cacheDataManager, ISearchRepository searchRepository, IResources resources) + public SearchPageFactory(ICacheDataManager cacheDataManager, ISearchRepository searchRepository, IResources resources, SavedSearchesMediator savedSearchesMediator) { _cacheDataManager = cacheDataManager; _searchRepository = searchRepository; - _resources = resources; + _resources = resources; + _savedSearchesMediator = savedSearchesMediator; } private ListPage CreatePageForSearch(ISearch search) @@ -43,10 +45,10 @@ public IListItem CreateItemForSearch(ISearch search) Icon = new IconInfo(GitHubIcon.IconDictionary[$"{search.Type}"]), MoreCommands = new CommandContextItem[] { - new(new RemoveSavedSearchCommand(search, _searchRepository, _resources)), + new(new RemoveSavedSearchCommand(search, _searchRepository, _resources, _savedSearchesMediator)), new(new EditSearchPage( _resources, - new SaveSearchForm(search, _searchRepository, _resources), + new SaveSearchForm(search, _searchRepository, _resources, _savedSearchesMediator), new StatusMessage(), _resources.GetResource("Pages_Search_Edited_Success"), _resources.GetResource("Pages_Search_Edited_Failed"))), diff --git a/GitHubExtension/Controls/SavedSearchesMediator.cs b/GitHubExtension/Controls/SavedSearchesMediator.cs new file mode 100644 index 0000000..2799f59 --- /dev/null +++ b/GitHubExtension/Controls/SavedSearchesMediator.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace GitHubExtension.Controls; + +public class SavedSearchesMediator +{ + public event EventHandler? SearchRemoving; + + public event EventHandler? SearchRemoved; + + public event EventHandler? SearchSaved; + + public SavedSearchesMediator() + { + } + + public void RemovingSearch(object args) + { + SearchRemoving?.Invoke(this, args); + } + + public void RemoveSearch(object args) + { + SearchRemoved?.Invoke(this, args); + } + + public void AddSearch(object args) + { + SearchSaved?.Invoke(this, args); + } +} diff --git a/GitHubExtension/GitHubExtension.cs b/GitHubExtension/GitHubExtension.cs index aa355a8..5b22b14 100644 --- a/GitHubExtension/GitHubExtension.cs +++ b/GitHubExtension/GitHubExtension.cs @@ -14,6 +14,8 @@ public sealed partial class GitHubExtension : IExtension { private readonly ManualResetEvent _extensionDisposedEvent; + public event EventHandler? Release; + private readonly CommandProvider _commandProvider; private readonly ILogger _log = Log.ForContext("SourceContext", nameof(GitHubExtension)); @@ -39,5 +41,5 @@ public object GetProvider(ProviderType providerType) } } - public void Dispose() => this._extensionDisposedEvent.Set(); + public void Dispose() => Release?.Invoke(this, _extensionDisposedEvent); } diff --git a/GitHubExtension/GitHubExtensionCommandsProvider.cs b/GitHubExtension/GitHubExtensionCommandsProvider.cs index 48b403e..ecf3686 100644 --- a/GitHubExtension/GitHubExtensionCommandsProvider.cs +++ b/GitHubExtension/GitHubExtensionCommandsProvider.cs @@ -4,8 +4,6 @@ using System.Diagnostics; using GitHubExtension.Controls; -using GitHubExtension.Controls.Commands; -using GitHubExtension.Controls.Forms; using GitHubExtension.Controls.Pages; using GitHubExtension.DeveloperId; using GitHubExtension.Helpers; @@ -23,6 +21,8 @@ public partial class GitHubExtensionCommandsProvider : CommandProvider private readonly ISearchRepository _persistentDataManager; private readonly ISearchPageFactory _searchPageFactory; private readonly IResources _resources; + private readonly SavedSearchesMediator _savedSearchesMediator; + private readonly AuthenticationMediator _authenticationMediator; public GitHubExtensionCommandsProvider( SavedSearchesPage savedSearchesPage, @@ -31,7 +31,9 @@ public GitHubExtensionCommandsProvider( IDeveloperIdProvider developerIdProvider, ISearchRepository persistentDataManager, IResources resources, - ISearchPageFactory searchPageFactory) + ISearchPageFactory searchPageFactory, + SavedSearchesMediator savedSearchesMediator, + AuthenticationMediator authenticationMediator) { _savedSearchesPage = savedSearchesPage; _signOutPage = signOutPage; @@ -40,21 +42,22 @@ public GitHubExtensionCommandsProvider( _persistentDataManager = persistentDataManager; _resources = resources; _searchPageFactory = searchPageFactory; + _savedSearchesMediator = savedSearchesMediator; + _authenticationMediator = authenticationMediator; DisplayName = _resources.GetResource("ExtensionTitle"); - // Static events here. Hard dependency. But maybe it is ok in this case - SignInForm.SignInAction += OnSignInStatusChanged; - SignOutForm.SignOutAction += OnSignInStatusChanged; - SaveSearchForm.SearchSaved += OnSearchSaved; - RemoveSavedSearchCommand.SearchRemoved += OnSearchRemoved; + _authenticationMediator.SignInAction += OnSignInStatusChanged; + _authenticationMediator.SignOutAction += OnSignInStatusChanged; + _savedSearchesMediator.SearchSaved += OnSearchSaved; + _savedSearchesMediator.SearchRemoved += OnSearchRemoved; // This async method raises the RaiseItemsChanged event to update the top-level commands // So it is safe if we let it run asynchronously as "fire and forget" _ = UpdateSignInStatus(IsSignedIn()); } - private void OnSearchRemoved(object sender, object args) + private void OnSearchRemoved(object? sender, object? args) { if (args is bool isRemoved && isRemoved) { @@ -91,9 +94,7 @@ public override ICommandItem[] TopLevelCommands() }; } - List commands; - commands = GetTopLevelSearchCommands().GetAwaiter().GetResult().ToList(); - + var commands = GetTopLevelSearchCommands().GetAwaiter().GetResult().ToList(); var defaultCommands = new List { new(_savedSearchesPage) diff --git a/GitHubExtension/GlobalSuppressions.cs b/GitHubExtension/GlobalSuppressions.cs index 8804f5b..0c4928c 100644 --- a/GitHubExtension/GlobalSuppressions.cs +++ b/GitHubExtension/GlobalSuppressions.cs @@ -33,4 +33,3 @@ [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Static methods may improve performance but decrease maintainability")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Renaming everything would be a lot of work. It does not do any harm if an EventHandler delegate ends with the suffix EventHandler. Besides this, the Rule causes some false positives.")] [assembly: SuppressMessage("Performance", "CA1838:Avoid 'StringBuilder' parameters for P/Invokes", Justification = "We are not concerned about the performance impact of marshaling a StringBuilder")] -[assembly: SuppressMessage("Performance", "CA1852:Seal internal types", Justification = "The assembly is getting a ComVisible set to false already.", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] diff --git a/GitHubExtension/Program.cs b/GitHubExtension/Program.cs index 431195a..66dc649 100644 --- a/GitHubExtension/Program.cs +++ b/GitHubExtension/Program.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using GitHubExtension.Client; +using GitHubExtension.Controls; using GitHubExtension.Controls.Forms; using GitHubExtension.Controls.ListItems; using GitHubExtension.Controls.Pages; @@ -29,6 +30,8 @@ public class Program { private static DeveloperIdProvider? _developerIdProvider; + private static List? _disposables; + [MTAThread] public static async Task Main(string[] args) { @@ -126,24 +129,40 @@ private static async Task HandleCOMServerActivationAsync() var decoratorFactory = new DecoratorFactory(gitHubDataManager); var cacheDataManager = new CacheDataManagerFacade(cacheManager, gitHubDataManager, decoratorFactory); - var searchPageFactory = new SearchPageFactory(cacheDataManager, searchRepository, resources); + var savedSearchesMediator = new SavedSearchesMediator(); + + var searchPageFactory = new SearchPageFactory(cacheDataManager, searchRepository, resources, savedSearchesMediator); - var addSearchListItem = new AddSearchListItem(new SaveSearchPage(new SaveSearchForm(searchRepository, resources), new StatusMessage(), resources.GetResource("Message_Search_Saved"), resources.GetResource("Message_Search_Saved_Error"), resources.GetResource("ListItems_AddSearch")), resources); + var addSearchForm = new SaveSearchForm(searchRepository, resources, savedSearchesMediator); + var addSearchListItem = new AddSearchListItem(new SaveSearchPage(addSearchForm, new StatusMessage(), resources.GetResource("Message_Search_Saved"), resources.GetResource("Message_Search_Saved_Error"), resources.GetResource("ListItems_AddSearch")), resources); - var savedSearchesPage = new SavedSearchesPage(searchPageFactory, searchRepository, resources, addSearchListItem); + var savedSearchesPage = new SavedSearchesPage(searchPageFactory, searchRepository, resources, addSearchListItem, savedSearchesMediator); - var signOutPage = new SignOutPage(new SignOutForm(developerIdProvider, resources), new StatusMessage(), resources.GetResource("Message_Sign_Out_Success"), resources.GetResource("Message_Sign_Out_Fail")); - var signInPage = new SignInPage(new SignInForm(developerIdProvider, resources), new StatusMessage(), resources.GetResource("Message_Sign_In_Success"), resources.GetResource("Message_Sign_In_Fail")); + var authenticationMediator = new AuthenticationMediator(); - var commandProvider = new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, developerIdProvider, searchRepository, resources, searchPageFactory); + var signOutForm = new SignOutForm(developerIdProvider, resources, authenticationMediator); + var signOutPage = new SignOutPage(signOutForm, new StatusMessage(), resources.GetResource("Message_Sign_Out_Success"), resources.GetResource("Message_Sign_Out_Fail")); + var signInForm = new SignInForm(developerIdProvider, resources, authenticationMediator); + var signInPage = new SignInPage(signInForm, new StatusMessage(), resources.GetResource("Message_Sign_In_Success"), resources.GetResource("Message_Sign_In_Fail")); + + var commandProvider = new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, developerIdProvider, searchRepository, resources, searchPageFactory, savedSearchesMediator, authenticationMediator); var extensionInstance = new GitHubExtension(extensionDisposedEvent, commandProvider); + _disposables = new List + { + gitHubDataManager, + searchRepository, + cacheManager, + }; + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. // This makes sure that only one instance of GitHubExtension is alive, which is returned every time the host asks for the IExtension object. // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. server.RegisterClass(() => extensionInstance); server.Start(); + extensionInstance.Release += HandleExtensionInstanceRelease; + // END OF COMPOSITION ROOT AREA // This will make the main thread wait until the event is signalled by the extension class. @@ -151,5 +170,17 @@ private static async Task HandleCOMServerActivationAsync() extensionDisposedEvent.WaitOne(); } + private static void HandleExtensionInstanceRelease(object? sender, ManualResetEvent e) => DecompositionRoot(_disposables!, e); + + public static void DecompositionRoot(List disposables, ManualResetEvent extensionDisposedEvent) + { + foreach (var disposable in disposables) + { + disposable.Dispose(); + } + + extensionDisposedEvent.Set(); + } + private static void HandleProtocolActivation(Uri oauthRedirectUri) => _developerIdProvider?.HandleOauthRedirection(oauthRedirectUri); }