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
221 changes: 213 additions & 8 deletions Assets/Tests/Editor/CliSetupSectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ public void IsUninstallCliAction_ReturnsExpectedValue(
}

[Test]
public void Update_WhenCliRefreshIsChecking_ShowsSkillsCheckingState()
public void Update_WhenSkillStateIsChecking_ShowsSkillsCheckingState()
{
// Verifies that IsChecking routes the shared skills panel into ShowChecking.
// Verifies that IsSkillStateChecking routes the shared skills panel into ShowChecking.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
CliSetupData data = CreateData(
isCliInstalled: true,
isChecking: true,
isChecking: false,
isSkillStateChecking: true,
selectedTargetInstallState: SkillInstallState.Missing);

section.Update(data);
Expand All @@ -113,6 +114,28 @@ public void Update_WhenCliRefreshIsChecking_ShowsSkillsCheckingState()
Assert.That(groupSkillsToggle.enabledSelf, Is.False);
}

[Test]
public void Update_WhenOnlyCliIsChecking_DoesNotShowSkillsCheckingState()
{
// Verifies CLI refresh checking does not force the skills panel into Checking... state.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
CliSetupData data = CreateData(
isCliInstalled: true,
isChecking: true,
isSkillStateChecking: false,
selectedTargetInstallState: SkillInstallState.Missing);

section.Update(data);

Button installAllSkillsButton = root.Q<Button>("install-all-skills-button");
EnumField skillsTargetField = root.Q<EnumField>("skills-target-field");
Button refreshCliVersionButton = root.Q<Button>("refresh-cli-version-button");
Assert.That(installAllSkillsButton.text, Is.Not.EqualTo("Checking..."));
Assert.That(skillsTargetField.enabledSelf, Is.True);
Assert.That(refreshCliVersionButton.enabledSelf, Is.False);
}

[Test]
public void Update_WhenSkillsStateIsChecking_DisablesSkillsTargetField()
{
Expand All @@ -130,6 +153,186 @@ public void Update_WhenSkillsStateIsChecking_DisablesSkillsTargetField()
Assert.That(skillsTargetField.enabledSelf, Is.False);
}

[Test]
public void Update_WhenNoInstallableTargets_HidesBulkInstallAndShowsGuidance()
{
// Verifies empty detection hides the bulk Install Skills button and shows guidance instead.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
CliSetupData data = CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: new List<SkillSetupTargetInfo>());

section.Update(data);

Button installAllSkillsButton = root.Q<Button>("install-all-skills-button");
Label noTargetsMessage = root.Q<Label>("skills-no-targets-message");
Assert.That(installAllSkillsButton.style.display.value, Is.EqualTo(DisplayStyle.None));
Assert.That(noTargetsMessage.style.display.value, Is.EqualTo(DisplayStyle.Flex));
}

[Test]
public void Update_WhenInstallableTargetsExist_ShowsBulkInstallAndHidesGuidance()
{
// Verifies detected targets keep the bulk Install Skills button and hide empty-state guidance.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
List<SkillSetupTargetInfo> targets = new()
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Missing)
};
CliSetupData data = CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: targets);

section.Update(data);

Button installAllSkillsButton = root.Q<Button>("install-all-skills-button");
Label noTargetsMessage = root.Q<Label>("skills-no-targets-message");
Assert.That(installAllSkillsButton.style.display.value, Is.EqualTo(DisplayStyle.Flex));
Assert.That(noTargetsMessage.style.display.value, Is.EqualTo(DisplayStyle.None));
}

[Test]
public void Update_WhenCheckingTargetsArrive_DoesNotChangeSpecificTargetFoldout()
{
// Verifies Checking updates leave the foldout value untouched until a resolved state arrives.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
Foldout foldout = root.Q<Foldout>("install-specific-target-foldout");
section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Missing)
}));
Assert.That(foldout.value, Is.True);
foldout.SetValueWithoutNotify(false);

section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Checking,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Checking),
CreateSkillTarget("Common", ".agents", SkillInstallState.Missing)
}));

Assert.That(foldout.value, Is.False);
}

[Test]
public void Update_WhenMissingTargetsBecomeInstalled_ClosesSpecificTargetFoldout()
{
// Verifies the foldout closes when reload resolves missing targets to installed.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
Foldout foldout = root.Q<Foldout>("install-specific-target-foldout");
section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Missing)
}));
Assert.That(foldout.value, Is.True);

section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Installed,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Installed)
}));

Assert.That(foldout.value, Is.False);
}

