Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
Adds SoftDelete/Trash across characters, chapters, locations, beats with a purge schedule and Trash page. Reworks the web client for keyboard-driven navigation (focus helpers, help overlay, keyboard.md doc). Moves the ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Novelly.Api.Chapters;
|
||||
using Novelly.Api.Novels;
|
||||
using Novelly.Api.Trash;
|
||||
|
||||
namespace Novelly.Api.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class TrashPurgeScheduleTests
|
||||
{
|
||||
[Test]
|
||||
public void Before_the_run_time_the_next_run_is_today() =>
|
||||
Assert.That(
|
||||
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 1, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
||||
Is.EqualTo(new DateTimeOffset(2026, 8, 20, 2, 0, 0, TimeSpan.Zero)));
|
||||
|
||||
[Test]
|
||||
public void After_the_run_time_the_next_run_is_tomorrow() =>
|
||||
Assert.That(
|
||||
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 3, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
||||
Is.EqualTo(new DateTimeOffset(2026, 8, 21, 2, 0, 0, TimeSpan.Zero)));
|
||||
|
||||
[Test]
|
||||
public void Exactly_at_the_run_time_the_next_run_is_tomorrow() =>
|
||||
Assert.That(
|
||||
TrashPurgeSchedule.NextRunAfter(new DateTimeOffset(2026, 8, 20, 2, 0, 0, TimeSpan.Zero), new TimeOnly(2, 0)),
|
||||
Is.EqualTo(new DateTimeOffset(2026, 8, 21, 2, 0, 0, TimeSpan.Zero)));
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public class TrashPurgeSweepTests : ServiceTestFixture
|
||||
{
|
||||
private Guid _novelId;
|
||||
|
||||
protected override void OnSetUp() =>
|
||||
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
|
||||
|
||||
[Test]
|
||||
public async Task A_sweep_removes_only_items_older_than_the_retention_window()
|
||||
{
|
||||
var old = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Old News"));
|
||||
var recent = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Fresh Off The Press"));
|
||||
|
||||
await Chapters.DeleteAsync(old!.Id);
|
||||
await Chapters.DeleteAsync(recent!.Id);
|
||||
|
||||
using (var context = Db.CreateContext())
|
||||
{
|
||||
var oldRow = await context.Chapters.IgnoreQueryFilters().SingleAsync(c => c.Id == old.Id);
|
||||
oldRow.DeletedAt = DateTimeOffset.UtcNow.AddDays(-31);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
|
||||
var (characters, chapters, locations) = await TrashPurgeRunner.SweepAsync(Db.Context, cutoff, CancellationToken.None);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That((characters, chapters, locations), Is.EqualTo((0, 1, 0)));
|
||||
});
|
||||
|
||||
using var verification = Db.CreateContext();
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await verification.Chapters.IgnoreQueryFilters().AnyAsync(c => c.Id == old.Id), Is.False);
|
||||
Assert.That(await verification.Chapters.IgnoreQueryFilters().AnyAsync(c => c.Id == recent.Id), Is.True);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user