Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2555e9b
Added SetButton component
marcsyp Aug 16, 2019
9dead28
Create Separator Component
marcsyp Aug 19, 2019
8142065
Reverted Create Separator Component Commit
marcsyp Aug 19, 2019
fdd0055
Create Separator Component
marcsyp Aug 20, 2019
3719f07
Create Separator Component - Updates
marcsyp Aug 20, 2019
b6f4840
Updated Styling for Simple Grid
marcsyp Aug 20, 2019
1b88f64
Updated HorizontalAlignment to Stretch
marcsyp Aug 21, 2019
33fb5cb
Updated default thickness to 0.5
marcsyp Aug 21, 2019
57f09ec
merge in Set_Button
andrewheumann Sep 14, 2019
8cdfcce
Merged in set-button (pull request #15)
marcsyp Sep 14, 2019
aa6af3c
Merged in marcsyp/humanui-2019/psd-cleanup (pull request #17)
marcsyp Sep 14, 2019
6a0f55c
merged in marcsyp/humanui-2019/create-separator (pull request #18)
andrewheumann Sep 14, 2019
920175f
merge w master
andrewheumann Sep 14, 2019
4f7f45e
Merged in marcsyp/humanui-2019/simple-grid-updates (pull request #19)
marcsyp Sep 14, 2019
235d8a3
add CreateBorder, version bump
andrewheumann Nov 10, 2019
74bb8a3
get rid of wix, deployment stuff
andrewheumann Apr 30, 2020
f2f4d37
more stuff!!!!!!!!!!!!!!!!!!!!!!
andrewheumann Apr 30, 2020
25b60f6
Added auto column sizing in the DataTable component
karakasa Dec 5, 2020
637e9ca
Merge pull request #1 from karakasa/master
andrewheumann Dec 5, 2020
fba80a2
Added WebBrouser uri as value
max-parametrica Jan 19, 2021
e8d3409
Merge pull request #2 from Parametrica-team/BrowserListener
andrewheumann Jan 26, 2021
c279673
Check for null GH canvas
sbaer Apr 6, 2021
0e2e5fb
Merge pull request #3 from sbaer/sbaer/null_canvas_check
andrewheumann Apr 7, 2021
37dc454
update changelog, new dist
andrewheumann Apr 7, 2021
fbd15f1
Added textbox Enter event
max-malein Aug 18, 2021
f3a5e70
Enter menu is disabled when textbox button is on but submit on Enter …
max-malein Oct 9, 2021
7b5cf93
slider bug fix, version bump
andrewheumann Dec 17, 2021
8d0e48e
Merge pull request #4 from Parametrica-team/textBoxEnterEvent
andrewheumann Feb 14, 2022
272ca57
Modernize for Rhino 8 / .NET 7 and fix MahApps 2.x runtime regressions
andylebihan May 16, 2026
1aeabd8
Add HumanUI.Tests suite and master-snapshot regression tooling
andylebihan May 16, 2026
f7c6015
Fix three Window-lifecycle failure modes in LaunchWindow
andylebihan May 16, 2026
7b1fb21
Make user X-click hide the HumanUI window, not destroy it
andylebihan May 16, 2026
7dc402c
Debounce ValueListener solves and stop cross-instance state cross-talk
andylebihan May 16, 2026
e769244
Rebuild 0.8.10 yak against released Rhino 8.0 (8.0.23304.9001)
andylebihan May 23, 2026
47fd637
Mark upstream master as merged — those 28 commits were incorporated v…
andylebihan May 24, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ HumanUI/HumanUI/bin/
HUI_Install/bin/
HUI_Install/obj/
HumanUIBaseApp/HumanUIBaseApp/bin/*
HumanUI.Tests/bin/
HumanUI.Tests/obj/
HumanUI.Tests/diag*.log
HumanUI.Tests/diag.host*

# NuGet Dependencies
packages/
Expand Down
102 changes: 102 additions & 0 deletions HumanUI.Tests/ComponentSmokeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Grasshopper.Kernel;
using Xunit;

namespace HumanUI.Tests
{
/// <summary>
/// Smoke tests over every GH_Component subclass in the HumanUI assembly. These sit above
/// the UI toolkit, so they survive a WPF -> Eto migration: they exercise the Grasshopper
/// metadata layer (Name, Nickname, Category, ComponentGuid uniqueness) and instantiability.
/// </summary>
public class ComponentSmokeTests
{
private static readonly Type[] ComponentTypes = LoadComponentTypes();

private static Type[] LoadComponentTypes()
{
Assembly asm = typeof(HumanUIInfo).Assembly;
return asm.GetTypes()
.Where(t => !t.IsAbstract
&& typeof(GH_Component).IsAssignableFrom(t)
&& t.GetConstructor(Type.EmptyTypes) != null)
.ToArray();
}

public static IEnumerable<object[]> AllComponentTypes()
=> ComponentTypes.Select(t => new object[] { t });

[Fact]
public void Assembly_ExposesAtLeastOneComponent()
{
Assert.NotEmpty(ComponentTypes);
}

[Fact]
public void AssemblyInfo_IsWellFormed()
{
var info = new HumanUIInfo();
Assert.Equal("Human UI", info.Name);
Assert.False(string.IsNullOrWhiteSpace(info.Version), "Version must be set");
Assert.NotEqual(Guid.Empty, info.Id);
Assert.False(string.IsNullOrWhiteSpace(info.AuthorName));
}

[Theory]
[MemberData(nameof(AllComponentTypes))]
public void Component_InstantiatesViaParameterlessCtor(Type componentType)
{
object instance = Activator.CreateInstance(componentType);
Assert.NotNull(instance);
Assert.IsAssignableFrom<GH_Component>(instance);
}

[Theory]
[MemberData(nameof(AllComponentTypes))]
public void Component_HasNonEmptyName(Type componentType)
{
var component = (GH_Component)Activator.CreateInstance(componentType);
Assert.False(string.IsNullOrWhiteSpace(component.Name),
$"{componentType.FullName} has an empty Name");
Assert.False(string.IsNullOrWhiteSpace(component.NickName),
$"{componentType.FullName} has an empty NickName");
}

[Theory]
[MemberData(nameof(AllComponentTypes))]
public void Component_LivesInHumanUiCategory(Type componentType)
{
var component = (GH_Component)Activator.CreateInstance(componentType);
Assert.Equal("Human UI", component.Category);
}

[Theory]
[MemberData(nameof(AllComponentTypes))]
public void Component_HasNonEmptyComponentGuid(Type componentType)
{
var component = (GH_Component)Activator.CreateInstance(componentType);
Assert.NotEqual(Guid.Empty, component.ComponentGuid);
}

[Fact]
public void ComponentGuids_AreUniqueAcrossAssembly()
{
var guidsByType = ComponentTypes
.Select(t => new { Type = t, Component = (GH_Component)Activator.CreateInstance(t) })
.Select(x => new { x.Type, x.Component.ComponentGuid })
.ToList();

var duplicates = guidsByType
.GroupBy(x => x.ComponentGuid)
.Where(g => g.Count() > 1)
.Select(g => $"{g.Key}: {string.Join(", ", g.Select(x => x.Type.FullName))}")
.ToList();

Assert.True(duplicates.Count == 0,
"Duplicate ComponentGuids:\n" + string.Join("\n", duplicates));
}
}
}
115 changes: 115 additions & 0 deletions HumanUI.Tests/ContractSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Grasshopper.Kernel;
using Xunit;

namespace HumanUI.Tests
{
/// <summary>
/// Snapshot-style regression test over the GH-facing public contract of every component
/// in the HumanUI assembly. The shape of inputs/outputs and the component GUID are what
/// user .gh files encode against -- any change here can silently break saved files. This
/// captures the contract into a checked-in JSON baseline and fails on any drift.
///
/// To intentionally update the baseline (e.g. after adding a component), set the env var
/// HUMANUI_REGENERATE_SNAPSHOT=1 and run the test. It will overwrite the baseline and
/// report the path; review the diff in source control before committing.
/// </summary>
public class ContractSnapshotTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
};

[Fact]
public void Snapshot_MatchesBaseline()
{
ComponentSnapshot[] current = BuildSnapshot();
string actual = JsonSerializer.Serialize(current, JsonOptions);

string baselinePath = ResolveBaselinePath();

bool regenerate = Environment.GetEnvironmentVariable("HUMANUI_REGENERATE_SNAPSHOT") == "1";
if (regenerate || !File.Exists(baselinePath))
{
File.WriteAllText(baselinePath, actual);
Assert.Fail(File.Exists(baselinePath) && !regenerate
? $"Baseline did not exist; wrote a fresh one at {baselinePath}. Review and commit it, then re-run."
: $"Baseline rewritten at {baselinePath} because HUMANUI_REGENERATE_SNAPSHOT=1. Review the diff and commit.");
}

string expected = File.ReadAllText(baselinePath);
Assert.Equal(NormalizeNewlines(expected), NormalizeNewlines(actual));
}

private static ComponentSnapshot[] BuildSnapshot()
{
Assembly asm = typeof(HumanUIInfo).Assembly;
return asm.GetTypes()
.Where(t => !t.IsAbstract
&& typeof(GH_Component).IsAssignableFrom(t)
&& t.GetConstructor(Type.EmptyTypes) != null)
.Select(t => (GH_Component)Activator.CreateInstance(t))
.Select(c => new ComponentSnapshot
{
Type = c.GetType().FullName,
Guid = c.ComponentGuid.ToString("D"),
Name = c.Name,
NickName = c.NickName,
Category = c.Category,
SubCategory = c.SubCategory,
Exposure = c.Exposure.ToString(),
Inputs = c.Params.Input.Select(ToParamSnapshot).ToArray(),
Outputs = c.Params.Output.Select(ToParamSnapshot).ToArray(),
})
.OrderBy(s => s.Guid, StringComparer.Ordinal)
.ToArray();
}

private static ParamSnapshot ToParamSnapshot(IGH_Param param) => new()
{
Type = param.GetType().FullName,
Name = param.Name,
NickName = param.NickName,
Description = param.Description,
Access = param.Access.ToString(),
Optional = param.Optional,
};

private static string ResolveBaselinePath([CallerFilePath] string thisFile = null)
=> Path.Combine(Path.GetDirectoryName(thisFile)!, "contract-baseline.json");

private static string NormalizeNewlines(string s) => s.Replace("\r\n", "\n");

private sealed class ComponentSnapshot
{
public string Type { get; set; }
public string Guid { get; set; }
public string Name { get; set; }
public string NickName { get; set; }
public string Category { get; set; }
public string SubCategory { get; set; }
public string Exposure { get; set; }
public ParamSnapshot[] Inputs { get; set; }
public ParamSnapshot[] Outputs { get; set; }
}

private sealed class ParamSnapshot
{
public string Type { get; set; }
public string Name { get; set; }
public string NickName { get; set; }
public string Description { get; set; }
public string Access { get; set; }
public bool Optional { get; set; }
}
}
}
73 changes: 73 additions & 0 deletions HumanUI.Tests/HUI_UtilTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace HumanUI.Tests
{
public class HUI_UtilTests
{
[Fact]
public void BoolsFromString_RoundTripsThroughStringFromBools()
{
// stringFromBools emits a trailing comma; after a split the final empty token
// gets parsed back as a (default) False. The historical format always carries
// that extra False, so test that the original list is a prefix of the round trip.
var input = new List<bool> { true, false, true, true, false };
string serialized = HUI_Util.stringFromBools(input);
List<bool> roundTripped = HUI_Util.boolsFromString(serialized);
Assert.Equal(input, roundTripped.Take(input.Count));
Assert.Equal(input.Count + 1, roundTripped.Count);
Assert.False(roundTripped[^1]);
}

[Fact]
public void StringFromBools_EmitsCommaSeparatedTrailingComma()
{
var input = new List<bool> { true, false, true };
Assert.Equal("True,False,True,", HUI_Util.stringFromBools(input));
}

[Fact]
public void StringFromBools_EmptyListReturnsEmptyString()
{
Assert.Equal(string.Empty, HUI_Util.stringFromBools(new List<bool>()));
}

[Fact]
public void BoolsFromString_UnparseableTokensBecomeFalse()
{
// Boolean.TryParse on "garbage" leaves bl=false, which is the historical behavior we want to preserve.
List<bool> result = HUI_Util.boolsFromString("True,garbage,False");
Assert.Equal(new List<bool> { true, false, false }, result);
}

[Fact]
public void StringFromStrings_JoinsWithPipe()
{
var input = new List<string> { "alpha", "beta", "gamma" };
Assert.Equal("alpha|beta|gamma", HUI_Util.stringFromStrings(input));
}

[Fact]
public void StringsFromString_SplitsOnPipe()
{
List<string> result = HUI_Util.stringsFromString("alpha|beta|gamma");
Assert.Equal(new List<string> { "alpha", "beta", "gamma" }, result);
}

[Fact]
public void StringFromStrings_RoundTripsThroughStringsFromString()
{
var input = new List<string> { "one", "two with spaces", "three" };
string serialized = HUI_Util.stringFromStrings(input);
List<string> roundTripped = HUI_Util.stringsFromString(serialized);
Assert.Equal(input, roundTripped);
}

[Fact]
public void StringFromStrings_EmptyListReturnsEmptyString()
{
Assert.Equal(string.Empty, HUI_Util.stringFromStrings(new List<string>()));
}
}
}
30 changes: 30 additions & 0 deletions HumanUI.Tests/HumanUI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<RootNamespace>HumanUI.Tests</RootNamespace>
<AssemblyName>HumanUI.Tests</AssemblyName>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
<NoWarn>$(NoWarn);NU1701</NoWarn>
<!-- Grasshopper.targets auto-strips Grasshopper.dll/GH_IO.dll from the output. The .gha
must not redistribute them, but the test host needs them at runtime. -->
<ShouldRemoveRhinoReferences>False</ShouldRemoveRhinoReferences>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" />
<PackageReference Include="Xunit.StaFact" Version="1.1.11" />
<!-- Tests need Grasshopper/RhinoCommon at runtime to instantiate components. The
distributed HumanUI.gha still excludes these (see HumanUI.csproj). -->
<PackageReference Include="Grasshopper" Version="8.0.23304.9001" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HumanUI\HumanUI\HumanUI.csproj" />
</ItemGroup>

</Project>
Loading