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
72 changes: 72 additions & 0 deletions .github/skills/mtp-test-running/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: mtp-test-running
description: Run MaterialDesignInXamlToolkit tests correctly with dotnet test, TUnit, and Microsoft Testing Platform. Use when validating changes, choosing a safe test scope, or filtering to the exact affected tests.
---

# Test running with MTP

Use this skill when you need to run or suggest test commands in this repository.

## Repository-specific runner setup

1. Test projects use `UseMicrosoftTestingPlatformRunner=true`.
2. Test projects also set `TestingPlatformDotnetTestSupport=true`.
3. `global.json` does **not** opt into `test.runner = Microsoft.Testing.Platform`.
4. That means this repo currently uses `dotnet test` in the legacy CLI mode while delegating execution to MTP-backed test applications.
5. Runner-specific arguments must therefore be passed after an extra `--`.
6. Do **not** rely on top-level `dotnet test --filter ...` for these projects.

## Strong preferences

1. Start with the smallest non-UI test scope that covers the change.
2. Avoid `MaterialDesignThemes.UITests` unless the change is inherently runtime WPF behavior.
3. Only run UI tests after the user explicitly asks or grants permission.
4. When UI tests are necessary, run only the affected tests, never the whole UI suite.

## TUnit filter syntax

TUnit uses `--treenode-filter`.

Format:

`/<Assembly>/<Namespace>/<Class>/<Test>`

- Use `*` as a wildcard within a segment.
- Use `(A)|(B)` inside a segment for OR.
- Prefer exact class and test names over namespace-wide filters.
- Add `--minimum-expected-tests <n>` so an empty selection fails loudly.

## Command patterns

### Targeted non-UI test run

```powershell
dotnet test tests\MaterialDesignThemes.Wpf.Tests\MaterialDesignThemes.Wpf.Tests.csproj -c Release --no-build -- --treenode-filter "/*/*/UpDownButtonsPaddingConverterTests/*" --minimum-expected-tests 1 --no-ansi --no-progress
```

### Exact DecimalUpDown UI validation

```powershell
dotnet test tests\MaterialDesignThemes.UITests\MaterialDesignThemes.UITests.csproj -c Release --no-build -- --treenode-filter "/*/*/DecimalUpDownTests/(UpDownButtonsVisibility_Collapsed_RemovesReservedPadding)|(UpDownButtonsVisibility_Hidden_PreservesReservedPadding)" --minimum-expected-tests 2 --no-ansi --no-progress
```

### Verify a UI filter before running

```powershell
tests\MaterialDesignThemes.UITests\bin\Release\net10.0-windows\MaterialDesignThemes.UITests.exe --list-tests --no-ansi --treenode-filter "/*/*/DecimalUpDownTests/(UpDownButtonsVisibility_Collapsed_RemovesReservedPadding)|(UpDownButtonsVisibility_Hidden_PreservesReservedPadding)"
```

## Mistakes to avoid

- Using top-level `dotnet test --filter ...` for these MTP-backed projects.
- Passing runner-specific flags before the extra `--`.
- Running broad UI suites when one or two targeted tests are enough.
- Forgetting `--minimum-expected-tests`, which can hide a bad filter.

## Useful references

- `global.json`
- `tests/MaterialDesignThemes.Wpf.Tests/MaterialDesignThemes.Wpf.Tests.csproj`
- `tests/MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj`
- https://learn.microsoft.com/dotnet/core/testing/unit-testing-with-dotnet-test
- https://tunit.dev/docs/execution/test-filters
84 changes: 84 additions & 0 deletions .github/skills/xamltest-ui-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: xamltest-ui-tests
description: Write and update WPF UI tests in MaterialDesignThemes.UITests using XAMLTest. Use when template, layout, focus, interaction, or visual behavior needs coverage, or when a reviewer asks to move runtime UI checks out of MaterialDesignThemes.Wpf.Tests.
---

# XAMLTest UI tests

Use this skill when working on runtime WPF behavior in this repository.

## When to use it

