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
43 changes: 43 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Build and Test

on:
pull_request:
branches: [ main ]

jobs:
build-and-test:
runs-on: windows-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'

- name: Install required workloads
run: dotnet workload restore SarahsDailyApp.slnx

- name: Restore Android
run: dotnet restore SarahsDailyApp/SarahsDailyApp.csproj -p:TargetFrameworks=net10.0-android

- name: Build Android
run: dotnet build SarahsDailyApp/SarahsDailyApp.csproj --no-restore --configuration Release -p:TargetFrameworks=net10.0-android

- name: Restore Windows
run: dotnet restore SarahsDailyApp/SarahsDailyApp.csproj -p:TargetFrameworks=net10.0-windows10.0.19041.0 -r win-x64

- name: Build Windows
run: dotnet build SarahsDailyApp/SarahsDailyApp.csproj --no-restore --configuration Release -p:TargetFrameworks=net10.0-windows10.0.19041.0 -r win-x64 -p:WindowsPackageType=None

- name: Run tests
run: dotnet test SarahsDailyApp.Tests/SarahsDailyApp.Tests.csproj --configuration Release --logger "trx;LogFileName=test-results.trx" --results-directory ./test-results

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: ./test-results/*.trx
208 changes: 208 additions & 0 deletions SarahsDailyApp.Tests/Converters/ConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System.Globalization;
using SarahsDailyApp.Converters;
using SarahsDailyApp.Models;

namespace SarahsDailyApp.Tests.Converters;

public class ConverterTests
{
private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;

// --- InverseBoolConverter ---

[Theory]
[InlineData(true, false)]
[InlineData(false, true)]
public void InverseBoolConverter_Convert_InvertsBool(bool input, bool expected)
{
var converter = new InverseBoolConverter();
var result = converter.Convert(input, typeof(bool), null, Culture);
Assert.Equal(expected, result);
}

[Theory]
[InlineData(true, false)]
[InlineData(false, true)]
public void InverseBoolConverter_ConvertBack_InvertsBool(bool input, bool expected)
{
var converter = new InverseBoolConverter();
var result = converter.ConvertBack(input, typeof(bool), null, Culture);
Assert.Equal(expected, result);
}

[Fact]
public void InverseBoolConverter_Convert_NonBool_ReturnsValue()
{
var converter = new InverseBoolConverter();
var result = converter.Convert("not a bool", typeof(bool), null, Culture);
Assert.Equal("not a bool", result);
}

// --- BoolToStrikethroughConverter ---

[Fact]
public void BoolToStrikethroughConverter_True_ReturnsStrikethrough()
{
var converter = new BoolToStrikethroughConverter();
var result = converter.Convert(true, typeof(TextDecorations), null, Culture);
Assert.Equal(TextDecorations.Strikethrough, result);
}

[Fact]
public void BoolToStrikethroughConverter_False_ReturnsNone()
{
var converter = new BoolToStrikethroughConverter();
var result = converter.Convert(false, typeof(TextDecorations), null, Culture);
Assert.Equal(TextDecorations.None, result);
}

[Fact]
public void BoolToStrikethroughConverter_NonBool_ReturnsNone()
{
var converter = new BoolToStrikethroughConverter();
var result = converter.Convert("not a bool", typeof(TextDecorations), null, Culture);
Assert.Equal(TextDecorations.None, result);
}

// --- PercentToProgressConverter ---

[Theory]
[InlineData(0.0, 0.0)]
[InlineData(50.0, 0.5)]
[InlineData(100.0, 1.0)]
[InlineData(75.0, 0.75)]
public void PercentToProgressConverter_ConvertsCorrectly(double input, double expected)
{
var converter = new PercentToProgressConverter();
var result = converter.Convert(input, typeof(double), null, Culture);
Assert.Equal(expected, result);
}

[Fact]
public void PercentToProgressConverter_NonDouble_ReturnsZero()
{
var converter = new PercentToProgressConverter();
var result = converter.Convert("not a double", typeof(double), null, Culture);
Assert.Equal(0.0, result);
}

// --- IsNotNullConverter ---

[Fact]
public void IsNotNullConverter_Null_ReturnsFalse()
{
var converter = new IsNotNullConverter();
var result = converter.Convert(null, typeof(bool), null, Culture);
Assert.Equal(false, result);
}

[Fact]
public void IsNotNullConverter_NonNull_ReturnsTrue()
{
var converter = new IsNotNullConverter();
var result = converter.Convert(42, typeof(bool), null, Culture);
Assert.Equal(true, result);
}

[Fact]
public void IsNotNullConverter_EmptyString_ReturnsFalse()
{
var converter = new IsNotNullConverter();
var result = converter.Convert("", typeof(bool), null, Culture);
Assert.Equal(false, result);
}

[Fact]
public void IsNotNullConverter_NonEmptyString_ReturnsTrue()
{
var converter = new IsNotNullConverter();
var result = converter.Convert("hello", typeof(bool), null, Culture);
Assert.Equal(true, result);
}

// --- IsNotZeroConverter ---

[Fact]
public void IsNotZeroConverter_Zero_ReturnsFalse()
{
var converter = new IsNotZeroConverter();
var result = converter.Convert(0, typeof(bool), null, Culture);
Assert.Equal(false, result);
}

[Fact]
public void IsNotZeroConverter_NonZero_ReturnsTrue()
{
var converter = new IsNotZeroConverter();
var result = converter.Convert(5, typeof(bool), null, Culture);
Assert.Equal(true, result);
}

[Fact]
public void IsNotZeroConverter_NegativeNumber_ReturnsTrue()
{
var converter = new IsNotZeroConverter();
var result = converter.Convert(-1, typeof(bool), null, Culture);
Assert.Equal(true, result);
}

// --- IsNotNoneConverter ---

[Fact]
public void IsNotNoneConverter_None_ReturnsFalse()
{
var converter = new IsNotNoneConverter();
var result = converter.Convert(RepeatType.None, typeof(bool), null, Culture);
Assert.Equal(false, result);
}

[Theory]
[InlineData(RepeatType.Daily)]
[InlineData(RepeatType.Weekly)]
[InlineData(RepeatType.Monthly)]
[InlineData(RepeatType.Yearly)]
[InlineData(RepeatType.Custom)]
[InlineData(RepeatType.Movable)]
public void IsNotNoneConverter_NonNone_ReturnsTrue(RepeatType repeatType)
{
var converter = new IsNotNoneConverter();
var result = converter.Convert(repeatType, typeof(bool), null, Culture);
Assert.Equal(true, result);
}

// --- StatusTextConverter ---

[Fact]
public void StatusTextConverter_True_ReturnsCompleted()
{
var converter = new StatusTextConverter();
var result = converter.Convert(true, typeof(string), null, Culture);
Assert.Equal("completed", result);
}

[Fact]
public void StatusTextConverter_False_ReturnsPending()
{
var converter = new StatusTextConverter();
var result = converter.Convert(false, typeof(string), null, Culture);
Assert.Equal("pending", result);
}

// --- StatusColorConverter ---

[Fact]
public void StatusColorConverter_True_ReturnsGreen()
{
var converter = new StatusColorConverter();
var result = converter.Convert(true, typeof(Color), null, Culture);
Assert.IsType<Color>(result);
}

[Fact]
public void StatusColorConverter_False_ReturnsOrange()
{
var converter = new StatusColorConverter();
var result = converter.Convert(false, typeof(Color), null, Culture);
Assert.IsType<Color>(result);
}
}
56 changes: 56 additions & 0 deletions SarahsDailyApp.Tests/Helpers/TestDatabaseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using SQLite;
using SarahsDailyApp.Models;
using SarahsDailyApp.Services;

namespace SarahsDailyApp.Tests.Helpers;

/// <summary>
/// A test-friendly DatabaseService that uses a unique temp file SQLite database
/// instead of the MAUI FileSystem.AppDataDirectory path.
/// </summary>
public class TestDatabaseService : DatabaseService, IDisposable
{
private SQLiteAsyncConnection? _database;
private bool _initialized;
private readonly string _dbPath;

public TestDatabaseService()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.db3");
}

public override async Task<SQLiteAsyncConnection> GetDatabaseAsync()
{
if (_initialized && _database is not null)
return _database;

_database = new SQLiteAsyncConnection(_dbPath);

await _database.CreateTableAsync<TaskItem>();
await _database.CreateTableAsync<Project>();
await _database.CreateTableAsync<Habit>();
await _database.CreateTableAsync<HabitLog>();
await _database.CreateTableAsync<FinanceItem>();
await _database.CreateTableAsync<WishItem>();
await _database.CreateTableAsync<WishList>();
await _database.CreateTableAsync<Note>();
await _database.CreateTableAsync<ShoppingItem>();
await _database.CreateTableAsync<UserStats>();
await _database.CreateTableAsync<AppSettings>();
await _database.CreateTableAsync<Category>();

// Seed defaults
await _database.InsertAsync(new UserStats());
await _database.InsertAsync(new AppSettings());

_initialized = true;
return _database;
}

public void Dispose()
{
_database?.CloseAsync().GetAwaiter().GetResult();
try { File.Delete(_dbPath); } catch { }
GC.SuppressFinalize(this);
}
}
57 changes: 57 additions & 0 deletions SarahsDailyApp.Tests/Models/FinanceItemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using SarahsDailyApp.Models;

namespace SarahsDailyApp.Tests.Models;

public class FinanceItemTests
{
[Fact]
public void NewFinanceItem_HasDefaultValues()
{
var item = new FinanceItem();

Assert.NotNull(item.Id);
Assert.NotEmpty(item.Id);
Assert.Equal(string.Empty, item.Description);
Assert.Equal(0m, item.Amount);
Assert.Null(item.Date);
Assert.Null(item.Category);
Assert.Equal(RecurringType.Once, item.Recurring);
}

[Fact]
public void MonthlyAmount_OnceRecurring_ReturnsAmount()
{
var item = new FinanceItem { Amount = 120m, Recurring = RecurringType.Once };
Assert.Equal(120m, item.MonthlyAmount);
}

[Fact]
public void MonthlyAmount_MonthlyRecurring_ReturnsAmount()
{
var item = new FinanceItem { Amount = 50m, Recurring = RecurringType.Monthly };
Assert.Equal(50m, item.MonthlyAmount);
}

[Fact]
public void MonthlyAmount_YearlyRecurring_ReturnsDividedByTwelve()
{
var item = new FinanceItem { Amount = 1200m, Recurring = RecurringType.Yearly };
Assert.Equal(100m, item.MonthlyAmount);
}

[Fact]
public void MonthlyAmount_YearlyRecurring_HandlesNonEvenDivision()
{
var item = new FinanceItem { Amount = 100m, Recurring = RecurringType.Yearly };
var expected = 100m / 12m;
Assert.Equal(expected, item.MonthlyAmount);
}

[Fact]
public void Id_IsUnique_AcrossInstances()
{
var item1 = new FinanceItem();
var item2 = new FinanceItem();
Assert.NotEqual(item1.Id, item2.Id);
}
}
Loading
Loading