Fixes #4177: View.GetAttributeForRole now defers to SuperView for proper attribute hierarchy (#4292)

* Initial plan

* Fix GetAttributeForRole to defer to SuperView when no explicit scheme

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add test for Adornment attribute resolution

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix: Also check SchemeName when deferring to SuperView in GetAttributeForRole

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add test for StatusBar/Bar not deferring when SchemeName is set

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add comprehensive low-level tests for GetAttributeForRole hierarchy

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Update AGENTS.md with PR branch pull instructions

Co-authored-by: tig <585482+tig@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-19 12:19:33 -06:00
committed by GitHub
parent 86b7996598
commit 549436e343
4 changed files with 359 additions and 1 deletions

View File

@@ -269,4 +269,269 @@ public class SchemeTests
view.Dispose ();
}
[Fact]
public void GetAttributeForRole_SubView_DefersToSuperView_WhenNoExplicitScheme ()
{
var parentView = new View { SchemeName = "Base" };
var childView = new View ();
parentView.Add (childView);
// Parent customizes attribute resolution
var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
parentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = customAttribute;
args.Handled = true;
}
};
// Child without explicit scheme should get customized attribute from parent
Assert.Equal (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_SubView_UsesOwnScheme_WhenExplicitlySet ()
{
var parentView = new View { SchemeName = "Base" };
var childView = new View ();
parentView.Add (childView);
// Set explicit scheme on child
var childScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
childView.SetScheme (childScheme);
// Parent customizes attribute resolution
var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
parentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = customAttribute;
args.Handled = true;
}
};
// Child with explicit scheme should NOT get customized attribute from parent
Assert.NotEqual (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (childScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_Adornment_UsesParentScheme ()
{
// Border (an Adornment) doesn't have a SuperView but should use its Parent's scheme
var view = new View { SchemeName = "Dialog" };
var border = view.Border!;
Assert.NotNull (border);
Assert.Null (border.SuperView); // Adornments don't have SuperView
Assert.NotNull (border.Parent);
var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
// Border should use its Parent's scheme, not Base
Assert.Equal (dialogScheme!.Normal, border.GetAttributeForRole (VisualRole.Normal));
view.Dispose ();
}
[Fact]
public void GetAttributeForRole_SubView_UsesSchemeName_WhenSet ()
{
var parentView = new View { SchemeName = "Base" };
var childView = new View ();
parentView.Add (childView);
// Set SchemeName on child (not explicit scheme)
childView.SchemeName = "Dialog";
// Parent customizes attribute resolution
var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
parentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = customAttribute;
args.Handled = true;
}
};
// Child with SchemeName should NOT get customized attribute from parent
// It should use the Dialog scheme instead
var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.NotEqual (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (dialogScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_NestedHierarchy_DefersCorrectly ()
{
// Test: grandchild without explicit scheme defers through parent to grandparent
// Would fail without the SuperView deferral fix (commit 154ac15)
var grandparentView = new View { SchemeName = "Base" };
var parentView = new View (); // No scheme or SchemeName
var childView = new View (); // No scheme or SchemeName
grandparentView.Add (parentView);
parentView.Add (childView);
// Grandparent customizes attributes
var customAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
grandparentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = customAttribute;
args.Handled = true;
}
};
// Child should get attribute from grandparent through parent
Assert.Equal (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
// Parent should also get attribute from grandparent
Assert.Equal (customAttribute, parentView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
grandparentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_ParentWithSchemeNameBreaksChain ()
{
// Test: parent with SchemeName stops deferral chain
// Would fail without the SchemeName check (commit 866e002)
var grandparentView = new View { SchemeName = "Base" };
var parentView = new View { SchemeName = "Dialog" }; // Sets SchemeName
var childView = new View (); // No scheme or SchemeName
grandparentView.Add (parentView);
parentView.Add (childView);
// Grandparent customizes attributes
var customAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
grandparentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = customAttribute;
args.Handled = true;
}
};
// Parent should NOT get grandparent's customization (it has SchemeName)
var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.NotEqual (customAttribute, parentView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (dialogScheme!.Normal, parentView.GetAttributeForRole (VisualRole.Normal));
// Child should get parent's Dialog scheme (defers to parent, parent uses Dialog scheme)
Assert.Equal (dialogScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
grandparentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_OnGettingAttributeForRole_TakesPrecedence ()
{
// Test: view's own OnGettingAttributeForRole takes precedence over parent
// This should work with or without the fix, but validates precedence
var parentView = new View { SchemeName = "Base" };
var childView = new TestViewWithAttributeOverride ();
parentView.Add (childView);
// Parent customizes attributes
var parentAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
parentView.GettingAttributeForRole += (sender, args) =>
{
if (args.Role == VisualRole.Normal)
{
args.Result = parentAttribute;
args.Handled = true;
}
};
// Child's own override should take precedence
var childOverrideAttribute = new Attribute (Color.BrightRed, Color.BrightCyan);
childView.OverrideAttribute = childOverrideAttribute;
Assert.Equal (childOverrideAttribute, childView.GetAttributeForRole (VisualRole.Normal));
childView.Dispose ();
parentView.Dispose ();
}
[Fact]
public void GetAttributeForRole_MultipleRoles_DeferCorrectly ()
{
// Test: multiple VisualRoles all defer correctly
// Would fail without the SuperView deferral fix for any role
var parentView = new View { SchemeName = "Base" };
var childView = new View ();
parentView.Add (childView);
var normalAttr = new Attribute (Color.Red, Color.Blue);
var focusAttr = new Attribute (Color.Green, Color.Yellow);
var hotNormalAttr = new Attribute (Color.Magenta, Color.Cyan);
parentView.GettingAttributeForRole += (sender, args) =>
{
switch (args.Role)
{
case VisualRole.Normal:
args.Result = normalAttr;
args.Handled = true;
break;
case VisualRole.Focus:
args.Result = focusAttr;
args.Handled = true;
break;
case VisualRole.HotNormal:
args.Result = hotNormalAttr;
args.Handled = true;
break;
}
};
// All roles should defer to parent
Assert.Equal (normalAttr, childView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (focusAttr, childView.GetAttributeForRole (VisualRole.Focus));
Assert.Equal (hotNormalAttr, childView.GetAttributeForRole (VisualRole.HotNormal));
childView.Dispose ();
parentView.Dispose ();
}
private class TestViewWithAttributeOverride : View
{
public Attribute? OverrideAttribute { get; set; }
protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
{
if (OverrideAttribute.HasValue && role == VisualRole.Normal)
{
currentAttribute = OverrideAttribute.Value;
return true;
}
return base.OnGettingAttributeForRole (role, ref currentAttribute);
}
}
}