- A change touches templates, visual states, focus behavior, layout, coordinates, or input handling.
- You need to inspect named template parts such as `PART_TextBox` or `PART_IncreaseButton`.
- A reviewer asks to use XAMLTest instead of regular unit tests.
- You need to assert WPF-only properties like `Padding`, `Visibility`, `ActualWidth`, or `DataContext` state after layout.

## Repository-specific rules

1. Put runtime UI behavior tests in `tests/MaterialDesignThemes.UITests`.
2. Do **not** add visual-tree or layout interaction tests to `tests/MaterialDesignThemes.Wpf.Tests`.
3. Prefer extending the existing feature file under `tests/MaterialDesignThemes.UITests/WPF/<feature>/` instead of creating a generic catch-all test file.
4. Match the existing file's result-recording pattern; many current UI tests use `TestRecorder`, but it should not be treated as a blanket requirement.
5. Ask permission before running UI tests, and only run the affected ones.

## Standard pattern

1. Inherit from `TestBase`.
2. Use `LoadXaml<T>()` for focused control scenarios.
3. Use `LoadUserControl<T>()` when you need a sample view model, bindings, or additional focus targets.
4. Get named parts with `GetElement<T>("PART_Name")`.
5. Use `RemoteExecute` with static methods for properties that helpers do not expose directly.
6. Use `Wait.For(...)` for layout-sensitive assertions.

## What to prefer

- `GetCoordinates()` for alignment and spacing assertions.
- `RemoteExecute` for `Padding`, `Visibility`, `ActualWidth`, `Margin`, or `DataContext` inspection.
- Keyboard and mouse helpers such as `MoveKeyboardFocus`, `SendKeyboardInput`, and `LeftClick` for interaction.

## What to avoid

- Inspecting the visual tree from `MaterialDesignThemes.Wpf.Tests` for runtime template behavior.
- Overly broad UI suites when a single targeted XAMLTest scenario will do.
- Repeating helper code inline when a small static local or private helper method is enough.

## Example

```csharp
[Test]
public async Task DecimalUpDown_WhenButtonsAreCollapsed_RemovesReservedPadding()
{
await using var recorder = new TestRecorder(App);

Thickness originalPadding = new(4, 5, 6, 7);
var decimalUpDown = await LoadXaml<DecimalUpDown>("""
<materialDesign:DecimalUpDown Padding="4,5,6,7"
Width="160"
UpDownButtonsVisibility="Collapsed" />
""");
var buttonsHost = await decimalUpDown.GetElement<StackPanel>("ButtonsHost");
var textBox = await decimalUpDown.GetElement<TextBox>("PART_TextBox");

await Wait.For(async () =>
{
await Assert.That(await buttonsHost.RemoteExecute(GetButtonsHostVisibility)).IsEqualTo(Visibility.Collapsed);
await Assert.That(await buttonsHost.RemoteExecute(GetButtonsHostActualWidth)).IsEqualTo(0d);
await Assert.That(await textBox.RemoteExecute(GetTextBoxPadding)).IsEqualTo(originalPadding);
});

recorder.Success();
}

private static Visibility GetButtonsHostVisibility(StackPanel stackPanel) => stackPanel.Visibility;

private static double GetButtonsHostActualWidth(StackPanel stackPanel) => stackPanel.ActualWidth;

private static Thickness GetTextBoxPadding(TextBox textBox) => textBox.Padding;
```

## Useful local references

- `tests/MaterialDesignThemes.UITests/TestBase.cs`
- `tests/MaterialDesignThemes.UITests/XamlTestExtensions.cs`
- Existing feature tests under `tests/MaterialDesignThemes.UITests/WPF/`
9 changes: 9 additions & 0 deletions src/MainDemo.Wpf/NumericUpDown.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@
</materialDesign:NumericUpDown.DecreaseContent>
</materialDesign:NumericUpDown>
</smtx:XamlDisplay>

<smtx:XamlDisplay Margin="10,5"
VerticalAlignment="Top"
UniqueKey="decimalUpDown_hiddenButtons">
<materialDesign:DecimalUpDown UpDownButtonsVisibility="Collapsed"
MinWidth="0"
Value="12.5"
Width="80" />
</smtx:XamlDisplay>

