Skip to content

Commit a1316bc

Browse files
committed
feat: add WinForms command binding tests with real controls
Add WinForms-specific command binding tests that exercise the source generator's output against real System.Windows.Forms.Button and ToolStripButton controls. Tests run only on Windows CI where WinForms types are available. - Reference source generator as analyzer in WinForms test project - WinFormsCommandScenarios: BindCommand calls with Button, ToolStripButton, expression params, observable params, and custom MouseUp event - WinFormsCommandBindingTests: 12 tests covering click execution, CanExecute, disposal, command changes, parameter passing - Fix placeholder test to run on all platforms (was excluded on Windows)
1 parent 8db649d commit a1316bc

6 files changed

Lines changed: 452 additions & 12 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Reactive.Subjects;
6+
using System.Windows.Input;
7+
8+
namespace ReactiveUI.Binding.WinForms.Tests.CommandBinding;
9+
10+
/// <summary>
11+
/// Tests that source-generator-generated BindCommand code works correctly with real WinForms controls.
12+
/// These tests run only on Windows where WinForms types are available.
13+
/// </summary>
14+
[NotInParallel]
15+
public class WinFormsCommandBindingTests
16+
{
17+
/// <summary>
18+
/// Verifies that clicking a real WinForms Button executes the bound command.
19+
/// </summary>
20+
/// <returns>A task representing the asynchronous test operation.</returns>
21+
[Test]
22+
public async Task Button_ClickExecutesCommand()
23+
{
24+
var vm = new WinFormsCommandViewModel();
25+
var view = new WinFormsCommandView();
26+
var command = new TrackingCommand();
27+
vm.Save = command;
28+
29+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
30+
31+
view.SaveButton.PerformClick();
32+
33+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
34+
}
35+
36+
/// <summary>
37+
/// Verifies that setting the command after binding still wires correctly.
38+
/// </summary>
39+
/// <returns>A task representing the asynchronous test operation.</returns>
40+
[Test]
41+
public async Task Button_CommandSetAfterBinding_ExecutesOnClick()
42+
{
43+
var vm = new WinFormsCommandViewModel();
44+
var view = new WinFormsCommandView();
45+
var command = new TrackingCommand();
46+
47+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
48+
49+
vm.Save = command;
50+
view.SaveButton.PerformClick();
51+
52+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
53+
}
54+
55+
/// <summary>
56+
/// Verifies that when CanExecute is false, the command is not executed.
57+
/// </summary>
58+
/// <returns>A task representing the asynchronous test operation.</returns>
59+
[Test]
60+
public async Task Button_CanExecuteFalse_DoesNotExecute()
61+
{
62+
var vm = new WinFormsCommandViewModel();
63+
var view = new WinFormsCommandView();
64+
var command = new TrackingCommand { CanExecuteResult = false };
65+
vm.Save = command;
66+
67+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
68+
view.SaveButton.PerformClick();
69+
70+
await Assert.That(command.ExecuteCount).IsEqualTo(0);
71+
}
72+
73+
/// <summary>
74+
/// Verifies that disposing the binding prevents further command execution.
75+
/// </summary>
76+
/// <returns>A task representing the asynchronous test operation.</returns>
77+
[Test]
78+
public async Task Button_Dispose_StopsExecution()
79+
{
80+
var vm = new WinFormsCommandViewModel();
81+
var view = new WinFormsCommandView();
82+
var command = new TrackingCommand();
83+
vm.Save = command;
84+
85+
var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
86+
view.SaveButton.PerformClick();
87+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
88+
89+
binding.Dispose();
90+
view.SaveButton.PerformClick();
91+
92+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
93+
}
94+
95+
/// <summary>
96+
/// Verifies that when the command changes, the old command is unwired and the new command is wired.
97+
/// </summary>
98+
/// <returns>A task representing the asynchronous test operation.</returns>
99+
[Test]
100+
public async Task Button_CommandChanges_NewCommandExecutes()
101+
{
102+
var vm = new WinFormsCommandViewModel();
103+
var view = new WinFormsCommandView();
104+
var first = new TrackingCommand();
105+
var second = new TrackingCommand();
106+
vm.Save = first;
107+
108+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
109+
110+
view.SaveButton.PerformClick();
111+
await Assert.That(first.ExecuteCount).IsEqualTo(1);
112+
113+
vm.Save = second;
114+
view.SaveButton.PerformClick();
115+
116+
await Assert.That(second.ExecuteCount).IsEqualTo(1);
117+
await Assert.That(first.ExecuteCount).IsEqualTo(1);
118+
}
119+
120+
/// <summary>
121+
/// Verifies that an expression parameter is passed to the command on click.
122+
/// </summary>
123+
/// <returns>A task representing the asynchronous test operation.</returns>
124+
[Test]
125+
public async Task Button_ExpressionParam_PassesParameter()
126+
{
127+
var vm = new WinFormsCommandViewModel { CurrentItem = "TestItem" };
128+
var view = new WinFormsCommandView();
129+
var command = new TrackingCommand();
130+
vm.Save = command;
131+
132+
using var binding = WinFormsCommandScenarios.ButtonWithExpressionParam(vm, view);
133+
view.SaveButton.PerformClick();
134+
135+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
136+
await Assert.That(command.LastParameter).IsEqualTo("TestItem");
137+
}
138+
139+
/// <summary>
140+
/// Verifies that an observable parameter is passed to the command on click.
141+
/// </summary>
142+
/// <returns>A task representing the asynchronous test operation.</returns>
143+
[Test]
144+
public async Task Button_ObservableParam_PassesParameter()
145+
{
146+
var vm = new WinFormsCommandViewModel();
147+
var view = new WinFormsCommandView();
148+
var command = new TrackingCommand();
149+
var paramSubject = new BehaviorSubject<string>("obs-param");
150+
vm.Save = command;
151+
152+
using var binding = WinFormsCommandScenarios.ButtonWithObservableParam(vm, view, paramSubject);
153+
view.SaveButton.PerformClick();
154+
155+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
156+
await Assert.That(command.LastParameter).IsEqualTo("obs-param");
157+
}
158+
159+
/// <summary>
160+
/// Verifies that the observable parameter updates reactively.
161+
/// </summary>
162+
/// <returns>A task representing the asynchronous test operation.</returns>
163+
[Test]
164+
public async Task Button_ObservableParam_ParameterUpdates()
165+
{
166+
var vm = new WinFormsCommandViewModel();
167+
var view = new WinFormsCommandView();
168+
var command = new TrackingCommand();
169+
var paramSubject = new BehaviorSubject<string>("initial");
170+
vm.Save = command;
171+
172+
using var binding = WinFormsCommandScenarios.ButtonWithObservableParam(vm, view, paramSubject);
173+
174+
paramSubject.OnNext("updated");
175+
view.SaveButton.PerformClick();
176+
177+
await Assert.That(command.LastParameter).IsEqualTo("updated");
178+
}
179+
180+
/// <summary>
181+
/// Verifies that a custom MouseUp event fires the command.
182+
/// </summary>
183+
/// <returns>A task representing the asynchronous test operation.</returns>
184+
[Test]
185+
public async Task Button_CustomMouseUpEvent_ExecutesCommand()
186+
{
187+
var vm = new WinFormsCommandViewModel();
188+
var view = new WinFormsCommandView();
189+
var command = new TrackingCommand();
190+
vm.Save = command;
191+
192+
using var binding = WinFormsCommandScenarios.ButtonCustomEvent(vm, view);
193+
194+
// Raise MouseUp event on the button
195+
var mouseArgs = new MouseEventArgs(MouseButtons.Left, 1, 0, 0, 0);
196+
view.SaveButton.GetType()
197+
.GetMethod("OnMouseUp", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
198+
.Invoke(view.SaveButton, [mouseArgs]);
199+
200+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
201+
}
202+
203+
/// <summary>
204+
/// Verifies that BindCommand works with a real ToolStripButton (Component, not Control).
205+
/// </summary>
206+
/// <returns>A task representing the asynchronous test operation.</returns>
207+
[Test]
208+
public async Task ToolStripButton_ClickExecutesCommand()
209+
{
210+
var vm = new WinFormsCommandViewModel();
211+
var view = new WinFormsCommandView();
212+
var command = new TrackingCommand();
213+
vm.Save = command;
214+
215+
using var binding = WinFormsCommandScenarios.ToolStripButton(vm, view);
216+
217+
view.ToolStripSaveButton.PerformClick();
218+
219+
await Assert.That(command.ExecuteCount).IsEqualTo(1);
220+
}
221+
222+
/// <summary>
223+
/// Verifies that a null command doesn't throw when clicking a real Button.
224+
/// </summary>
225+
/// <returns>A task representing the asynchronous test operation.</returns>
226+
[Test]
227+
public async Task Button_NullCommand_ClickDoesNotThrow()
228+
{
229+
var vm = new WinFormsCommandViewModel();
230+
var view = new WinFormsCommandView();
231+
232+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
233+
234+
var action = () => view.SaveButton.PerformClick();
235+
await Assert.That(action).ThrowsNothing();
236+
}
237+
238+
/// <summary>
239+
/// Verifies that multiple clicks result in multiple executions.
240+
/// </summary>
241+
/// <returns>A task representing the asynchronous test operation.</returns>
242+
[Test]
243+
public async Task Button_MultipleClicks_ExecutesMultipleTimes()
244+
{
245+
var vm = new WinFormsCommandViewModel();
246+
var view = new WinFormsCommandView();
247+
var command = new TrackingCommand();
248+
vm.Save = command;
249+
250+
using var binding = WinFormsCommandScenarios.ButtonBasic(vm, view);
251+
252+
view.SaveButton.PerformClick();
253+
view.SaveButton.PerformClick();
254+
view.SaveButton.PerformClick();
255+
256+
await Assert.That(command.ExecuteCount).IsEqualTo(3);
257+
}
258+
259+
/// <summary>
260+
/// A simple <see cref="ICommand"/> implementation that tracks invocations for testing.
261+
/// </summary>
262+
private sealed class TrackingCommand : ICommand
263+
{
264+
/// <inheritdoc/>
265+
public event EventHandler? CanExecuteChanged
266+
{
267+
add { }
268+
remove { }
269+
}
270+
271+
/// <summary>
272+
/// Gets or sets a value indicating whether <see cref="CanExecute"/> returns <see langword="true"/>.
273+
/// </summary>
274+
public bool CanExecuteResult { get; set; } = true;
275+
276+
/// <summary>
277+
/// Gets the number of times <see cref="Execute"/> has been called.
278+
/// </summary>
279+
public int ExecuteCount { get; private set; }
280+
281+
/// <summary>
282+
/// Gets the parameter passed to the most recent <see cref="Execute"/> call.
283+
/// </summary>
284+
public object? LastParameter { get; private set; }
285+
286+
/// <inheritdoc/>
287+
public bool CanExecute(object? parameter) => CanExecuteResult;
288+
289+
/// <inheritdoc/>
290+
public void Execute(object? parameter)
291+
{
292+
ExecuteCount++;
293+
LastParameter = parameter;
294+
}
295+
}
296+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
namespace ReactiveUI.Binding.WinForms.Tests.CommandBinding;
6+
7+
/// <summary>
8+
/// Scenario methods for BindCommand with real WinForms controls.
9+
/// The source generator processes these call sites at compile time.
10+
/// </summary>
11+
public static class WinFormsCommandScenarios
12+
{
13+
/// <summary>
14+
/// Binds the Save command to a real WinForms Button control.
15+
/// </summary>
16+
/// <param name="vm">The source view model.</param>
17+
/// <param name="view">The target view.</param>
18+
/// <returns>A disposable representing the binding.</returns>
19+
public static IDisposable ButtonBasic(WinFormsCommandViewModel vm, WinFormsCommandView view)
20+
=> view.BindCommand(vm, x => x.Save, x => x.SaveButton);
21+
22+
/// <summary>
23+
/// Binds the Save command to a real WinForms Button with an expression parameter.
24+
/// </summary>
25+
/// <param name="vm">The source view model.</param>
26+
/// <param name="view">The target view.</param>
27+
/// <returns>A disposable representing the binding.</returns>
28+
public static IDisposable ButtonWithExpressionParam(WinFormsCommandViewModel vm, WinFormsCommandView view)
29+
=> view.BindCommand(vm, x => x.Save, x => x.SaveButton, vm, x => x.CurrentItem);
30+
31+
/// <summary>
32+
/// Binds the Save command to a real WinForms Button with an observable parameter.
33+
/// </summary>
34+
/// <param name="vm">The source view model.</param>
35+
/// <param name="view">The target view.</param>
36+
/// <param name="parameter">An observable producing command parameters.</param>
37+
/// <returns>A disposable representing the binding.</returns>
38+
public static IDisposable ButtonWithObservableParam(WinFormsCommandViewModel vm, WinFormsCommandView view, IObservable<string> parameter)
39+
=> view.BindCommand(vm, x => x.Save, x => x.SaveButton, parameter);
40+
41+
/// <summary>
42+
/// Binds the Save command to a WinForms Button using a custom MouseUp event.
43+
/// </summary>
44+
/// <param name="vm">The source view model.</param>
45+
/// <param name="view">The target view.</param>
46+
/// <returns>A disposable representing the binding.</returns>
47+
public static IDisposable ButtonCustomEvent(WinFormsCommandViewModel vm, WinFormsCommandView view)
48+
=> view.BindCommand(vm, x => x.Save, x => x.SaveButton, "MouseUp");
49+
50+
/// <summary>
51+
/// Binds the Save command to a real WinForms ToolStripButton (Component, not Control).
52+
/// </summary>
53+
/// <param name="vm">The source view model.</param>
54+
/// <param name="view">The target view.</param>
55+
/// <returns>A disposable representing the binding.</returns>
56+
public static IDisposable ToolStripButton(WinFormsCommandViewModel vm, WinFormsCommandView view)
57+
=> view.BindCommand(vm, x => x.Save, x => x.ToolStripSaveButton);
58+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.ComponentModel;
6+
7+
namespace ReactiveUI.Binding.WinForms.Tests.CommandBinding;
8+
9+
/// <summary>
10+
/// View containing real WinForms controls for command binding tests.
11+
/// </summary>
12+
#pragma warning disable CS0067 // Event is never used
13+
public class WinFormsCommandView : IViewFor, INotifyPropertyChanged
14+
{
15+
/// <inheritdoc/>
16+
public event PropertyChangedEventHandler? PropertyChanged;
17+
18+
/// <inheritdoc/>
19+
public object? ViewModel { get; set; }
20+
21+
/// <summary>
22+
/// Gets the save button (real WinForms Button with Click event and Enabled property).
23+
/// </summary>
24+
public Button SaveButton { get; } = new Button();
25+
26+
/// <summary>
27+
/// Gets the tool strip button (Component with Click event and Enabled property).
28+
/// </summary>
29+
public ToolStripButton ToolStripSaveButton { get; } = new ToolStripButton();
30+
}
31+
#pragma warning restore CS0067

0 commit comments

Comments
 (0)