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
5 changes: 4 additions & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
installWorkloads: true
installWindowsAppRuntime: true
# Test only the test projects: the solution also builds the documentation example apps, which are not tests.
testProjects: tests/**/*.csproj
# The device test app needs an emulator or simulator; device-tests.yml runs it.
testProjects: |
tests/**/*.csproj
!tests/ReactiveUI.Device.Tests/**
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
19 changes: 19 additions & 0 deletions .github/workflows/device-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Device Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

permissions:
contents: read

jobs:
device-tests:
uses: reactiveui/actions-common/.github/workflows/workflow-common-device-tests.yml@main
with:
projects: src/tests/ReactiveUI.Device.Tests/ReactiveUI.Device.Tests.csproj
configuration: Release
solutionFile: reactiveui.slnx
5 changes: 4 additions & 1 deletion .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ jobs:
installWorkloads: true
installWindowsAppRuntime: true
# Test only the test projects: the solution also builds the documentation example apps, which are not tests.
testProjects: tests/**/*.csproj
# The device test app needs an emulator or simulator; device-tests.yml runs it.
testProjects: |
tests/**/*.csproj
!tests/ReactiveUI.Device.Tests/**
sonarProjectKey: reactiveui_ReactiveUI
sonarOrganization: reactiveui
sonarExclusions: '**/tests/**,**/integrationtests/**,**/benchmarks/**,**/examples/**,**/TestResults/**'
Expand Down
2 changes: 2 additions & 0 deletions src/reactiveui.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<Folder Name="/Solution Items/Workflows/">
<File Path="../.github/workflows/ci-build.yml" />
<File Path="../.github/workflows/codeql.yml" />
<File Path="../.github/workflows/device-tests.yml" />
<File Path="../.github/workflows/lock.yml" />
<File Path="../.github/workflows/release.yml" />
<File Path="../.github/workflows/sonarcloud.yml" />
Expand All @@ -68,6 +69,7 @@
<Project Path="tests/ReactiveUI.Blazor.Tests.Reactive/ReactiveUI.Blazor.Tests.Reactive.csproj" />
<Project Path="tests/ReactiveUI.Builder.Maui.Tests/ReactiveUI.Builder.Maui.Tests.csproj" />
<Project Path="tests/ReactiveUI.Builder.Tests/ReactiveUI.Builder.Tests.csproj" />
<Project Path="tests/ReactiveUI.Device.Tests/ReactiveUI.Device.Tests.csproj" />
<Project Path="tests/ReactiveUI.Maui.Tests/ReactiveUI.Maui.Tests.csproj" />
<Project Path="tests/ReactiveUI.Splat.Tests/ReactiveUI.Splat.Tests.csproj" />
<Project Path="tests/ReactiveUI.Test.Utilities/ReactiveUI.Test.Utilities.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Widget;

namespace ReactiveUI.Device.Tests;

/// <summary>A <see cref="ReactiveActivity{TViewModel}"/> that counts its activations and wires its layout by name.</summary>
[Activity(Exported = false)]
public class ActivatingActivity : ReactiveActivity<TestViewModel>
{
/// <summary>The number of times the activity has been activated.</summary>
private int _activations;

/// <summary>The number of times the activity has been deactivated.</summary>
private int _deactivations;

/// <summary>Initializes a new instance of the <see cref="ActivatingActivity"/> class.</summary>
public ActivatingActivity() =>
this.WhenActivated(disposables =>
{
_ = Interlocked.Increment(ref _activations);
disposables(new ActionDisposable(() => Interlocked.Increment(ref _deactivations)));
});

/// <summary>Gets the most recently created instance, so a test can reach the instance Android creates when it recreates one.</summary>
public static ActivatingActivity? LastCreated { get; private set; }

/// <summary>Gets the number of times the activity has been activated.</summary>
public int Activations => Volatile.Read(ref _activations);

/// <summary>Gets a value indicating whether Android passed saved state to <see cref="OnCreate(Bundle?)"/>.</summary>
public bool CreatedFromSavedState { get; private set; }

/// <summary>Gets the number of times the activity has been deactivated.</summary>
public int Deactivations => Volatile.Read(ref _deactivations);

/// <summary>Gets or sets the title control, wired by name.</summary>
public TextView? TitleText { get; set; }

/// <summary>Gets or sets the save button, wired by name.</summary>
public Button? SaveButton { get; set; }

/// <summary>Gets the error wiring the controls threw, if it threw.</summary>
public Exception? WireUpError { get; private set; }

/// <inheritdoc/>
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
CreatedFromSavedState = savedInstanceState is not null;
LastCreated = this;
SetContentView(Resource.Layout.wireup_layout);

try
{
this.WireUpControls();
}
catch (MissingFieldException ex)
{
WireUpError = ex;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Views;
using Android.Widget;
using ReactiveUI.AndroidX;

namespace ReactiveUI.Device.Tests;

/// <summary>A <see cref="ReactiveAppCompatActivity{TViewModel}"/> that hosts a <see cref="WiredFragment"/>.</summary>
[Activity(Exported = false)]
public class CompatHostActivity : ReactiveAppCompatActivity<TestViewModel>
{
/// <summary>The number of times the activity has been activated.</summary>
private int _activations;

/// <summary>Initializes a new instance of the <see cref="CompatHostActivity"/> class.</summary>
public CompatHostActivity() =>
this.WhenActivated((Action<IDisposable> _) => Interlocked.Increment(ref _activations));

/// <summary>Gets the number of times the activity has been activated.</summary>
public int Activations => Volatile.Read(ref _activations);

/// <summary>Gets the hosted fragment.</summary>
public WiredFragment Fragment { get; } = new();

/// <inheritdoc/>
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);

var container = new FrameLayout(this) { Id = View.GenerateViewId() };
SetContentView(container);
SupportFragmentManager!.BeginTransaction()!.Add(container.Id, Fragment)!.CommitNow();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.OS;

namespace ReactiveUI.Device.Tests;

/// <summary>The binder <see cref="EchoService"/> returns; a local client receives this same instance.</summary>
public class EchoBinder : Binder
{
/// <summary>Returns the text it was given, so a test can call through the bound service.</summary>
/// <param name="text">The text to echo.</param>
/// <returns>The same text.</returns>
public string Echo(string text) => text;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Content;
using Android.OS;

namespace ReactiveUI.Device.Tests;

/// <summary>A local bound service the service-binding tests connect to.</summary>
[Service(Exported = false)]
public class EchoService : Service
{
/// <summary>The binder handed to every client.</summary>
private readonly EchoBinder _binder = new();

/// <inheritdoc/>
public override IBinder? OnBind(Intent? intent) => _binder;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Widget;

namespace ReactiveUI.Device.Tests;

/// <summary>A plain activity whose content view the tests add views to, so the views attach to a real window.</summary>
[Activity(Exported = false)]
public class HostActivity : Activity
{
/// <summary>Gets the container the tests add views to.</summary>
public FrameLayout Container { get; private set; } = null!;

/// <inheritdoc/>
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
Container = new(this);
SetContentView(Container);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Content;
using AndroidResult = Android.App.Result;

namespace ReactiveUI.Device.Tests;

/// <summary>An activity that returns a result to its caller as soon as it is created.</summary>
[Activity(Exported = false)]
public class ResultSourceActivity : Activity
{
/// <summary>The intent extra key carrying the answer.</summary>
internal const string AnswerKey = "answer";

/// <summary>The answer the activity returns.</summary>
internal const int Answer = 42;

/// <inheritdoc/>
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);

var data = new Intent();
_ = data.PutExtra(AnswerKey, Answer);
SetResult(AndroidResult.Ok, data);
Finish();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Views;
using Android.Widget;
using ReactiveUI.AndroidX;

namespace ReactiveUI.Device.Tests;

/// <summary>An AndroidX <see cref="ReactiveFragment{TViewModel}"/> that wires its layout by name and counts activations.</summary>
public class WiredFragment : ReactiveUI.AndroidX.ReactiveFragment<TestViewModel>
{
/// <summary>The number of times the fragment has been activated.</summary>
private int _activations;

/// <summary>The number of times the fragment has been deactivated.</summary>
private int _deactivations;

/// <summary>Initializes a new instance of the <see cref="WiredFragment"/> class.</summary>
public WiredFragment() =>
this.WhenActivated(disposables =>
{
_ = Interlocked.Increment(ref _activations);
disposables(new ActionDisposable(() => Interlocked.Increment(ref _deactivations)));
});

/// <summary>Gets the number of times the fragment has been activated.</summary>
public int Activations => Volatile.Read(ref _activations);

/// <summary>Gets the number of times the fragment has been deactivated.</summary>
public int Deactivations => Volatile.Read(ref _deactivations);

/// <summary>Gets or sets the title control, wired by name.</summary>
public TextView? TitleText { get; set; }

/// <summary>Gets or sets the renamed input, wired through its resource-name override.</summary>
[WireUpResource("renamed_input")]
public EditText? Input { get; set; }

/// <inheritdoc/>
public override View? OnCreateView(LayoutInflater? inflater, ViewGroup? container, Bundle? savedInstanceState)
{
if (inflater?.Inflate(Resource.Layout.wireup_layout, container, false) is not { } view)
{
return null;
}

this.WireUpControls(view);
return view;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="ReactiveUI Device Tests" android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Content;

namespace ReactiveUI.Device.Tests;

/// <summary>Starts and finishes real activities through the test instrumentation.</summary>
internal static class ActivityLauncher
{
/// <summary>Gets the application context of the app under test.</summary>
internal static Context TargetContext => TestInstrumentation.Current.TargetContext!;

/// <summary>Starts <typeparamref name="TActivity"/> and waits until it is resumed.</summary>
/// <typeparam name="TActivity">The activity type, declared in the manifest by its <c>[Activity]</c> attribute.</typeparam>
/// <returns>The resumed activity.</returns>
internal static Task<TActivity> StartAsync<TActivity>()
where TActivity : Activity =>
Task.Run(static () =>
{
var intent = new Intent(TargetContext, typeof(TActivity));
_ = intent.AddFlags(ActivityFlags.NewTask);

// StartActivitySync blocks until the activity is resumed and must not run on the main thread.
return (TActivity)TestInstrumentation.Current.StartActivitySync(intent)!;
});

/// <summary>Finishes <paramref name="activity"/> and waits for the main thread to go idle.</summary>
/// <param name="activity">The activity to finish.</param>
/// <returns>A task that completes once the finish has been processed.</returns>
internal static async Task FinishAsync(Activity activity)
{
ArgumentNullException.ThrowIfNull(activity);

await MainThread.RunAsync(activity.Finish).ConfigureAwait(false);
await Task.Run(TestInstrumentation.Current.WaitForIdleSync).ConfigureAwait(false);
}
}
Loading
Loading