</StackPanel>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace MaterialDesignThemes.Wpf.Converters.Internal;

public sealed class UpDownButtonsPaddingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is not [Thickness padding, double buttonsWidth, ..])
{
return DependencyProperty.UnsetValue;
}

if (double.IsNaN(buttonsWidth) || double.IsInfinity(buttonsWidth))
{
return padding;
}

return new Thickness(padding.Left, padding.Top, padding.Right + buttonsWidth, padding.Bottom);
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@
<Setter.Value>
<ControlTemplate TargetType="{x:Type wpf:UpDownBase}">
<ControlTemplate.Resources>
<converters:ThicknessCloneConverter x:Key="NumericUpDownPaddingConverter"
AdditionalOffsetRight="55"
CloneEdges="All" />
<convertersInternal:UpDownButtonsPaddingConverter x:Key="NumericUpDownPaddingConverter" />
<converters:ThicknessCloneConverter x:Key="PartButtonMarginConverter"
CloneEdges="Top,Right,Bottom"
FixedLeft="0" />
Expand All @@ -60,7 +58,6 @@
<Grid>
<TextBox x:Name="PART_TextBox"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding, Converter={StaticResource NumericUpDownPaddingConverter}}"
VerticalAlignment="Stretch"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Expand Down Expand Up @@ -103,21 +100,31 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Focusable="{TemplateBinding Focusable}"
Style="{DynamicResource NestedTextBoxStyle}" />

<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
Style="{DynamicResource NestedTextBoxStyle}">
<TextBox.Padding>
<MultiBinding Converter="{StaticResource NumericUpDownPaddingConverter}">
<Binding Path="Padding" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding ElementName="ButtonsHost" Path="ActualWidth" />
</MultiBinding>
</TextBox.Padding>
</TextBox>

<StackPanel x:Name="ButtonsHost"
HorizontalAlignment="Right"
Orientation="Horizontal"
Visibility="{TemplateBinding UpDownButtonsVisibility}">
<RepeatButton x:Name="PART_DecreaseButton"
Margin="{TemplateBinding Padding, Converter={StaticResource PartButtonMarginConverter}}"
Content="{TemplateBinding DecreaseContent}"
Foreground="{Binding ElementName=PART_TextBox, Path=Foreground}"
Opacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
Style="{DynamicResource NestedNumericUpDownButtonsStyle}" />
Margin="{TemplateBinding Padding, Converter={StaticResource PartButtonMarginConverter}}"
Content="{TemplateBinding DecreaseContent}"
Foreground="{Binding ElementName=PART_TextBox, Path=Foreground}"
Opacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
Style="{DynamicResource NestedNumericUpDownButtonsStyle}" />
<RepeatButton x:Name="PART_IncreaseButton"
Margin="{TemplateBinding Padding, Converter={StaticResource PartButtonMarginConverter}}"
Content="{TemplateBinding IncreaseContent}"
Foreground="{Binding ElementName=PART_TextBox, Path=Foreground}"
Opacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
Style="{DynamicResource NestedNumericUpDownButtonsStyle}" />
Margin="{TemplateBinding Padding, Converter={StaticResource PartButtonMarginConverter}}"
Content="{TemplateBinding IncreaseContent}"
Foreground="{Binding ElementName=PART_TextBox, Path=Foreground}"
Opacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
Style="{DynamicResource NestedNumericUpDownButtonsStyle}" />
</StackPanel>
</Grid>
</Border>
Expand Down
9 changes: 9 additions & 0 deletions src/MaterialDesignThemes.Wpf/UpDownBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,15 @@ public override void OnApplyTemplate()

public void SelectAll() => _textBoxField?.SelectAll();

public Visibility UpDownButtonsVisibility
{
get => (Visibility)GetValue(UpDownButtonsVisibilityProperty);
set => SetValue(UpDownButtonsVisibilityProperty, value);
}

public static readonly DependencyProperty UpDownButtonsVisibilityProperty =
DependencyProperty.Register(nameof(UpDownButtonsVisibility), typeof(Visibility), typeof(UpDownBase), new PropertyMetadata(Visibility.Visible));

