Skip to content
Merged
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
48 changes: 48 additions & 0 deletions Assets/Tests/Editor/ToolSettingsSectionLayoutSignatureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.Presentation;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Characterization tests for Tool Settings list layout signatures.
/// </summary>
public sealed class ToolSettingsSectionLayoutSignatureTests
{
[Test]
public void Create_WhenGroupsHaveTools_IncludesGroupMarkersAndToolFields()
{
// Pins the rebuild signature format used to decide whether the ListView must rebuild.
ToolSettingsSectionData data = new(
showToolSettings: true,
builtInTools: new[]
{
new ToolToggleItem("compile", true, false, "Compile the project")
},
thirdPartyTools: new[]
{
new ToolToggleItem("vendor.tool", false, true, "Vendor tool")
},
isRegistryAvailable: true);

string signature = ToolSettingsSectionLayoutSignature.Create(data);

Assert.That(signature, Is.EqualTo("B:compile|Compile the project|;T:vendor.tool|Vendor tool|;"));
}

[Test]
public void Create_WhenGroupsAreEmpty_KeepsEmptyGroupMarkers()
{
// Pins empty-group signatures so collapsed/empty catalogs stay stable across refreshes.
ToolSettingsSectionData data = new(
showToolSettings: true,
builtInTools: System.Array.Empty<ToolToggleItem>(),
thirdPartyTools: System.Array.Empty<ToolToggleItem>(),
isRegistryAvailable: true);

string signature = ToolSettingsSectionLayoutSignature.Create(data);

Assert.That(signature, Is.EqualTo("B:;T:;"));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using System;
using System.Threading;
using System.Threading.Tasks;

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

using io.github.hatayama.UnityCliLoop.Application;
using io.github.hatayama.UnityCliLoop.Domain;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Presentation
{
/// <summary>
/// Owns Setup Wizard CLI-step workflow state and async install/repair operations.
/// </summary>
internal sealed class SetupWizardCliWorkflowController
{
private readonly SetupWizardCliStepPresenter _cliStepPresenter;
private readonly CliSetupApplicationService _cliSetupApplicationService;
private readonly Action<bool> _refreshUi;

private bool _isInstallingCli;
private bool _needsCliPathSetup;

internal SetupWizardCliWorkflowController(
VisualElement cliStatusIcon,
Label cliStatusLabel,
Button installCliButton,
CliSetupApplicationService cliSetupApplicationService,
Action<bool> refreshUi)
{
Debug.Assert(cliSetupApplicationService != null, "cliSetupApplicationService must not be null");
Debug.Assert(refreshUi != null, "refreshUi must not be null");

_cliSetupApplicationService = cliSetupApplicationService
?? throw new ArgumentNullException(nameof(cliSetupApplicationService));
_refreshUi = refreshUi
?? throw new ArgumentNullException(nameof(refreshUi));

_cliStepPresenter = new SetupWizardCliStepPresenter(
cliStatusIcon,
cliStatusLabel,
installCliButton,
HandleInstallCli);
}

internal void ShowChecking()
{
_cliStepPresenter.ShowChecking();
}

internal async Task<bool> RefreshAndUpdateAsync(CancellationToken ct)
{
await _cliSetupApplicationService.ForceRefreshCliVersionAsync(ct);
_needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct);
string cliVersion = _cliSetupApplicationService.GetCachedCliVersion();
UpdateFromCachedState(cliVersion);
return IsCliInstalled(cliVersion);
}

private void UpdateFromCachedState(string cliVersion = null)
{
if (cliVersion == null)
{
cliVersion = _cliSetupApplicationService.GetCachedCliVersion();
}

bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher();
string requiredCliVersion = GetMinimumRequiredCliVersion();
bool cliInstalled = IsCliInstalled(cliVersion);
_cliStepPresenter.Update(
cliInstalled,
cliVersion,
cliIsDispatcher,
requiredCliVersion,
_isInstallingCli,
_needsCliPathSetup);
}

private void HandleInstallCli()
{
HandleInstallCliAsync(CancellationToken.None).Forget();
}

private async Task HandleInstallCliAsync(CancellationToken ct)
{
await RefreshCliPrimaryActionStateAsync(ct);

string cliVersion = _cliSetupApplicationService.GetCachedCliVersion();
bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher();
CliSetupCompatibilityState state = EvaluateCliSetupCompatibilityForSetupWizard(
cliVersion,
cliIsDispatcher,
GetMinimumRequiredCliVersion());
if (SetupWizardWindow.ShouldRepairCliPathFromPrimaryButton(_needsCliPathSetup, state.NeedsUpdate))
{
await HandleRepairCliPathSetup(ct);
return;
}

bool wasCliInstalledBeforeInstall = _cliSetupApplicationService.IsCliInstalled();
_needsCliPathSetup = false;
_isInstallingCli = true;
_cliStepPresenter.Update(
cliInstalled: false,
cliVersion: null,
cliIsDispatcher: false,
requiredCliVersion: GetMinimumRequiredCliVersion(),
isInstallingCli: _isInstallingCli,
needsCliPathSetup: _needsCliPathSetup);

try
{
CliInstallResult result = await _cliSetupApplicationService.InstallGlobalCliAsync(
UnityEngine.Application.platform,
ct);

if (!result.Success)
{
NativeCliInstallCommand command = _cliSetupApplicationService.GetGlobalCliInstallCommand(
UnityEngine.Application.platform,
true);
EditorUtility.DisplayDialog(
"Installation Failed",
$"Failed to install uloop CLI.\n\n{result.ErrorOutput}\n\n"
+ $"You can install manually:\n {command.ManualCommand}",
"OK");
return;
}

await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync(
UnityEngine.Application.platform,
_cliSetupApplicationService,
ct);
_needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct);
}
finally
{
_isInstallingCli = false;
_refreshUi(CliInstallRefreshPolicy.ShouldRefreshSkillsAfterCliInstall(
wasCliInstalledBeforeInstall));
}
}

private async Task RefreshCliPrimaryActionStateAsync(CancellationToken ct)
{
_cliStepPresenter.ShowRefreshingPrimaryAction();

try
{
await _cliSetupApplicationService.ForceRefreshCliVersionAsync(ct);
_needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct);
}
finally
{
RefreshCliStepFromCachedState();
}
}

private void RefreshCliStepFromCachedState()
{
UpdateFromCachedState();
}

private async Task HandleRepairCliPathSetup(CancellationToken ct)
{
_isInstallingCli = true;
_cliStepPresenter.Update(
cliInstalled: true,
cliVersion: _cliSetupApplicationService.GetCachedCliVersion(),
cliIsDispatcher: _cliSetupApplicationService.GetCachedCliIsDispatcher(),
requiredCliVersion: GetMinimumRequiredCliVersion(),
isInstallingCli: _isInstallingCli,
needsCliPathSetup: _needsCliPathSetup);

try
{
await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync(
UnityEngine.Application.platform,
_cliSetupApplicationService,
ct);
_needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct);
}
finally
{
_isInstallingCli = false;
_refreshUi(true);
}
}

private async Task<bool> ShouldRepairCliPathSetupAsync(CancellationToken ct)
{
bool hasPackageOwnedCurrentUserInstall =
_cliSetupApplicationService.HasPackageOwnedCurrentUserInstall(UnityEngine.Application.platform);
if (!SetupWizardWindow.ShouldCheckCliPathSetupForSetupWizard(
UnityEngine.Application.platform,
hasPackageOwnedCurrentUserInstall))
{
return false;
}

bool isCliVisibleFromShell = await _cliSetupApplicationService.IsCliVisibleFromShellAsync(
UnityEngine.Application.platform,
ct);
return !isCliVisibleFromShell;
}

private static CliSetupCompatibilityState EvaluateCliSetupCompatibilityForSetupWizard(
string cliVersion,
bool cliIsDispatcher,
string requiredCliVersion)
{
return CliSetupCompatibility.Evaluate(
cliVersion,
cliIsDispatcher,
requiredCliVersion);
}

private string GetMinimumRequiredCliVersion()
{
return _cliSetupApplicationService.GetMinimumRequiredCliVersion();
}

private static bool IsCliInstalled(string cliVersion)
{
return !string.IsNullOrEmpty(cliVersion);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading