Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions GitHubExtension.Test/Controls/WorkflowRunsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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.Text.Json;
using GitHubExtension.Controls;
using GitHubExtension.Controls.Forms;
using GitHubExtension.Controls.Pages;
using GitHubExtension.DataManager;
using GitHubExtension.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Moq;

namespace GitHubExtension.Test.Controls;

[TestClass]
public class WorkflowRunsTests
{
private static Mock<IResources> CreateResources()
{
var resources = new Mock<IResources>();
resources.Setup(x => x.GetResource(It.IsAny<string>(), null)).Returns<string, object>((key, _) => key);
return resources;
}

private static Mock<IWorkflowRun> CreateRun(long id, string status, string conclusion)
{
var run = new Mock<IWorkflowRun>();
run.Setup(x => x.Id).Returns(id);
run.Setup(x => x.Name).Returns("CI");
run.Setup(x => x.RunNumber).Returns(id);
run.Setup(x => x.Status).Returns(status);
run.Setup(x => x.Conclusion).Returns(conclusion);
run.Setup(x => x.HeadBranch).Returns("main");
run.Setup(x => x.HtmlUrl).Returns($"https://github.com/owner/repo/actions/runs/{id}");
return run;
}

[TestMethod]
[TestCategory("Unit")]
public void TriggerWorkflowForm_TemplateJson_IsValidJson()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
var form = new TriggerWorkflowForm(dataManager.Object, CreateResources().Object, "owner/repo");

var json = form.TemplateJson;

var parsed = JsonDocument.Parse(json);
Assert.AreEqual("AdaptiveCard", parsed.RootElement.GetProperty("type").GetString());
}

[TestMethod]
[TestCategory("Unit")]
public void WorkflowRunsPage_EmptyQuery_ShowsPrompt()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
var page = new WorkflowRunsPage(dataManager.Object, new WorkflowRunsMediator(), CreateResources().Object);

var items = page.GetItems();

Assert.AreEqual(1, items.Length);
Assert.AreEqual("Pages_WorkflowRuns_Prompt", items[0].Title);
dataManager.Verify(x => x.GetWorkflowRunsAsync(It.IsAny<string>()), Times.Never);
}

[TestMethod]
[TestCategory("Unit")]
public void WorkflowRunsPage_WithRuns_ReturnsItems()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
dataManager.Setup(x => x.GetWorkflowRunsAsync("owner/repo"))
.ReturnsAsync(new[] { CreateRun(1, "completed", "success").Object, CreateRun(2, "completed", "failure").Object });

var page = new WorkflowRunsPage(dataManager.Object, new WorkflowRunsMediator(), CreateResources().Object)
{
SearchText = "owner/repo",
};

var items = page.GetItems();

Assert.AreEqual(2, items.Length);
}

[TestMethod]
[TestCategory("Unit")]
public void WorkflowRunsPage_InProgressRun_HasCancelCommand()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
dataManager.Setup(x => x.GetWorkflowRunsAsync("owner/repo"))
.ReturnsAsync(new[] { CreateRun(1, "in_progress", string.Empty).Object });

var page = new WorkflowRunsPage(dataManager.Object, new WorkflowRunsMediator(), CreateResources().Object)
{
SearchText = "owner/repo",
};

var item = (ListItem)page.GetItems()[0];

// Re-run + Cancel + Copy URL
Assert.AreEqual(3, item.MoreCommands.Length);
}

[TestMethod]
[TestCategory("Unit")]
public void WorkflowRunsPage_CompletedRun_HasNoCancelCommand()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
dataManager.Setup(x => x.GetWorkflowRunsAsync("owner/repo"))
.ReturnsAsync(new[] { CreateRun(1, "completed", "success").Object });

var page = new WorkflowRunsPage(dataManager.Object, new WorkflowRunsMediator(), CreateResources().Object)
{
SearchText = "owner/repo",
};

var item = (ListItem)page.GetItems()[0];

// Re-run + Copy URL (no cancel for a completed run)
Assert.AreEqual(2, item.MoreCommands.Length);
}

[TestMethod]
[TestCategory("Unit")]
public void WorkflowRunsPage_NoRuns_ShowsNoneMessage()
{
var dataManager = new Mock<IWorkflowRunsDataManager>();
dataManager.Setup(x => x.GetWorkflowRunsAsync("owner/repo"))
.ReturnsAsync(Array.Empty<IWorkflowRun>());

var page = new WorkflowRunsPage(dataManager.Object, new WorkflowRunsMediator(), CreateResources().Object)
{
SearchText = "owner/repo",
};

var items = page.GetItems();

Assert.AreEqual(1, items.Length);
Assert.AreEqual("Pages_WorkflowRuns_None", items[0].Title);
}
}
5 changes: 4 additions & 1 deletion GitHubExtension.Test/Helpers/TestSetupHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ public static GitHubExtensionCommandsProvider CreateGitHubExtensionCommandsProvi
mockNotificationsDataManager.Setup(x => x.GetUnreadCountAsync()).ReturnsAsync(0);
var notificationsPage = new NotificationsPage(mockNotificationsDataManager.Object, notificationsMediator, mockResources);
var mockCreateManager = new Mock<IGitHubCreateManager>().Object;
return new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, mockDeveloperIdProvider, persistentDataManager, mockResources, searchPageFactory, savedSearchesMediator, mockAuthenticationMediator, notificationsMediator, mockCreateManager);
var workflowRunsMediator = new WorkflowRunsMediator();
var mockWorkflowRunsDataManager = new Mock<IWorkflowRunsDataManager>();
var workflowRunsPage = new WorkflowRunsPage(mockWorkflowRunsDataManager.Object, workflowRunsMediator, mockResources);
return new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, mockDeveloperIdProvider, persistentDataManager, mockResources, searchPageFactory, savedSearchesMediator, mockAuthenticationMediator, notificationsMediator, mockCreateManager, workflowRunsPage, mockWorkflowRunsDataManager.Object);
}
}
7 changes: 7 additions & 0 deletions GitHubExtension/Controls/Commands/LinkCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ internal LinkCommand(INotification notification, IResources resources)
Icon = new IconInfo("\uE8A7");
}

