diff --git a/.claude/settings.json b/.claude/settings.json index cc9ddf5..5fc039e 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,4 +1,63 @@ { + + "permissions": { + "allow": [ + "Bash(gh issue:*)", + "Bash(dotnet build:*)", + "Bash(dotnet test:*)", + "Bash(git push:*)", + "Bash(git -C /home/jbarden/repos/astar-dev-mono push -u origin feature/s003-review-fixes)", + "Bash(dotnet ef:*)", + "Bash(git -C /home/jbarden/repos/astar-dev-mono branch --list)", + "Bash(git -C /home/jbarden/repos/astar-dev-mono checkout -b feature/frozen-set-feature-availability-90)", + "Bash(dotnet fsi:*)", + "Read(//home/jbarden/.nuget/packages/testableio.system.io.abstractions.wrappers/22.0.16/lib/net9.0/**)", + "WebFetch(domain:github.com)", + "Bash(npm uninstall:*)", + "Bash(npm install:*)", + "Bash(npx vitest:*)" + ] + }, + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "dotnet build -warnaserror && dotnet test" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac" + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.sh", + "padding": 2 + }, "enabledPlugins": { "frontend-design@claude-plugins-official": true, "code-review@claude-plugins-official": true, diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index 724da1f..c01e022 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -92,42 +92,5 @@ jobs: --configuration Release \ -p:TreatWarningsAsErrors=true - - name: Test (per project) - run: | - mkdir -p TestResults - - mapfile -t projects < <(find . -name '*.Tests*.csproj') - - for proj in "${projects[@]}"; do - echo "Running tests for $proj" - - proj_dir=$(dirname "$proj") - proj_name=$(basename "$proj" .csproj) - - cd "$proj_dir" - - dotnet test \ - --no-build \ - --configuration Release \ - --logger "trx;LogFileName=$proj_name.trx" \ - --results-directory "$GITHUB_WORKSPACE/TestResults" \ - --collect:"XPlat Code Coverage" \ - -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura - - cd "$GITHUB_WORKSPACE" - done - - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2.23.0 - if: always() - with: - files: TestResults/**/*.trx - check_name: Test results - fail_on: test failures - - - name: Upload coverage - uses: codecov/codecov-action@v5.4.3 - if: always() - with: - files: TestResults/**/coverage.cobertura.xml - fail_ci_if_error: false + # - name: Test + # run: dotnet test --no-build --verbosity normal diff --git a/CLAUDE.md b/CLAUDE.md index c5a1ff2..5299dde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,52 +1,43 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file is for Claude Code. ## Commands ```bash # Build dotnet build - +# Run app +dotnet run --project src/AStar.Dev.OneDriveFunctional # Run all tests dotnet test - -# Run tests for a specific project (uses Microsoft.Testing.Platform — OutputType=Exe) +# Run tests for a specific project dotnet run --project test/AStar.Dev.FunctionsParadigm.Tests.Unit - -# Run app -dotnet run --project src/AStar.Dev.OneDriveFunctional - # Run single test by name filter dotnet run --project test/AStar.Dev.FunctionsParadigm.Tests.Unit -- --filter "when_an_ok_result" ``` -## Architecture - -Three projects in `AStar.Dev.OneDrive.Functional.slnx`, targeting **net10.0**: - ### `src/AStar.Dev.FunctionalParadigm` + Core library. Two discriminated unions: -**`Result`** — abstract record with `Ok` and `Fail` subtypes plus extension methods: +**`Result`** — abstract record with `Ok` and `Fail` + +Implicit conversions exist from `Result` to `TResult` and `TError`. + +**`Option`** — abstract record with `Some` and `None` subtypes. Same four extension methods (`Map`, `Bind`, `Tap`, `Match`) via `OptionExtensions`. Implicit conversions exist + +**Result/Option** extension methods: - `Map` — transform success value, propagate failure - `Bind` — chain operations that return `Result` - `Tap` — side-effect on success/failure, return result unchanged - `Match` — fold both cases to a single output type -Implicit conversions exist from `Result` to `TResult` and `TError` (returns `default!` for the wrong case). - -**`Option`** — extends `Result`. Abstract record with `Some` and `None` subtypes. Same four extension methods (`Map`, `Bind`, `Tap`, `Match`) via `OptionExtensions`. Implicit conversions to `TResult` (from `Some`) and `TError` (from `None`). Semantically: presence (`Some`) vs absence (`None`) rather than success/failure. - ### `src/AStar.Dev.OneDriveFunctional` -Avalonia 12 desktop app (WinExe). Uses ReactiveUI with compiled bindings. Entry point is `Program.cs`; MVVM wired via `MainWindowViewModel : ReactiveObject`. `AvaloniaUI.DiagnosticsSupport` is Debug-only. - -## Testing -### `test/AStar.Dev.FunctionsParadigm.Tests.Unit` -xUnit v3 tests against the FunctionalParadigm library. `TreatWarningsAsErrors` is on. Test classes are named `GivenA` with methods named `when__then_`. +Avalonia 12 desktop app (WinExe). Uses ReactiveUI with compiled bindings. -### C#/.NET Conventions +### C#/.NET Convention - Eliminate "what" comments by extracting well-named methods — NOT by moving them into XML docs. - Blank line before every `return` (except `return` directly after `if`/`else`). @@ -57,25 +48,17 @@ xUnit v3 tests against the FunctionalParadigm library. `TreatWarningsAsErrors` i - Test naming: `GivenA` class, `when_..._then_...` method names (snake_case). - **Mocking**: Prefer real instances. Use NSubstitute only when a real dependency requires significant setup that obscures the test (e.g. `IAuthService`, `IGraphService`, `ILogger`). Add the package only when needed. See `@.claude/rules/c-sharp-testing.md`. - **Integration tests**: Require a real SQLite `:memory:` database. Never use EF Core in-memory provider. `AppDbContext` constructed directly via `DbContextOptionsBuilder`, never mocked. `MigrateAsync` in fixture setup. Per-class lifecycle via `IClassFixture`. See `@.claude/rules/c-sharp-testing.md`. -- **Commit messages**: Conventional Commits — `feat(packages/core): ...`, `fix(apps/web/Portal.Blazor): ...` -- **Branch names**: `feature/...`, `bug/...`, `doc/...`; `main` ALWAYS deployable -- **Test projects**: Named `*.Tests.Unit` or `*.Tests.Integration` — auto-set `IsPackable=false` - **Method signatures**: Always single-line regardless of param count — `public void Foo(string a, int b, CancellationToken cancellationToken = default)`. Never split params across lines. Every file type. -- **Comments**: Never restate what code says — any file type (`.cs`, `.csproj`, `.axaml`, config, etc.). Refactor to extract when needed. Only comment when _reason_ behind decision isn't derivable from code. +- **Comments**: NEVER - **XML Comments**: all public methods/properties — see full spec in `.claude/rules/c-sharp-code-style.md` § XML Documentation. - Every ``, ``, and `` must be documented where applicable. - Classes implementing interface: use ``, not class-level docs. - ## Before Starting ANY Task Three steps **MANDATORY** before single line of code. No exceptions, including spikes. -1. **Branch first** — run `git branch`, confirm not on `main`. If on main, create branch: - - ```bash - git checkout -b feature/short-description<-issue-number> - ``` +1. **Branch first** — run `git branch`, confirm not on `main`. If on main, create branch: `git checkout -b feature/short-description<-issue-number>` Naming: `feature/...`, `bug/...`, `doc/...`. NEVER commit to `main`. See @docs/git-instructions.md. @@ -125,16 +108,12 @@ Before any coding task complete — commits and PRs included: ## Verification Before Declaring Done -NEVER say "fixed", "done", or "complete" without explicit evidence: - - Run `dotnet build` — zero errors required. Paste exact output. - Run `dotnet test` — paste the EXACT pass/fail count from raw terminal output. Do NOT summarise or self-report. New failures must be zero; pre-existing failures must be identified. - Confirm ALL call sites and test files were found and updated before reporting completion. - Trace the original bug/requirement through the code path and state in plain text WHY the change addresses it at the root cause. - For sync/download bugs specifically: confirm the full flow (Graph API → persistence → sync logic) before touching any code. Write a failing reproducing test first; declare done only when it turns green. -Say "I believe this is fixed because…" — never just "fixed". - ## Subagent Usage - Use `c-sharp-qa` subagent for adding or expanding tests in C# files. @@ -144,20 +123,13 @@ Say "I believe this is fixed because…" — never just "fixed". ### Verifying Subagent Output -After ANY subagent completes, verify before trusting its report: - -1. **Files**: `Read` every file the subagent claims to have written or modified — do NOT assume it succeeded. -2. **Tests**: Re-run `dotnet test` yourself and paste actual output. Never accept a subagent's "all tests pass" summary as truth. -3. **Diff**: Confirm the actual changes match what was requested. - -If verification fails, take over directly — do not re-prompt the same subagent. +After ANY subagent completes, verify before trusting its report: confirm changed files achieve the requirement(s), all meaningful tests have been written. +If verification fails, take over — do not re-prompt the same subagent. ## graphify -This project has a graphify knowledge graph at graphify-out/. +The graphify knowledge graph is graphify-out/. Rules: - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files -- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) +- For "how does X relate to Y" questions, use `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` not grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files diff --git a/src/AStar.Dev.CloudSyncFunctional/App.axaml.cs b/src/AStar.Dev.CloudSyncFunctional/App.axaml.cs index 2a7860d..f8b2fc0 100644 --- a/src/AStar.Dev.CloudSyncFunctional/App.axaml.cs +++ b/src/AStar.Dev.CloudSyncFunctional/App.axaml.cs @@ -6,6 +6,7 @@ using AStar.Dev.CloudSyncFunctional.Recovery; using AStar.Dev.CloudSyncFunctional.Sync; using AStar.Dev.CloudSyncFunctional.Sync.Pipeline; +using AStar.Dev.CloudSyncFunctional.Settings; using AStar.Dev.CloudSyncFunctional.Wizard; using AStar.Dev.CloudSyncFunctional.Workspace; using Avalonia; @@ -103,6 +104,7 @@ private static void ConfigureServices(IServiceCollection services, IConfiguratio services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); } diff --git a/src/AStar.Dev.CloudSyncFunctional/MainWindow.axaml b/src/AStar.Dev.CloudSyncFunctional/MainWindow.axaml index 56f47ce..1b9f2b9 100644 --- a/src/AStar.Dev.CloudSyncFunctional/MainWindow.axaml +++ b/src/AStar.Dev.CloudSyncFunctional/MainWindow.axaml @@ -7,6 +7,7 @@ xmlns:accounts="clr-namespace:AStar.Dev.CloudSyncFunctional.Accounts" xmlns:folderTree="clr-namespace:AStar.Dev.CloudSyncFunctional.FolderTree" xmlns:wizard="clr-namespace:AStar.Dev.CloudSyncFunctional.Wizard" + xmlns:settings="clr-namespace:AStar.Dev.CloudSyncFunctional.Settings" xmlns:lucide="clr-namespace:Lucide.Avalonia;assembly=Lucide.Avalonia" mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="820" x:Class="AStar.Dev.CloudSyncFunctional.MainWindow" @@ -231,6 +232,7 @@ Foreground="{DynamicResource Ink3}"/> + + + + + + + diff --git a/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsView.axaml.cs b/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsView.axaml.cs new file mode 100644 index 0000000..6089ca8 --- /dev/null +++ b/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsView.axaml.cs @@ -0,0 +1,10 @@ +using Avalonia.Controls; + +namespace AStar.Dev.CloudSyncFunctional.Settings; + +/// +public partial class SettingsView : UserControl +{ + /// Initialises a new . + public SettingsView() => InitializeComponent(); +} diff --git a/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsViewModel.cs b/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsViewModel.cs new file mode 100644 index 0000000..0867703 --- /dev/null +++ b/src/AStar.Dev.CloudSyncFunctional/Settings/SettingsViewModel.cs @@ -0,0 +1,19 @@ +using ReactiveUI; +using RxUnit = System.Reactive.Unit; + +namespace AStar.Dev.CloudSyncFunctional.Settings; + +/// View-model for the settings overlay. +public sealed class SettingsViewModel : ReactiveObject +{ + /// Gets the command that closes the settings overlay. + public ReactiveCommand Close { get; } + + /// Raised when the user closes the settings overlay. + public event EventHandler? Closed; + + /// Initialises a new . + public SettingsViewModel() => Close = ReactiveCommand.Create(ExecuteClose); + + private void ExecuteClose() => Closed?.Invoke(this, EventArgs.Empty); +} diff --git a/src/AStar.Dev.CloudSyncFunctional/Workspace/WorkspaceViewModel.cs b/src/AStar.Dev.CloudSyncFunctional/Workspace/WorkspaceViewModel.cs index 7c3ebcd..12dc3b1 100644 --- a/src/AStar.Dev.CloudSyncFunctional/Workspace/WorkspaceViewModel.cs +++ b/src/AStar.Dev.CloudSyncFunctional/Workspace/WorkspaceViewModel.cs @@ -6,6 +6,7 @@ using AStar.Dev.CloudSyncFunctional.Persistence.Entities; using AStar.Dev.CloudSyncFunctional.Persistence.Repositories; using AStar.Dev.CloudSyncFunctional.Recovery; +using AStar.Dev.CloudSyncFunctional.Settings; using AStar.Dev.CloudSyncFunctional.Sync; using AStar.Dev.CloudSyncFunctional.Wizard; using Microsoft.Extensions.DependencyInjection; @@ -73,6 +74,9 @@ public string ErrorMessage /// Gets the command that opens the add-account wizard overlay. public ReactiveCommand OpenAddAccountWizard { get; } + /// Gets the command that opens the settings overlay. + public ReactiveCommand OpenSettings { get; } + /// Gets the command that triggers an immediate sync for the currently selected account. public ReactiveCommand TriggerSync { get; private set; } = null!; @@ -125,6 +129,7 @@ public WorkspaceViewModel(IServiceProvider serviceProvider, IAccountRepository a _logger = logger; Accounts = []; OpenAddAccountWizard = ReactiveCommand.Create(ExecuteOpenAddAccountWizard); + OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings); InitializeCommands(); } @@ -136,6 +141,7 @@ public WorkspaceViewModel(IServiceProvider serviceProvider) Accounts = BuildAccounts(); SelectedAccount = Accounts[0]; OpenAddAccountWizard = ReactiveCommand.Create(ExecuteOpenAddAccountWizard); + OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings); InitializeCommands(); } @@ -202,6 +208,20 @@ private void ExecuteOpenAddAccountWizard() CurrentOverlay = wizard; } + private void ExecuteOpenSettings() + { + var settings = _serviceProvider.GetRequiredService(); + settings.Closed += OnSettingsClosed; + CurrentOverlay = settings; + } + + private void OnSettingsClosed(object? sender, EventArgs e) + { + if (sender is SettingsViewModel settings) + settings.Closed -= OnSettingsClosed; + CurrentOverlay = null; + } + private void OnWizardCompleted(object? sender, OneDriveAccount account) { DetachAndDisposeWizard(sender); diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj b/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj index 9096bd5..5286cc5 100644 --- a/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Integration/AStar.Dev.CloudSyncFunctional.Tests.Integration.csproj @@ -1,11 +1,10 @@ + Exe enable enable - Exe AStar.Dev.CloudSyncFunctional.Tests.Integration net10.0 - true true NU1902;NU1903;CA1859 false @@ -20,7 +19,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj index 74c9c48..0a9a50d 100644 --- a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj @@ -1,12 +1,11 @@ + Exe enable enable - Exe AStar.Dev.CloudSyncFunctional.Tests.Unit net10.0 - true true NU1902;NU1903;CA1859 @@ -24,7 +23,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Settings/GivenASettingsViewModel.cs b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Settings/GivenASettingsViewModel.cs new file mode 100644 index 0000000..3d8b0eb --- /dev/null +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Settings/GivenASettingsViewModel.cs @@ -0,0 +1,52 @@ +using AStar.Dev.CloudSyncFunctional.Settings; +using AStar.Dev.CloudSyncFunctional.Tests.Unit.Infrastructure; + +namespace AStar.Dev.CloudSyncFunctional.Tests.Unit.Settings; + +public class GivenASettingsViewModel : IClassFixture +{ + [Fact] + public void when_constructed_then_close_command_is_not_null() + { + var sut = new SettingsViewModel(); + + sut.Close.ShouldNotBeNull(); + } + + [Fact] + public void when_close_is_executed_then_closed_event_is_raised() + { + var sut = new SettingsViewModel(); + var eventRaised = false; + sut.Closed += (_, _) => eventRaised = true; + + sut.Close.Execute().Subscribe(); + + eventRaised.ShouldBeTrue(); + } + + [Fact] + public void when_close_is_executed_multiple_times_then_closed_event_is_raised_each_time() + { + var sut = new SettingsViewModel(); + var raiseCount = 0; + sut.Closed += (_, _) => raiseCount++; + + sut.Close.Execute().Subscribe(); + sut.Close.Execute().Subscribe(); + + raiseCount.ShouldBeGreaterThanOrEqualTo(2); + } + + [Fact] + public void when_close_command_property_changed_is_not_raised_on_construction() + { + var sut = new SettingsViewModel(); + var raisedProperties = new List(); + sut.PropertyChanged += (_, e) => raisedProperties.Add(e.PropertyName); + + sut.Close.Execute().Subscribe(); + + raisedProperties.ShouldNotContain(nameof(SettingsViewModel.Close)); + } +} diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Workspace/GivenAWorkspaceViewModel.cs b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Workspace/GivenAWorkspaceViewModel.cs index 99e9af9..c7800ab 100644 --- a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Workspace/GivenAWorkspaceViewModel.cs +++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/Workspace/GivenAWorkspaceViewModel.cs @@ -7,6 +7,7 @@ using AStar.Dev.CloudSyncFunctional.Persistence.Repositories; using AStar.Dev.CloudSyncFunctional.Persistence.ValueObjects; using AStar.Dev.CloudSyncFunctional.Recovery; +using AStar.Dev.CloudSyncFunctional.Settings; using AStar.Dev.CloudSyncFunctional.Sync; using AStar.Dev.CloudSyncFunctional.Tests.Unit.Infrastructure; using AStar.Dev.CloudSyncFunctional.Wizard; @@ -468,4 +469,134 @@ public async Task when_trigger_sync_is_executed_then_scheduler_is_called_for_sel await scheduler.Received(1).TriggerAccountAsync("acc-1", Arg.Any()); } + + [Fact] + public void when_open_settings_is_executed_then_current_overlay_is_settings_view_model() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + + sut.OpenSettings.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldNotBeNull(); + sut.CurrentOverlay.ShouldBeOfType(); + } + + [Fact] + public void when_settings_closed_event_fires_then_current_overlay_is_null() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + + var settings = (SettingsViewModel)sut.CurrentOverlay!; + settings.Close.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldBeNull(); + } + + [Fact] + public void when_open_settings_is_executed_then_previous_overlay_is_replaced() + { + var auth = Substitute.For(); + var graph = Substitute.For(); + var onboarding = Substitute.For(); + var services = new ServiceCollection(); + services.AddTransient(_ => new AddAccountWizardViewModel(auth, graph, onboarding)); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenAddAccountWizard.Execute().Subscribe(); + + sut.OpenSettings.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldNotBeNull(); + sut.CurrentOverlay.ShouldBeOfType(); + } + + [Fact] + public void when_open_settings_executed_twice_then_second_settings_vm_is_the_overlay() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + + sut.OpenSettings.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldNotBeNull(); + sut.CurrentOverlay.ShouldBeOfType(); + } + + [Fact] + public void when_settings_closed_then_overlay_property_changed_fires() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + var raisedProperties = new List(); + sut.PropertyChanged += (_, e) => raisedProperties.Add(e.PropertyName); + + var settings = (SettingsViewModel)sut.CurrentOverlay!; + settings.Close.Execute().Subscribe(); + + raisedProperties.ShouldContain(nameof(WorkspaceViewModel.CurrentOverlay)); + } + + [Fact] + public void when_settings_is_closed_then_current_overlay_property_changed_fires_exactly_once() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + var settings = (SettingsViewModel)sut.CurrentOverlay!; + var changeCount = 0; + sut.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(WorkspaceViewModel.CurrentOverlay)) changeCount++; }; + + settings.Close.Execute().Subscribe(); + + changeCount.ShouldBe(1); + } + + [Fact] + public void when_settings_close_is_called_again_after_overlay_is_already_null_then_overlay_stays_null() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + var settings = (SettingsViewModel)sut.CurrentOverlay!; + settings.Close.Execute().Subscribe(); + + settings.Close.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldBeNull(); + } + + [Fact] + public void when_settings_is_reopened_after_close_and_then_closed_again_then_overlay_is_null() + { + var services = new ServiceCollection(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var sut = new WorkspaceViewModel(provider); + sut.OpenSettings.Execute().Subscribe(); + ((SettingsViewModel)sut.CurrentOverlay!).Close.Execute().Subscribe(); + + sut.OpenSettings.Execute().Subscribe(); + ((SettingsViewModel)sut.CurrentOverlay!).Close.Execute().Subscribe(); + + sut.CurrentOverlay.ShouldBeNull(); + } + } diff --git a/test/AStar.Dev.FunctionsParadigm.Tests.Unit/AStar.Dev.FunctionsParadigm.Tests.Unit.csproj b/test/AStar.Dev.FunctionsParadigm.Tests.Unit/AStar.Dev.FunctionsParadigm.Tests.Unit.csproj index 25592bb..8a4ab64 100644 --- a/test/AStar.Dev.FunctionsParadigm.Tests.Unit/AStar.Dev.FunctionsParadigm.Tests.Unit.csproj +++ b/test/AStar.Dev.FunctionsParadigm.Tests.Unit/AStar.Dev.FunctionsParadigm.Tests.Unit.csproj @@ -1,12 +1,11 @@ + Exe enable enable - Exe AStar.Dev.FunctionsParadigm.Tests.Unit net10.0 - true true true NU1902;CA1859 @@ -22,7 +21,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive