GithubHelp home page GithubHelp logo

gu.wpf.uiautomation's Introduction

Gu.Wpf.UiAutomation

Join the chat at https://gitter.im/JohanLarsson/Gu.Wpf.UiAutomation License NuGet Build Status

Introduction

Gu.Wpf.UiAutomation is a .NET library which helps with automated UI testing of WPF applications. The library wraps UIAutomationClient and tries to provide an API that is nice for WPF.

The code inspired by FlaUI and White. Tested on Windows 7, Windows 10, and the default AppVeyor image.

Typical test class

Using the same window and restoring state can be a good strategy as the tests run faster and generate more input to the application finding more bugs.

public class FooTests
{
    // Current sln directory is searched rtecursively for this exe.
    private const string ExeFileName = "WpfApplication.exe";
    // This is optional
    private const string WindowName = "MainWindow";

    [SetUp]
    public void SetUp()
    {
        // restore state for the next test if the application is reused.
        using var app = Application.AttachOrLaunch(ExeFileName, WindowName);
        app.MainWindow.FindButton("Reset").Click();
    }

    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        // Close the shared window after the last test.
        Application.KillLaunched(ExeFileName);
    }

    [Test]
    public void Test1()
    {
        // AttachOrLaunch uses the already open app or creates a new. Dispose does not close the app.
        using var app = Application.AttachOrLaunch(ExeFileName, WindowName);
        var window = app.MainWindow;
        ...
    }

    [Test]
    public void Test2()
    {
        using var app = Application.AttachOrLaunch(ExeFileName, WindowName);
        var window = app.MainWindow;
        ...
    }

    [Test]
    public void Test3()
    {
        // If we for some reason needs a separate instance of the application in a test we use Launch()
        using var app = Application.Launch(ExeFileName, WindowName);
        var window = app.MainWindow;
        ...
    }
}

Usage of the window parameter in App.Xaml.cs

protected override void OnStartup(StartupEventArgs e)
{
    if (e is { Args: { Length: 1 } args })
    {
        var window = args[0];
        this.StartupUri = new Uri($"Windows/{window}.xaml", UriKind.Relative);
    }

    base.OnStartup(e);
}

Table of contents

Supported types

  • Button
  • Calendar
  • CalendarDayButton
  • CheckBox
  • ColumnHeader
  • ComboBox
  • ComboBoxItem
  • ContentControl
  • ContextMenu
  • Control
  • DataGrid
  • DataGridCell
  • DataGridColumnHeader
  • DataGridColumnHeadersPresenter
  • DataGridDetailsPresenter
  • DataGridRow
  • DataGridRowHeader
  • DatePicker
  • Dialog
  • Expander
  • Frame
  • GridHeader
  • GridSplitter
  • GridViewCell
  • GridViewColumnHeader
  • GridViewHeaderRowPresenter
  • GridViewRowHeader
  • GroupBox
  • HeaderedContentControl
  • HorizontalScrollBar
  • Label
  • ListBox
  • ListBoxItem
  • ListView
  • ListViewItem
  • Menu
  • MenuBar
  • MenuItem
  • MessageBox
  • MultiSelector`1
  • OpenFileDialog
  • PasswordBox
  • ExpandCollapseControl
  • InvokeControl
  • SelectionItemControl
  • Popup
  • ProgressBar
  • RadioButton
  • RepeatButton
  • RichTextBox
  • RowHeader
  • SaveFileDialog
  • ScrollBar
  • ScrollViewer
  • Selector
  • Selector`1
  • Separator
  • Slider
  • StatusBar
  • TabControl
  • TabItem
  • TextBlock
  • TextBox
  • TextBoxBase
  • Thumb
  • TitleBar
  • ToggleButton
  • ToolBar
  • ToolTip
  • TreeView
  • TreeViewItem
  • UiElement
  • UserControl
  • VerticalScrollBar
  • Window

Sample test

The entry point is usually an application or the desktop so you get an automation element (like a the main window of the application). On this, you can then search sub-elements and interact with them. The Application class is a helper for launching, attaching and closing applications. Since the application is not related to any UIA library, you need to create the automation you want and use it to get your first element, which then is your entry point.