[Test]
public void Update_WhenInstalledDefaultUnchanged_PreservesUserOpenedFoldout()
{
// Verifies a user-opened foldout stays open across later installed-only updates.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
Foldout foldout = root.Q<Foldout>("install-specific-target-foldout");
List<SkillSetupTargetInfo> installedTargets = new()
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Installed)
};
section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Installed,
installableSkillTargets: installedTargets));
Assert.That(foldout.value, Is.False);
foldout.SetValueWithoutNotify(true);

section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Installed,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Installed)
}));

Assert.That(foldout.value, Is.True);
}

[Test]
public void Update_WhenMissingDefaultUnchanged_PreservesUserClosedFoldout()
{
// Verifies a user-closed foldout is not forced open while targets remain missing.
VisualElement root = CreateRootElement();
CliSetupSection section = new(root);
Foldout foldout = root.Q<Foldout>("install-specific-target-foldout");
section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Missing)
}));
Assert.That(foldout.value, Is.True);
foldout.SetValueWithoutNotify(false);

section.Update(CreateData(
isCliInstalled: true,
isChecking: false,
selectedTargetInstallState: SkillInstallState.Missing,
installableSkillTargets: new List<SkillSetupTargetInfo>
{
CreateSkillTarget("Claude", ".claude", SkillInstallState.Missing)
}));

Assert.That(foldout.value, Is.False);
}

private static SkillSetupTargetInfo CreateSkillTarget(
string displayName,
string dirName,
SkillInstallState installState)
{
return new(
displayName,
dirName,
"--flag",
hasSkillsDirectory: true,
hasExistingSkills: true,
hasDifferentLayoutSkills: false,
installState);
}

private static VisualElement CreateRootElement()
{
VisualElement root = new();
Expand All @@ -141,11 +344,13 @@ private static VisualElement CreateRootElement()
installProgress.Add(new Label { name = "cli-install-progress-label" });
root.Add(installProgress);

root.Add(new Button { name = "refresh-skills-state-button" });
VisualElement skillsSubsection = new() { name = "skills-subsection" };
VisualElement skillsSetupPanel = new() { name = "skills-setup-panel" };
skillsSetupPanel.Add(new VisualElement { name = "skill-target-status-list" });
skillsSetupPanel.Add(new VisualElement { name = "skill-target-status-divider" });
skillsSetupPanel.Add(new Label { name = "skill-target-status-summary" });
skillsSetupPanel.Add(new Label { name = "skills-no-targets-message" });
skillsSetupPanel.Add(new Button { name = "install-all-skills-button" });
Foldout specificTargetFoldout = new() { name = "install-specific-target-foldout" };
VisualElement groupSkillsRow = new() { name = "group-skills-row" };
Expand All @@ -154,7 +359,6 @@ private static VisualElement CreateRootElement()
specificTargetFoldout.Add(groupSkillsRow);
VisualElement skillsTargetRow = new() { name = "skills-target-row" };
skillsTargetRow.Add(new EnumField { name = "skills-target-field" });
skillsTargetRow.Add(new Button { name = "refresh-skills-state-button" });
specificTargetFoldout.Add(skillsTargetRow);
specificTargetFoldout.Add(new Button { name = "install-selected-skills-button" });
skillsSetupPanel.Add(specificTargetFoldout);
Expand All @@ -166,7 +370,9 @@ private static VisualElement CreateRootElement()
private static CliSetupData CreateData(
bool isCliInstalled,
bool isChecking,
SkillInstallState selectedTargetInstallState)
SkillInstallState selectedTargetInstallState,
IReadOnlyList<SkillSetupTargetInfo> installableSkillTargets = null,
bool? isSkillStateChecking = null)
{
return new CliSetupData(
isCliInstalled,
Expand All @@ -177,17 +383,16 @@ private static CliSetupData CreateData(
needsCliPathSetup: false,
isInstallingCli: false,
isChecking,
isSkillStateChecking: isSkillStateChecking ?? isChecking,
isClaudeSkillsInstalled: false,
isAgentsSkillsInstalled: false,
isCursorSkillsInstalled: false,
isGeminiSkillsInstalled: false,
isCodexSkillsInstalled: false,
isAntigravitySkillsInstalled: false,
selectedTargetInstallState,
SkillsTarget.Claude,
groupSkillsUnderUnityCliLoop: false,
isInstallingSkills: false,
installableSkillTargets: new List<SkillSetupTargetInfo>());
installableSkillTargets: installableSkillTargets ?? new List<SkillSetupTargetInfo>());
}
}
}
83 changes: 69 additions & 14 deletions Assets/Tests/Editor/SkillsSetupPanelViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void FilterInstallableSkillTargets_ExcludesTargetsWithoutSkillsDirectory(
List<SkillSetupTargetInfo> targets = new()
{
new("Claude Code", ".claude", "--claude", true, true),
new("Cursor", ".cursor", "--cursor", false, false),
new("Common", ".agents", "--agents", false, false),
new("Codex CLI", ".codex", "--codex", true, false, hasDifferentLayoutSkills: true)
};

Expand All @@ -45,10 +45,8 @@ public void CreateFirstInstallSkillTarget_WhenClaudeSelected_ReturnsClaudeProjec
Assert.That(target.HasExistingSkills, Is.False);
}

