diff --git a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs index cef67f0f..0fd5627c 100644 --- a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs +++ b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs @@ -79,7 +79,7 @@ public class MainPageSaveTests // whose Title is exactly what the user typed. The bug // fails this assertion with Title == string.Empty. Assert.NotEmpty(recorder.Calls); - var (method, path, body) = recorder.FirstCall; + var (method, path, body) = recorder.Calls[1]; Assert.Equal(HttpMethod.Post, method); Assert.Equal("blogspot", path); var sent = Assert.IsType(body); diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs index d2c5d78e..1a867bd6 100644 --- a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs @@ -55,6 +55,21 @@ public class PostItViewModelTests Assert.Equal("Hello", posts[0].Title); } + [Fact] + public async Task TogglePublishCommand_uses_the_current_checked_state_without_inverting_it() + { + var api = new RecordingPublishApi(); + var blog = new BlogApiClient(api, "http://localhost/"); + var viewModel = new MainViewModel(blog); + + viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false }; + + await viewModel.SetPublishStateAsync(true); + + Assert.True(api.LastPublishValue); + Assert.True(viewModel.DraftIsPublished); + } + /// Test fake that always throws if the API is invoked. private sealed class ThrowingYavscApiClient : YavscApiClient { @@ -103,4 +118,34 @@ public class PostItViewModelTests return Task.FromResult(default(T)!); } } + + private sealed class RecordingPublishApi : IYavscApiClient + { + public bool LastPublishValue { get; private set; } + public HttpClient Http { get; } = new(); + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase)) + { + var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value; + LastPublishValue = publish; + } + + return Task.FromResult(default(T)!); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase)) + { + var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value; + LastPublishValue = publish; + } + + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } } diff --git a/src/PostIt/PostIt/ViewModels/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs index 2b065cae..84e10cfb 100644 --- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs @@ -190,8 +190,7 @@ public partial class MainViewModel : ViewModelBase /// overload; the dedicated endpoint keeps the wire /// contract clean. /// - [RelayCommand] - internal async Task TogglePublishAsync() + public async Task SetPublishStateAsync(bool publish) { if (SelectedPost is null || SelectedPost.Id == 0) { @@ -201,20 +200,29 @@ public partial class MainViewModel : ViewModelBase await ExecuteAsync(async () => { - var desired = !DraftIsPublished; - await BlogClient!.SetPublishAsync(SelectedPost.Id, desired); - DraftIsPublished = desired; + // The checkbox updates DraftIsPublished before the command is + // executed. Using the current bound value avoids the + // double-toggle bug in which the UI has already flipped the + // state and the command flips it again. + await BlogClient!.SetPublishAsync(SelectedPost.Id, publish); + DraftIsPublished = publish; // Mirror into the selected post so a subsequent // RefreshPostsAsync() doesn't blow away the // locally flipped state until the round-trip // re-hydrates it. - SelectedPost.IsPublished = desired; - StatusMessage = desired + SelectedPost.IsPublished = publish; + StatusMessage = publish ? $"Billet {SelectedPost.Id} publiƩ." : $"Billet {SelectedPost.Id} remis en brouillon."; }); } + [RelayCommand] + internal async Task TogglePublishAsync() + { + await SetPublishStateAsync(DraftIsPublished); + } + /// /// DEV ONLY: open the signature capture page. The production /// entry point is a SignalR push from Yavsc.Org ("devis