[Test]
public void CheckBoxIsChecked()
{
    using var app = Application.Launch("WpfApplication.exe");
    var window = app.MainWindow;
    var checkBox = window.FindCheckBox("Test Checkbox");
    checkBox.IsChecked = true;
    Assert.AreEqual(true, checkBox.IsChecked);
}

Application

The application class iss the way to start an application to test. There are a couple of factory methods.

Launch

Starts a new instance of the application and closes it on dispose. There is a flag to leave the app open but the default is close on dispose. Launch is useful for tests that mutate state where resetting can be slow and painful.

[Test]
public void IsChecked()
{
    using var app = Application.Launch("WpfApplication.exe");
    var window = app.MainWindow;
    var checkBox = window.FindCheckBox("Test Checkbox");
    checkBox.IsChecked = true;
    Assert.AreEqual(true, checkBox.IsChecked);
}

Attach

Attaches to a running process and leaves it open when disposing disposing by default.

AttachOrLaunch

Attaches to a running process or launches a new if not found and leaves it open when disposing by default.

[SetUp]
public void SetUp()
{
    if (Application.TryAttach("WpfApplication.exe", "ButtonWindow", out var app))
    {
        using (app)
        {
            app.MainWindow.FindButton("Reset").Invoke();
        }
    }
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
    Application.KillLaunched("WpfApplication.exe");
}

[TestCase("AutomationId", "AutomationProperties.AutomationId")]
[TestCase("XName", "x:Name")]
[TestCase("Content", "Content")]
public void Content(string key, string expected)
{
    using var app = Application.AttachOrLaunch("WpfApplication.exe", "ButtonWindow");
    var window = app.MainWindow;
    var button = window.FindButton(key);
    Assert.AreEqual(expected, ((TextBlock)button.Content).Text);
}

Arguments

Launch and AttachOrLaunch has an overload that takes an argument string. It can be used like this:

[OneTimeTearDown]
public void OneTimeTearDown()
{
    Application.KillLaunched("WpfApplication.exe");
}

[Test]
public void SelectByIndex()
{
    using var app = Application.AttachOrLaunch("WpfApplication.exe", "ListBoxWindow");
    var window = app.MainWindow;
    var listBox = window.FindListBox("BoundListBox");
    Assert.AreEqual(2, listBox.Items.Count);
    Assert.IsInstanceOf<ListBoxItem>(listBox.Items[0]);
    Assert.IsInstanceOf<ListBoxItem>(listBox.Items[1]);
    Assert.IsNull(listBox.SelectedItem);

    var item = listBox.Select(0);
    Assert.AreEqual("Johan", item.FindTextBlock().Text);
    Assert.AreEqual("Johan", listBox.SelectedItem.FindTextBlock().Text);

    item = listBox.Select(1);
    Assert.AreEqual("Erik", item.FindTextBlock().Text);
    Assert.AreEqual("Erik", listBox.SelectedItem.FindTextBlock().Text);
}
public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        if (e is { Args: { Length: 1 } args })
        {
            var window = args[0];
            this.StartupUri = new Uri($"Windows/{window}.xaml", UriKind.Relative);
        }

        base.OnStartup(e);
    }
}

Input

Mouse

For mouse input like click, drag, scroll etc.

[Test]
public void DragFromCenterToTopLeft()
{
    using var app = Application.Launch("WpfApplication.exe");
    var window = app.MainWindow;
    Mouse.Drag(window.Bound.Center(), window.Bound.TopLeft);
    Assert.AreEqual(...);
}

Keyboard

For typing or holding modifier keys.

[Test]
public void ShiftDragFromCenterToTopLeft()
{
    using var app = Application.Launch("WpfApplication.exe");
    var window = app.MainWindow;
    using (Keyboard.Hold(Key.SHIFT))
    {
        Mouse.Drag(window.Bound.Center(), window.Bound.TopLeft);
        Assert.AreEqual(...);
    }
}

Touch

For mouse input like click, drag, scroll etc.

[Test]
public void DragFromCenterToTopLeft()
{
    using var app = Application.Launch("WpfApplication.exe");
    var window = app.MainWindow;
    Touch.Drag(window.Bound.Center(), window.Bound.TopLeft);
    Assert.AreEqual(...);
}

ImageAssert

For asserting using an expected image of how the control will render.

