Dashboard replaces the tabbed overview: Outline + Characters cards (recent-first), gated by a new Project.Phase (Brainstorming -> Outlining -> Writing -> Editing -> Complete). Brainstorming shows just a notes editor; existing projects backfill to Outlining. Old brief editor moves to a Settings route. Beat table now renders as plain readable text, turning into editable fields only for the clicked row. Fixed horizontal overflow on the chapters list and beat table caused by unconstrained CSS grid/table tracks sizing to their widest unwrapped child. Widened the project layout's max width.
168 lines
6.7 KiB
C#
168 lines
6.7 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Novelly.Api.Chapters;
|
|
using Novelly.Api.Characters;
|
|
using Novelly.Api.Common;
|
|
using Novelly.Api.Projects;
|
|
using Novelly.Api.Scenes;
|
|
|
|
namespace Novelly.Api.Tests;
|
|
|
|
[TestFixture]
|
|
public class ProjectDataTests : ServiceTestFixture
|
|
{
|
|
private async Task<Guid> NewProjectAsync() =>
|
|
(await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
|
|
|
|
[Test]
|
|
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
|
{
|
|
var id = (await Projects.CreateAsync(
|
|
new CreateProjectRequest("Draft", Genre: "Fantasy", Logline: "A cartographer goes to sea."))).Id;
|
|
|
|
var afterPartialUpdate = (await Projects.UpdateAsync(id, new UpdateProjectRequest(Title: "The Salt Road")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(afterPartialUpdate.Title, Is.EqualTo("The Salt Road"));
|
|
Assert.That(afterPartialUpdate.Genre, Is.EqualTo("Fantasy"));
|
|
Assert.That(afterPartialUpdate.Logline, Is.EqualTo("A cartographer goes to sea."));
|
|
});
|
|
|
|
var afterClear = (await Projects.UpdateAsync(id, new UpdateProjectRequest(Genre: "")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(afterClear.Genre, Is.Null);
|
|
Assert.That(afterClear.Logline, Is.EqualTo("A cartographer goes to sea."));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task New_projects_start_in_the_brainstorming_phase_and_can_be_advanced()
|
|
{
|
|
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
|
|
Assert.That(project.Phase, Is.EqualTo(ProjectPhase.Brainstorming));
|
|
|
|
var afterAdvance = (await Projects.UpdateAsync(project.Id, new UpdateProjectRequest(Phase: ProjectPhase.Outlining)))!;
|
|
Assert.That(afterAdvance.Phase, Is.EqualTo(ProjectPhase.Outlining));
|
|
|
|
var afterUnrelatedUpdate = (await Projects.UpdateAsync(project.Id, new UpdateProjectRequest(Genre: "Fantasy")))!;
|
|
Assert.That(afterUnrelatedUpdate.Phase, Is.EqualTo(ProjectPhase.Outlining));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Chapters_are_numbered_in_sequence_when_no_number_is_given()
|
|
{
|
|
var projectId = await NewProjectAsync();
|
|
|
|
var first = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
|
var second = await Chapters.CreateAsync(projectId, new CreateChapterRequest("The Harbour"));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(first.Number, Is.EqualTo(1));
|
|
Assert.That(second.Number, Is.EqualTo(2));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Word_count_is_recomputed_whenever_prose_changes()
|
|
{
|
|
var projectId = await NewProjectAsync();
|
|
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
|
|
|
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
|
"The dock at dawn", Prose: "Five words go right here"));
|
|
|
|
Assert.That(scene.WordCount, Is.EqualTo(5));
|
|
|
|
var rewritten = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(
|
|
Prose: "Now\nthere are seven words in total")))!;
|
|
|
|
Assert.That(rewritten.WordCount, Is.EqualTo(7));
|
|
|
|
var cleared = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: "")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(cleared.Prose, Is.Null);
|
|
Assert.That(cleared.WordCount, Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Scene_updates_that_omit_prose_leave_the_draft_untouched()
|
|
{
|
|
var projectId = await NewProjectAsync();
|
|
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
|
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
|
"The dock at dawn", Prose: "The tide came in slow."));
|
|
|
|
var updated = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: DraftStatus.Revised)))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(updated.Prose, Is.EqualTo("The tide came in slow."));
|
|
Assert.That(updated.WordCount, Is.EqualTo(5));
|
|
Assert.That(updated.Status, Is.EqualTo(DraftStatus.Revised));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Deleting_a_project_takes_its_characters_chapters_and_scenes()
|
|
{
|
|
var projectId = await NewProjectAsync();
|
|
await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
|
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
|
await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest("The dock at dawn"));
|
|
|
|
await Projects.DeleteAsync(projectId);
|
|
|
|
using var verification = Db.CreateContext();
|
|
|
|
Assert.Multiple(async () =>
|
|
{
|
|
Assert.That(await verification.Projects.CountAsync(), Is.EqualTo(0));
|
|
Assert.That(await verification.Characters.CountAsync(), Is.EqualTo(0));
|
|
Assert.That(await verification.Chapters.CountAsync(), Is.EqualTo(0));
|
|
Assert.That(await verification.Scenes.CountAsync(), Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Relating_characters_across_projects_is_refused()
|
|
{
|
|
var firstProject = await NewProjectAsync();
|
|
var secondProject = (await Projects.CreateAsync(new CreateProjectRequest("Other Book"))).Id;
|
|
|
|
var ines = await Characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines"));
|
|
var stranger = await Characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger"));
|
|
|
|
Assert.That(
|
|
async () => await Characters.AddRelationshipAsync(
|
|
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")),
|
|
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Relationships_resolve_the_other_character_by_name()
|
|
{
|
|
var projectId = await NewProjectAsync();
|
|
var ines = await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
|
var mara = await Characters.CreateAsync(projectId, new CreateCharacterRequest("Mara"));
|
|
|
|
var updated = (await Characters.AddRelationshipAsync(
|
|
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", "Estranged since the fire.")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(updated.Relationships, Has.Count.EqualTo(1));
|
|
Assert.That(updated.Relationships[0].RelatedCharacter!.Name, Is.EqualTo("Mara"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Reading_a_missing_project_returns_null_rather_than_throwing() =>
|
|
Assert.That(await Projects.GetAsync(Guid.NewGuid()), Is.Null);
|
|
}
|