internal LinkCommand(string htmlUrl, IResources resources)
{
_htmlUrl = htmlUrl;
Name = resources.GetResource("Commands_Open_Link");
Icon = new IconInfo("\uE8A7");
}

public override CommandResult Invoke()
{
Process.Start(new ProcessStartInfo(_htmlUrl) { UseShellExecute = true });
Expand Down
51 changes: 51 additions & 0 deletions GitHubExtension/Controls/Commands/WorkflowRunActionCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.DataManager;
using GitHubExtension.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Serilog;

namespace GitHubExtension.Controls.Commands;

// Runs a workflow-run action (re-run or cancel) against GitHub, showing a
// success or error toast and notifying the workflow-runs mediator so the open
// page can refresh. Non-destructive actions use this command directly;
// destructive actions wrap it in a ConfirmedCommand.
internal sealed partial class WorkflowRunActionCommand : InvokableCommand
{
private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(WorkflowRunActionCommand));

private readonly Func<Task> _action;
private readonly string _successMessage;
private readonly string _errorMessage;
private readonly WorkflowRunsMediator _mediator;

internal WorkflowRunActionCommand(string name, IconInfo icon, Func<Task> action, string successMessage, string errorMessage, WorkflowRunsMediator mediator)
{
Name = name;
Icon = icon;
_action = action;
_successMessage = successMessage;
_errorMessage = errorMessage;
_mediator = mediator;
}

public override CommandResult Invoke()
{
try
{
_action().GetAwaiter().GetResult();
ToastHelper.ShowSuccessToast(_successMessage);
_mediator.NotifyWorkflowRunsChanged();
}
catch (Exception ex)
{
_log.Error(ex, "Workflow run action failed.");
ToastHelper.ShowErrorToast(_errorMessage);
}

return CommandResult.KeepOpen();
}
}
77 changes: 77 additions & 0 deletions GitHubExtension/Controls/Forms/TriggerWorkflowForm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.Text.Json;
using System.Text.Json.Nodes;
using GitHubExtension.DataManager;
using GitHubExtension.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

namespace GitHubExtension.Controls.Forms;

public sealed partial class TriggerWorkflowForm : FormContent, IGitHubForm
{
private readonly IWorkflowRunsDataManager _dataManager;
private readonly IResources _resources;
private readonly string _initialRepository;

public event EventHandler<bool>? LoadingStateChanged;

public event EventHandler<FormSubmitEventArgs>? FormSubmitted;

public TriggerWorkflowForm(IWorkflowRunsDataManager dataManager, IResources resources, string initialRepository = "")
{
_dataManager = dataManager;
_resources = resources;
_initialRepository = initialRepository;
}

public Dictionary<string, string> TemplateSubstitutions => new()
{
{ "{{TriggerWorkflowFormTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_Title")) },
{ "{{RepositoryLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryLabel")) },
{ "{{RepositoryPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryPlaceholder")) },
{ "{{RepositoryErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryError")) },
{ "{{RepositoryValue}}", JsonSerializer.Serialize(_initialRepository) },
{ "{{WorkflowFileLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_WorkflowFileLabel")) },
{ "{{WorkflowFilePlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_WorkflowFilePlaceholder")) },
{ "{{WorkflowFileErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_WorkflowFileError")) },
{ "{{RefLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_RefLabel")) },
{ "{{RefPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_RefPlaceholder")) },
{ "{{RefErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_RefError")) },
{ "{{RefValue}}", JsonSerializer.Serialize("main") },
{ "{{TriggerWorkflowActionTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_TriggerWorkflow_Action")) },
};

public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("TriggerWorkflow", TemplateSubstitutions);

public override ICommandResult SubmitForm(string? inputs, string data)
{
LoadingStateChanged?.Invoke(this, true);
_ = SubmitInternalAsync(inputs);
return CommandResult.KeepOpen();
}

private async Task SubmitInternalAsync(string? inputs)
{
try
{
var payload = JsonNode.Parse(inputs ?? string.Empty) ?? throw new InvalidOperationException("No input provided.");
var repository = payload["Repository"]?.ToString() ?? string.Empty;
var workflowFile = payload["WorkflowFile"]?.ToString() ?? string.Empty;
var gitRef = payload["Ref"]?.ToString() ?? string.Empty;

await _dataManager.TriggerWorkflowAsync(repository, workflowFile, gitRef);

LoadingStateChanged?.Invoke(this, false);
FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null));
}
catch (Exception ex)
{
LoadingStateChanged?.Invoke(this, false);
FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex));
}
}
}
28 changes: 28 additions & 0 deletions GitHubExtension/Controls/IWorkflowRun.cs
Original file line number Diff line number Diff line change
@@ -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.

namespace GitHubExtension.Controls;

public interface IWorkflowRun
{
long Id { get; }

string Name { get; }

string Status { get; }

string Conclusion { get; }

string HeadBranch { get; }

string TriggerEvent { get; }

long RunNumber { get; }

string HtmlUrl { get; }

string RepositoryFullName { get; }

DateTimeOffset CreatedAt { get; }
}
Loading
Loading