[Test]
public void DefaultAdornerWhenNotFocused()
{
    using var app = Application.Launch("Gu.Wpf.Adorners.Demo.exe", "WatermarkWindow");
    var window = app.MainWindow;
    var textBox = window.FindTextBox("WithDefaultAdorner");
    ImageAssert.AreEqual("Images\\WithDefaultAdorner_not_focused.png", textBox);
}

OnFail

[Test]
public void DefaultAdornerWhenNotFocused()
{
    using var app = Application.Launch("Gu.Wpf.Adorners.Demo.exe", "WatermarkWindow");
    var window = app.MainWindow;
    var textBox = window.FindTextBox("WithDefaultAdorner");
    ImageAssert.AreEqual("Images\\WithDefaultAdorner_not_focused.png", textBox, OnFail);
}


private static void OnFail(Bitmap bitmap, Bitmap actual, string resource)
{
    var fullFileName = Path.Combine(Path.GetTempPath(), resource);
    _ = Directory.CreateDirectory(Path.GetDirectoryName(fullFileName));
    actual.Save(fullFileName);
    TestContext.AddTestAttachment(fullFileName);
}

And in appveyor.yml

on_failure:
  - ps: Get-ChildItem $env:temp\*.png | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }

Normalize styles

For image asserts to work on build servers forcing a theme may be needed:

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/PresentationFramework.Classic;V4.0.0.0;31bf3856ad364e35;component/themes/Classic.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

Azure devops pipelines

    public static class TestImage
    {
        internal static readonly string Current = GetCurrent();

        // [Test]
        public static void Rename()
        {
            var folder = @"C:\Git\_GuOrg\Gu.Wpf.Gauges\Gu.Wpf.Gauges.Tests";
            var oldName = "Red_border_default_visibility_width_100.png";
            var newName = "Red_border_default_visibility_width_100.png";

            foreach (var file in Directory.EnumerateFiles(folder, oldName, SearchOption.AllDirectories))
            {
                File.Move(file, file.Replace(oldName, newName));
            }

            foreach (var file in Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories))
            {
                File.WriteAllText(file, File.ReadAllText(file).Replace(oldName, newName));
            }
        }

#pragma warning disable IDE0060 // Remove unused parameter
        internal static void OnFail(Bitmap? expected, Bitmap actual, string resource)
#pragma warning restore IDE0060 // Remove unused parameter
        {
            var fullFileName = Path.Combine(Path.GetTempPath(), resource);
            //// ReSharper disable once AssignNullToNotNullAttribute
            _ = Directory.CreateDirectory(Path.GetDirectoryName(fullFileName));
            if (File.Exists(fullFileName))
            {
                File.Delete(fullFileName);
            }

            actual.Save(fullFileName);
            TestContext.AddTestAttachment(fullFileName);
        }

        private static string GetCurrent()
        {
            if (WindowsVersion.IsWindows7())
            {
                return "Win7";
            }

            if (WindowsVersion.IsWindows10())
            {
                return "Win10";
            }

            if (WindowsVersion.CurrentContains("Windows Server 2019"))
            {
                return "WinServer2019";
            }

            return WindowsVersion.CurrentVersionProductName;
        }
    }
ImageAssert.AreEqual($"Images\\{TestImage.Current}\\{name}", element, TestImage.OnFail);

AppVeyor

Troubleshooting failing UI-tests on AppVeyor is tricky. Here is a snippet that can be used for getting a screenshot of what things look like.

[Test]
public void SomeTest()
{
    try
    {
        using var app = Application.AttachOrLaunch("SomeApp.exe");
        ...
    }
    catch (TimeoutException)
    {
        Capture.ScreenToFile(Path.Combine(Path.GetTempPath(), "SomeTest.png"));
        throw;
    }
}

And in appveyor.yml

on_failure:
  - ps: Get-ChildItem $env:temp\*.png | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }

Contribution

Feel free to fork Gu.Wpf.UiAutomation and send pull requests of your modifications.
You can also create issues if you find problems or have ideas on how to further improve Gu.Wpf.UiAutomation.

gu.wpf.uiautomation's People

Contributors

johanlarsson avatar dependabot[bot] avatar leptitdev avatar foggyfinder avatar codex04 avatar laurentm-ubi avatar forki avatar gitter-badger avatar jnm2 avatar milleniumbug avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.