Les livres d'une même série se suivaient au hasard de l'alphabet, et rien ne disait qu'ils allaient ensemble. Ils tiennent maintenant sous le nom de leur série, à la place qu'occupait le premier d'entre eux — l'ordre général ne bouge donc pas — et ce nom mène à la série, seul écran qui montre aussi les tomes qui manquent. Le regroupement ne cache ni ne duplique rien : une bascule le défait, et un livre rattaché à deux séries ne paraît qu'une fois, toujours sous la même. La règle est une fonction pure, avec ses tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
4.9 KiB
C#
137 lines
4.9 KiB
C#
using MaBibli.Client.Services;
|
|
using MaBibli.Shared.Dtos;
|
|
|
|
namespace MaBibli.Tests;
|
|
|
|
/// <summary>
|
|
/// Regroupement des tomes d'une série au catalogue (2026-08-22).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// L'écran n'est pas éprouvable ici, mais la règle l'est : ces tests portent sur ce qui a été
|
|
/// <b>décidé</b> — la place du bloc, l'ordre des tomes à l'intérieur, et le fait que rien ne
|
|
/// disparaisse ni ne se duplique.
|
|
/// </remarks>
|
|
public class GroupementCatalogueTests
|
|
{
|
|
private static LivreDto Livre(int id, string titre) =>
|
|
new()
|
|
{
|
|
Id = id,
|
|
Titre = titre,
|
|
Format = MaBibli.Shared.Entites.Format.Physique,
|
|
DateAjout = DateTime.UtcNow,
|
|
};
|
|
|
|
private static SerieDto Serie(int id, string titre, params (int Position, int LivreId)[] tomes) =>
|
|
new()
|
|
{
|
|
Id = id,
|
|
Titre = titre,
|
|
Elements =
|
|
[
|
|
.. tomes.Select(t => new ElementSerieDto
|
|
{
|
|
Id = t.LivreId,
|
|
Position = t.Position,
|
|
Titre = $"tome {t.Position}",
|
|
LivreId = t.LivreId,
|
|
}),
|
|
],
|
|
};
|
|
|
|
/// <summary>
|
|
/// ⚠️ Le cas qui a tranché la décision : le bloc se pose à la place du PREMIER tome, pour
|
|
/// que l'ordre général du catalogue ne bouge pas.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Le_bloc_prend_la_place_de_son_premier_tome()
|
|
{
|
|
List<LivreDto> catalogue =
|
|
[
|
|
Livre(1, "Dracula"),
|
|
Livre(2, "L'Éclat de cristal"),
|
|
Livre(3, "Dune"),
|
|
Livre(4, "Terre natale"),
|
|
];
|
|
|
|
var entrees = GroupementCatalogue.Grouper(
|
|
catalogue, [Serie(9, "La Légende de Drizzt", (1, 2), (0, 4))]);
|
|
|
|
Assert.Collection(
|
|
entrees,
|
|
e => Assert.Equal("Dracula", Assert.Single(e.Livres).Titre),
|
|
e =>
|
|
{
|
|
Assert.Equal("La Légende de Drizzt", e.Serie!.Titre);
|
|
|
|
// À l'intérieur, l'ordre est celui de LECTURE, pas celui du catalogue.
|
|
Assert.Equal(["Terre natale", "L'Éclat de cristal"], e.Livres.Select(l => l.Titre));
|
|
},
|
|
e => Assert.Equal("Dune", Assert.Single(e.Livres).Titre));
|
|
}
|
|
|
|
/// <summary>Aucun livre ne disparaît, aucun n'apparaît deux fois : c'est la règle du catalogue.</summary>
|
|
[Fact]
|
|
public void Le_regroupement_ne_perd_ni_ne_duplique_aucun_livre()
|
|
{
|
|
List<LivreDto> catalogue = [Livre(1, "A"), Livre(2, "B"), Livre(3, "C")];
|
|
|
|
// Le livre 2 est rattaché à DEUX séries — le modèle l'autorise.
|
|
var entrees = GroupementCatalogue.Grouper(
|
|
catalogue, [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2), (1, 3))]);
|
|
|
|
Assert.Equal([1, 2, 3], entrees.SelectMany(e => e.Livres).Select(l => l.Id).Order());
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⚠️ La série retenue est la première par ordre alphabétique, et non celle que l'API a
|
|
/// rendue en premier : sans cela, le bloc changerait de nom d'un chargement à l'autre.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Un_livre_de_deux_series_est_range_sous_la_premiere_alphabetiquement()
|
|
{
|
|
var entrees = GroupementCatalogue.Grouper(
|
|
[Livre(2, "B")], [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2))]);
|
|
|
|
Assert.Equal("Alpha", Assert.Single(entrees).Serie!.Titre);
|
|
}
|
|
|
|
/// <summary>Sans série connue — hors-ligne sans instantané, ou groupement défait — la liste est plate.</summary>
|
|
[Fact]
|
|
public void Sans_series_la_liste_reste_celle_du_catalogue()
|
|
{
|
|
List<LivreDto> catalogue = [Livre(1, "A"), Livre(2, "B")];
|
|
|
|
var entrees = GroupementCatalogue.Grouper(catalogue, null);
|
|
|
|
Assert.All(entrees, e => Assert.Null(e.Serie));
|
|
Assert.Equal(["A", "B"], entrees.Select(e => e.Livres[0].Titre));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Un seul tome possédé forme quand même un bloc : c'est ce qui dit à quoi ce livre
|
|
/// appartient, et ce qui mène à la série où figurent les tomes manquants.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Un_seul_tome_possede_forme_quand_meme_un_bloc()
|
|
{
|
|
var entrees = GroupementCatalogue.Grouper(
|
|
[Livre(4, "Terre natale")], [Serie(9, "L'Elfe noir", (0, 4), (1, 5))]);
|
|
|
|
Assert.Equal("L'Elfe noir", Assert.Single(entrees).Serie!.Titre);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Les tomes filtrés restent absents : le regroupement rassemble ce que la liste contient,
|
|
/// il ne va pas rechercher ce qu'un filtre a écarté.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Un_filtre_ne_ramene_pas_les_tomes_ecartes()
|
|
{
|
|
var entrees = GroupementCatalogue.Grouper(
|
|
[Livre(5, "L'Éclat de cristal")], [Serie(9, "Drizzt", (0, 4), (1, 5))]);
|
|
|
|
Assert.Equal(["L'Éclat de cristal"], Assert.Single(entrees).Livres.Select(l => l.Titre));
|
|
}
|
|
}
|