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
60 changes: 60 additions & 0 deletions Navigation.Test/JsonFilePersistenceProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Navigation.Test;

using ktsu.Navigation.Models;
using ktsu.Navigation.Services;

[TestClass]
public class JsonFilePersistenceProviderTests
{
private string? _filePath;

[TestInitialize]
public void Setup() => _filePath = Path.Combine(Path.GetTempPath(), $"navigation-{Guid.NewGuid():N}.json");

[TestCleanup]
public void Cleanup()
{
if (File.Exists(_filePath))
{
File.Delete(_filePath);
}
}

[TestMethod]
public async Task SaveThenLoad_IntoNewNavigation_RestoresCurrentAndStacks()
{
// Arrange
JsonFilePersistenceProvider<NavigationItem> provider = new(_filePath!);
Navigation<NavigationItem> navigation = new(null, provider);
navigation.NavigateTo(new NavigationItem("a", "A"));
navigation.NavigateTo(new NavigationItem("b", "B"));
navigation.NavigateTo(new NavigationItem("c", "C"));
navigation.GoBack();
await navigation.SaveStateAsync().ConfigureAwait(false);

Check warning on line 35 in Navigation.Test/JsonFilePersistenceProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Navigation&issues=AaDgD0X8obAzMXtwHGKD&open=AaDgD0X8obAzMXtwHGKD&pullRequest=58

// Act
Navigation<NavigationItem> restored = new(null, provider);
bool loaded = await restored.LoadStateAsync().ConfigureAwait(false);

Check warning on line 39 in Navigation.Test/JsonFilePersistenceProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Navigation&issues=AaDgD0X8obAzMXtwHGKE&open=AaDgD0X8obAzMXtwHGKE&pullRequest=58

// Assert
Assert.IsTrue(loaded);
Assert.AreEqual(3, restored.Count);
Assert.AreEqual("b", restored.Current?.Id);
Assert.AreEqual("B", restored.Current?.DisplayName);
Assert.AreEqual("a", string.Join(",", restored.GetBackStack().Select(item => item.Id)));
Assert.AreEqual("c", string.Join(",", restored.GetForwardStack().Select(item => item.Id)));
}

[TestMethod]
public async Task LoadStateAsync_UnreadableData_ReturnsNull()
{
// Arrange
await File.WriteAllTextAsync(_filePath!, """{"items":[{"id":"a","displayName":"A"}],"currentIndex":5}""").ConfigureAwait(false);

Check warning on line 54 in Navigation.Test/JsonFilePersistenceProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Navigation&issues=AaDgD0X8obAzMXtwHGKB&open=AaDgD0X8obAzMXtwHGKB&pullRequest=58
JsonFilePersistenceProvider<NavigationItem> provider = new(_filePath!);

// Act & Assert
Assert.IsNull(await provider.LoadStateAsync().ConfigureAwait(false));

Check warning on line 58 in Navigation.Test/JsonFilePersistenceProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Navigation&issues=AaDgD0X8obAzMXtwHGKC&open=AaDgD0X8obAzMXtwHGKC&pullRequest=58
}
}
13 changes: 13 additions & 0 deletions Navigation/Models/NavigationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace ktsu.Navigation.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using ktsu.Navigation.Contracts;

/// <summary>
Expand Down Expand Up @@ -35,6 +36,18 @@ public NavigationState(IEnumerable<T> items, int currentIndex)
CreatedAt = DateTime.UtcNow;
}

/// <summary>
/// Initializes a new instance of the <see cref="NavigationState{T}"/> class when deserializing.
/// System.Text.Json requires each constructor parameter to have the same type as the property it binds to.
/// </summary>
/// <param name="items">The items in the navigation stack</param>
/// <param name="currentIndex">The index of the current item</param>
[JsonConstructor]
public NavigationState(IReadOnlyList<T> items, int currentIndex)
: this((IEnumerable<T>)items, currentIndex)
{
}

/// <inheritdoc />
public IReadOnlyList<T> Items { get; }

Expand Down
5 changes: 5 additions & 0 deletions Navigation/Services/JsonFilePersistenceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public async Task SaveStateAsync(INavigationState<T> state, CancellationToken ca
// If file read fails, return null
return null;
}
catch (ArgumentException)
{
// If the saved state is invalid (for example, a current index outside the items), return null
return null;
}
}

/// <inheritdoc />
Expand Down
Loading