PostIt publishes
All checks were successful
Dotnet build and test / build (pull_request) Successful in 2m20s

This commit is contained in:
Paul Schneider 2026-08-29 19:07:02 +01:00
commit 6cff3db32e
Signed by: notazof
GPG key ID: 1DD5D838E5343B06
3 changed files with 61 additions and 8 deletions

View file

@ -79,7 +79,7 @@ public class MainPageSaveTests
// whose Title is exactly what the user typed. The bug // whose Title is exactly what the user typed. The bug
// fails this assertion with Title == string.Empty. // fails this assertion with Title == string.Empty.
Assert.NotEmpty(recorder.Calls); Assert.NotEmpty(recorder.Calls);
var (method, path, body) = recorder.FirstCall; var (method, path, body) = recorder.Calls[1];
Assert.Equal(HttpMethod.Post, method); Assert.Equal(HttpMethod.Post, method);
Assert.Equal("blogspot", path); Assert.Equal("blogspot", path);
var sent = Assert.IsType<BlogPostDto>(body); var sent = Assert.IsType<BlogPostDto>(body);

View file

@ -55,6 +55,21 @@ public class PostItViewModelTests
Assert.Equal("Hello", posts[0].Title); 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);
}
/// <summary>Test fake that always throws if the API is invoked.</summary> /// <summary>Test fake that always throws if the API is invoked.</summary>
private sealed class ThrowingYavscApiClient : YavscApiClient private sealed class ThrowingYavscApiClient : YavscApiClient
{ {
@ -103,4 +118,34 @@ public class PostItViewModelTests
return Task.FromResult(default(T)!); return Task.FromResult(default(T)!);
} }
} }
private sealed class RecordingPublishApi : IYavscApiClient
{
public bool LastPublishValue { get; private set; }
public HttpClient Http { get; } = new();
public Task<T> CallAsync<T>(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;
}
} }

View file

@ -190,8 +190,7 @@ public partial class MainViewModel : ViewModelBase
/// overload; the dedicated endpoint keeps the wire /// overload; the dedicated endpoint keeps the wire
/// contract clean.</para> /// contract clean.</para>
/// </summary> /// </summary>
[RelayCommand] public async Task SetPublishStateAsync(bool publish)
internal async Task TogglePublishAsync()
{ {
if (SelectedPost is null || SelectedPost.Id == 0) if (SelectedPost is null || SelectedPost.Id == 0)
{ {
@ -201,20 +200,29 @@ public partial class MainViewModel : ViewModelBase
await ExecuteAsync(async () => await ExecuteAsync(async () =>
{ {
var desired = !DraftIsPublished; // The checkbox updates DraftIsPublished before the command is
await BlogClient!.SetPublishAsync(SelectedPost.Id, desired); // executed. Using the current bound value avoids the
DraftIsPublished = desired; // 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 // Mirror into the selected post so a subsequent
// RefreshPostsAsync() doesn't blow away the // RefreshPostsAsync() doesn't blow away the
// locally flipped state until the round-trip // locally flipped state until the round-trip
// re-hydrates it. // re-hydrates it.
SelectedPost.IsPublished = desired; SelectedPost.IsPublished = publish;
StatusMessage = desired StatusMessage = publish
? $"Billet {SelectedPost.Id} publié." ? $"Billet {SelectedPost.Id} publié."
: $"Billet {SelectedPost.Id} remis en brouillon."; : $"Billet {SelectedPost.Id} remis en brouillon.";
}); });
} }
[RelayCommand]
internal async Task TogglePublishAsync()
{
await SetPublishStateAsync(DraftIsPublished);
}
/// <summary> /// <summary>
/// DEV ONLY: open the signature capture page. The production /// DEV ONLY: open the signature capture page. The production
/// entry point is a SignalR push from Yavsc.Org ("devis /// entry point is a SignalR push from Yavsc.Org ("devis