Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ jobs:

- name: Build Avalonia Demo
run: dotnet build examples/Avalonia.Demo/AvaloniaDemo.csproj --configuration Release

# Renders every component and drives them with mouse and keyboard, headless (no display needed)
- name: Run Avalonia headless tests
run: dotnet test tests/ShellUI.Native.Avalonia.Tests/ShellUI.Native.Avalonia.Tests.csproj --configuration Release
1 change: 1 addition & 0 deletions ShellUI.Native.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<Project Path="src/ShellUI.Native.Templates/ShellUI.Native.Templates.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ShellUI.Native.Avalonia.Tests/ShellUI.Native.Avalonia.Tests.csproj" />
<Project Path="tests/ShellUI.Native.Tests/ShellUI.Native.Tests.csproj" />
</Folder>
</Solution>
9 changes: 5 additions & 4 deletions examples/Avalonia.Demo/Components/UI/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ public static class ShellTheme
public const double RadiusMd = 6;
public const double RadiusLg = 8;

private static bool _initialized;
// The app the palettes were published to. A new Application (e.g. one per headless test)
// gets them too.
private static Application? _initialized;

// Raised when a palette is published or the theme variant changes, for anything resources
// can't reach (e.g. a BoxShadow).
Expand All @@ -161,10 +163,9 @@ public static Color Get(ShellToken token, double opacity)

public static void EnsureInitialized()
{
if (_initialized) return;
var app = Application.Current;
if (app is null) return;
_initialized = true;
if (app is null || ReferenceEquals(app, _initialized)) return;
_initialized = app;
app.ActualThemeVariantChanged += (_, _) => ThemeChanged?.Invoke(null, EventArgs.Empty);
Apply();
}
Expand Down
9 changes: 5 additions & 4 deletions src/ShellUI.Native.Templates/Templates/ShellTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,9 @@ public static class ShellTheme
public const double RadiusMd = 6;
public const double RadiusLg = 8;

private static bool _initialized;
// The app the palettes were published to. A new Application (e.g. one per headless test)
// gets them too.
private static Application? _initialized;

// Raised when a palette is published or the theme variant changes, for anything resources
// can't reach (e.g. a BoxShadow).
Expand All @@ -1235,10 +1237,9 @@ public static Color Get(ShellToken token, double opacity)

public static void EnsureInitialized()
{
if (_initialized) return;
var app = Application.Current;
if (app is null) return;
_initialized = true;
if (app is null || ReferenceEquals(app, _initialized)) return;
_initialized = app;
app.ActualThemeVariantChanged += (_, _) => ThemeChanged?.Invoke(null, EventArgs.Empty);
Apply();
}
Expand Down
223 changes: 223 additions & 0 deletions tests/ShellUI.Native.Avalonia.Tests/ChangeEventTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System.Linq;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
using AvaloniaDemo.Components.UI;
using Xunit;
using Switch = AvaloniaDemo.Components.UI.Switch;
using Slider = AvaloniaDemo.Components.UI.Slider;

namespace ShellUI.Native.Avalonia.Tests;