public object? IncreaseContent
{
get => GetValue(IncreaseContentProperty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,64 @@ public async Task LostFocusWhenTextIsInvalid_RevertsToOriginalValue(string input

recorder.Success();
}

[Test]
public async Task UpDownButtonsVisibility_Collapsed_RemovesReservedPadding()
{
await using var recorder = new TestRecorder(App);

Thickness originalPadding = new(4, 5, 6, 7);
var decimalUpDown = await LoadXaml<DecimalUpDown>("""
<materialDesign:DecimalUpDown Padding="4,5,6,7"
Width="160"
UpDownButtonsVisibility="Collapsed" />
""");
var buttonsHost = await decimalUpDown.GetElement<StackPanel>("ButtonsHost");
var textBox = await decimalUpDown.GetElement<TextBox>("PART_TextBox");

await Wait.For(async () =>
{
await Assert.That(await buttonsHost.RemoteExecute(GetButtonsHostVisibility)).IsEqualTo(Visibility.Collapsed);
await Assert.That(await buttonsHost.RemoteExecute(GetButtonsHostActualWidth)).IsEqualTo(0d);
await Assert.That(await textBox.RemoteExecute(GetTextBoxPadding)).IsEqualTo(originalPadding);
});

recorder.Success();
}

[Test]
public async Task UpDownButtonsVisibility_Hidden_PreservesReservedPadding()
{
await using var recorder = new TestRecorder(App);

Thickness originalPadding = new(4, 5, 6, 7);
var decimalUpDown = await LoadXaml<DecimalUpDown>("""
<materialDesign:DecimalUpDown Padding="4,5,6,7"
Width="160"
UpDownButtonsVisibility="Hidden" />
""");
var buttonsHost = await decimalUpDown.GetElement<StackPanel>("ButtonsHost");
var textBox = await decimalUpDown.GetElement<TextBox>("PART_TextBox");

await Wait.For(async () =>
{
double buttonsWidth = await buttonsHost.RemoteExecute(GetButtonsHostActualWidth);
Thickness textBoxPadding = await textBox.RemoteExecute(GetTextBoxPadding);

await Assert.That(await buttonsHost.RemoteExecute(GetButtonsHostVisibility)).IsEqualTo(Visibility.Hidden);
await Assert.That(buttonsWidth).IsGreaterThan(0d);
await Assert.That(textBoxPadding.Left).IsEqualTo(originalPadding.Left);
await Assert.That(textBoxPadding.Top).IsEqualTo(originalPadding.Top);
await Assert.That(textBoxPadding.Bottom).IsEqualTo(originalPadding.Bottom);
await Assert.That(textBoxPadding.Right).IsEqualTo(originalPadding.Right + buttonsWidth);
});

recorder.Success();
}

private static Visibility GetButtonsHostVisibility(StackPanel stackPanel) => stackPanel.Visibility;

private static double GetButtonsHostActualWidth(StackPanel stackPanel) => stackPanel.ActualWidth;

private static Thickness GetTextBoxPadding(TextBox textBox) => textBox.Padding;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Globalization;
using MaterialDesignThemes.Wpf.Converters.Internal;

namespace MaterialDesignThemes.Wpf.Tests.Converters;

public sealed class UpDownButtonsPaddingConverterTests
{
[Test]
public async Task Convert_WhenButtonsVisible_AddsButtonsWidthToRightPadding()
{
UpDownButtonsPaddingConverter converter = new();

var result = (Thickness)converter.Convert([new Thickness(1, 2, 3, 4), 20d], typeof(Thickness), null!, CultureInfo.InvariantCulture);

await Assert.That(result).IsEqualTo(new Thickness(1, 2, 23, 4));
}

[Test]
public async Task Convert_WhenButtonsWidthIsNotFinite_ReturnsOriginalPadding()
{
UpDownButtonsPaddingConverter converter = new();

var result = (Thickness)converter.Convert([new Thickness(1, 2, 3, 4), double.NaN], typeof(Thickness), null!, CultureInfo.InvariantCulture);

await Assert.That(result).IsEqualTo(new Thickness(1, 2, 3, 4));
}
}
Loading