[TestCase(SkillsTarget.Cursor, "Cursor", ".cursor", "--cursor")]
[TestCase(SkillsTarget.Gemini, "Gemini CLI", ".gemini", "--gemini")]
[TestCase(SkillsTarget.Codex, "Codex CLI", ".codex", "--codex")]
[TestCase(SkillsTarget.Agents, "Other (.agents)", ".agents", "--agents")]
[TestCase(SkillsTarget.Agents, "Common", ".agents", "--agents")]
public void CreateFirstInstallSkillTarget_ReturnsMappedTarget(
SkillsTarget targetType,
string expectedDisplayName,
Expand Down Expand Up @@ -250,18 +248,75 @@ public void BuildInstalledSummaryText_ReturnsInstalledForNTargetsFormat()
Assert.That(summary, Is.EqualTo("Installed for 2 targets"));
}

[TestCase(0, true)]
[TestCase(1, false)]
[TestCase(3, false)]
public void ShouldExpandSpecificTargetFoldout_ReturnsExpectedValue(
int installableTargetCount,
bool expected)
[Test]
public void ShouldExpandSpecificTargetFoldout_WhenNoTargets_ReturnsTrue()
{
// Verifies the specific-target foldout auto-expands only when no installable targets exist.
bool shouldExpand = SkillsSetupPanelView.ShouldExpandSpecificTargetFoldout(
installableTargetCount);
// Verifies the foldout expands when no installable skill targets were detected.
List<SkillSetupTargetInfo> targets = new();

bool shouldExpand = SkillsSetupPanelView.ShouldExpandSpecificTargetFoldout(targets);

Assert.That(shouldExpand, Is.EqualTo(expected));
Assert.That(shouldExpand, Is.True);
}

[Test]
public void ShouldExpandSpecificTargetFoldout_WhenAllInstalled_ReturnsFalse()
{
// Verifies the foldout stays collapsed when every detected target is already installed.
List<SkillSetupTargetInfo> targets = new()
{
CreateTarget("Claude", ".claude", SkillInstallState.Installed),
CreateTarget("Agents", ".agents", SkillInstallState.Installed)
};

bool shouldExpand = SkillsSetupPanelView.ShouldExpandSpecificTargetFoldout(targets);

Assert.That(shouldExpand, Is.False);
}

[Test]
public void ShouldExpandSpecificTargetFoldout_WhenAnyMissing_ReturnsTrue()
{
// Verifies the foldout expands when at least one detected target still needs install.
List<SkillSetupTargetInfo> targets = new()
{
CreateTarget("Claude", ".claude", SkillInstallState.Installed),
CreateTarget("Common", ".agents", SkillInstallState.Missing)
};

bool shouldExpand = SkillsSetupPanelView.ShouldExpandSpecificTargetFoldout(targets);

Assert.That(shouldExpand, Is.True);
}

[Test]
public void ShouldExpandSpecificTargetFoldout_WhenOnlyOutdated_ReturnsFalse()
{
// Verifies outdated targets count as installed for foldout expansion and stay collapsed.
List<SkillSetupTargetInfo> targets = new()
{
CreateTarget("Claude", ".claude", SkillInstallState.Outdated),
CreateTarget("Agents", ".agents", SkillInstallState.Outdated)
};

bool shouldExpand = SkillsSetupPanelView.ShouldExpandSpecificTargetFoldout(targets);

Assert.That(shouldExpand, Is.False);
}

private static SkillSetupTargetInfo CreateTarget(
string displayName,
string dirName,
SkillInstallState installState)
{
return new(
displayName,
dirName,
"--flag",
hasSkillsDirectory: true,
hasExistingSkills: true,
hasDifferentLayoutSkills: false,
installState);
}
}
}
Loading
Loading