// Change events report user input only: values set from code stay silent, a click or a key raises
// the event once.
public class ChangeEventTests
{
[AvaloniaFact]
public void Switch_reports_clicks_and_keys_not_code()
{
var sw = new Switch { Label = "Wi-Fi" };
var window = Ui.Show(sw);
var raised = 0;
sw.Toggled += (_, _) => raised++;

sw.IsToggled = true;
Assert.Equal(0, raised);

Ui.Click(window, sw);
Assert.Equal(1, raised);
Assert.False(sw.IsToggled);

sw.Focus();
Ui.Press(window, Key.Space);
Assert.Equal(2, raised);
Assert.True(sw.IsToggled);
}

[AvaloniaFact]
public void Checkbox_reports_clicks_not_code()
{
var cb = new Checkbox { Label = "Accept" };
var window = Ui.Show(cb);
var raised = 0;
cb.CheckedChanged += (_, _) => raised++;

cb.IsChecked = true;
Assert.Equal(0, raised);
Ui.Click(window, cb);
Assert.Equal(1, raised);
Assert.False(cb.IsChecked);
}

[AvaloniaFact]
public void Toggle_reports_clicks_not_code()
{
var toggle = new Toggle { Text = "Bold" };
var window = Ui.Show(toggle);
var raised = 0;
toggle.PressedChanged += (_, _) => raised++;

toggle.IsPressed = true;
Assert.Equal(0, raised);
Ui.Click(window, toggle);
Assert.Equal(1, raised);
}

[AvaloniaFact]
public void Slider_reports_drags_and_keys_not_code_or_new_limits()
{
var slider = new Slider { Width = 300, HorizontalAlignment = HorizontalAlignment.Left };
var window = Ui.Show(slider);
var raised = 0;
slider.ValueChanged += (_, _) => raised++;

slider.Value = 70;
slider.Maximum = 60; // coerces Value to 60
Assert.Equal(0, raised);

var left = slider.TranslatePoint(new Point(10, slider.Bounds.Height / 2), window)!.Value;
Ui.ClickAt(window, left);
Assert.Equal(1, raised);
Assert.True(slider.Value < 10);

slider.Focus();
Ui.Press(window, Key.Right);
Assert.Equal(2, raised);
}

[AvaloniaFact]
public void Tabs_report_trigger_clicks_and_arrow_keys_not_code()
{
var a = new TabsTrigger { Value = "a", Text = "Account" };
var b = new TabsTrigger { Value = "b", Text = "Password" };
var list = new TabsList();
list.Items.Add(a);
list.Items.Add(b);
var tabs = new Tabs { Value = "a", Children = { list } };
var window = Ui.Show(tabs);
var raised = 0;
tabs.ValueChanged += (_, _) => raised++;

tabs.Value = "b";
Assert.Equal(0, raised);

Ui.Click(window, a);
Assert.Equal(1, raised);
Assert.Equal("a", tabs.Value);

a.Focus();
Ui.Press(window, Key.Right);
Assert.Equal(2, raised);
Assert.Equal("b", tabs.Value);
}

[AvaloniaFact]
public void Pagination_reports_page_clicks_not_code()
{
var pagination = new Pagination { TotalPages = 20, Page = 5 };
var window = Ui.Show(pagination);
var raised = 0;
pagination.PageChanged += (_, _) => raised++;

pagination.Page = 3;
Assert.Equal(0, raised);
Ui.Settle(); // the page items are rebuilt

var page4 = pagination.GetVisualDescendants().OfType<Border>().First(b => AutomationProperties.GetName(b) == "Go to page 4");
Ui.Click(window, page4);
Assert.Equal(1, raised);
Assert.Equal(4, pagination.Page);
}

[AvaloniaFact]
public void Accordion_reports_trigger_clicks_not_code()
{
var accordion = new Accordion { Value = "a" };
foreach (var value in new[] { "a", "b" })
{
var item = new AccordionItem { Value = value };
item.Items.Add(new AccordionTrigger { Text = value });
item.Items.Add(new AccordionContent { Child = new TextBlock { Text = value } });
accordion.Children.Add(item);
}
var window = Ui.Show(accordion);
var toggled = 0;
var itemOpened = 0;
accordion.ItemToggled += (_, _) => toggled++;
foreach (var item in accordion.FindDescendantsOfType<AccordionItem>())
item.OpenChanged += (_, _) => itemOpened++;

accordion.Value = "b";
Assert.Equal(0, toggled);
Assert.Equal(0, itemOpened);

// Single mode: opening "a" closes "b", and each change is reported.
Ui.Click(window, accordion.FindDescendantsOfType<AccordionTrigger>().First());
Assert.Equal(2, toggled);
Assert.Equal(2, itemOpened);
Assert.True(accordion.IsOpen("a"));
}

[AvaloniaFact]
public void Collapsible_reports_trigger_clicks_not_code()
{
var trigger = new CollapsibleTrigger { Child = new TextBlock { Text = "Show more" } };
var collapsible = new Collapsible { Children = { trigger, new CollapsibleContent { Child = new TextBlock { Text = "More" } } } };
var window = Ui.Show(collapsible);
var raised = 0;
collapsible.OpenChanged += (_, _) => raised++;

collapsible.Open = true;
collapsible.Toggle();
Assert.Equal(0, raised);

Ui.Click(window, trigger);
Assert.Equal(1, raised);
Assert.True(collapsible.Open);
}

[AvaloniaFact]
public void RadioGroup_reports_item_clicks_not_code()
{
var first = new RadioGroupItem { Value = "a", Text = "Default" };
var second = new RadioGroupItem { Value = "b", Text = "Compact" };
var group = new RadioGroup { Value = "a", Children = { first, second } };
var window = Ui.Show(group);
var raised = 0;
group.ValueChanged += (_, _) => raised++;

group.Value = "b";
group.SetValue("a");
Assert.Equal(0, raised);

Ui.Click(window, second);
Assert.Equal(1, raised);
Assert.Equal("b", group.Value);
}

[AvaloniaFact]
public void Select_reports_picked_items_not_code()
{
var select = new Select { Width = 240, ItemsSource = new[] { "Kenya", "Norway", "Zimbabwe" } };
var window = Ui.Show(select);
var raised = 0;
select.SelectedIndexChanged += (_, _) => raised++;

select.SelectedIndex = 1;
Assert.Equal(0, raised);

Ui.Click(window, select);
Assert.True(select.IsOpen);
Ui.Wait(250); // the list fades and scales in
var zimbabwe = window.GetVisualDescendants().OfType<TextBlock>()
.First(t => t.Text == "Zimbabwe" && t.IsEffectivelyVisible && !Ui.IsWithin(t, select));
Ui.Click(window, zimbabwe);
Assert.Equal(1, raised);
Assert.Equal(2, select.SelectedIndex);
Assert.False(select.IsOpen);
}
}
36 changes: 36 additions & 0 deletions tests/ShellUI.Native.Avalonia.Tests/ComponentSmokeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Styling;
using AvaloniaDemo.Components.UI;
using Xunit;

namespace ShellUI.Native.Avalonia.Tests;

// Every component can be created, laid out and drawn on its own, in both themes.
public class ComponentSmokeTests
{
public static IEnumerable<object[]> Components() =>
typeof(ShellTheme).Assembly.GetTypes()
.Where(t => t.Namespace == typeof(ShellTheme).Namespace && t.IsPublic && !t.IsAbstract
&& typeof(Control).IsAssignableFrom(t) && t.GetConstructor(Type.EmptyTypes) != null)
.OrderBy(t => t.Name)
.Select(t => new object[] { t.Name });

[AvaloniaTheory]
[MemberData(nameof(Components))]
public void Renders_in_light_and_dark(string typeName)
{
var type = typeof(ShellTheme).Assembly.GetTypes().Single(t => t.Name == typeName && t.Namespace == typeof(ShellTheme).Namespace);
foreach (var theme in new[] { ThemeVariant.Light, ThemeVariant.Dark })
{
var control = (Control)Activator.CreateInstance(type)!;
var window = Ui.Show(control, theme);
Assert.NotNull(window.CaptureRenderedFrame());
window.Close();
}
}
}
Loading
Loading