- Catalogage rapide (douchette, code-barres)
- Cataloguer en rafale — nouvel écran /ajout/rafale : on scanne une pile de livres à la suite dans une zone de texte, chaque code est traité (BnF puis OpenLibrary), les doublons connus sont passés automatiquement. La collecte marche même hors-ligne. Le compte rendu liste maintenant les livres créés, en lien vers leur fiche, et reste consultable en revenant sur l'écran même après une rafale entièrement réussie. Le catalogue reconnaît un ISBN dans sa barre de recherche (13 ou 10 chiffres, avec ou sans tirets) : scanner un livre en main ouvre directement sa fiche s'il n'y en a qu'un. Un bouton « Scanner » l'alimente, actif hors-ligne. - Ajouter un tome à une série accepte aussi un ISBN dans le champ manuel : le catalogue est cherché d'abord (rattachement direct si un seul exemplaire), sinon la BnF prend le relais. Séries et sagas - Numéro de tome distinct de la position de lecture : on peut indiquer « c'est le tome 7 » même si on ne possède pas les six premiers ; l'ordre de lecture reste un réglage séparé (utile pour les préquelles). - Tri par numéro en plus du tri par ordre de lecture quand des tomes en portent un. - Panneau « Ajouter » regroupé et repliable sur la fiche d'une série (manuellement / en rafale / depuis le catalogue / depuis les envies), au lieu de quatre formulaires ouverts en permanence. - Filtre catalogue « sans couverture » pour repérer les livres à illustrer. Le catalogue groupe les tomes d'une même série sous un bloc repliable, avec un décompte plus clair (affichés / possédés / total). - Corrections directes sur la fiche - Effacer un prêt saisi par erreur (bouton ✕ sur chaque ligne, avec confirmation), sans passer par « rendre ». - Corriger une couverture manquante ou cassée en cliquant dessus : le champ d'adresse s'ouvre focalisé, Entrée enregistre. Étendu aux numéros de revue dans la dernière modification. - Les thèmes déjà utilisés dans la bibliothèque sont proposés à la frappe. - La recherche d'un livre à rattacher montre des suggestions dès le focus, sans attendre de taper. - Visuel : un rendu manquant après une écriture asynchrone dans le formulaire de livre, une bascule de rôle cassée, des débordements à 320 px, et le style d'un bouton-lien qui restait souligné.
This commit is contained in:
@@ -33,11 +33,13 @@ public class FiltreLivresLocalTests
|
||||
private static Livre Livre(
|
||||
int id, string titre, Auteur? auteur, Format format, Statut? statut,
|
||||
TypeDocument type = TypeDocument.NonPrecise,
|
||||
string? preteA = null, bool pretClos = false)
|
||||
string? preteA = null, bool pretClos = false, string? couverture = null,
|
||||
string? isbn = null)
|
||||
{
|
||||
var livre = new Livre
|
||||
{
|
||||
Id = id, Titre = titre, Format = format, TypeDocument = type, AjoutePar = Lecteur,
|
||||
CoverUrl = couverture, Isbn = isbn,
|
||||
};
|
||||
livre.RecalculerFormes();
|
||||
|
||||
@@ -70,12 +72,20 @@ public class FiltreLivresLocalTests
|
||||
|
||||
private static readonly Livre[] Entites =
|
||||
[
|
||||
Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, TypeDocument.Roman),
|
||||
Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, TypeDocument.BandeDessinee),
|
||||
// ⚠️ Germinal est rangé sous son ISBN-10, comme tout ce qui a paru avant 2007 — alors
|
||||
// qu'un code-barres se lit toujours en 13. C'est le cas que la recherche par code doit
|
||||
// rattraper, sans quoi tout le fonds ancien serait introuvable à la douchette.
|
||||
Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, TypeDocument.Roman,
|
||||
couverture: "https://exemple.test/germinal.jpg", isbn: "2253004227"),
|
||||
Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, TypeDocument.BandeDessinee,
|
||||
isbn: "9782070612758"),
|
||||
Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours, TypeDocument.Roman,
|
||||
preteA: "Marie"),
|
||||
preteA: "Marie", couverture: "https://exemple.test/horla.jpg"),
|
||||
Livre(4, "Bel-Ami", Maupassant, Format.Numerique, null, preteA: "Paul", pretClos: true),
|
||||
Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire),
|
||||
|
||||
// ⚠️ Couverture VIDE, pas absente. C'est le cas où les deux implémentations pourraient
|
||||
// diverger sans qu'on le voie : le serveur écrit `IsNullOrEmpty` en SQL, le client en C#.
|
||||
Livre(5, "Œuvres complètes", Zola, Format.Physique, Statut.ALire, couverture: ""),
|
||||
Livre(6, "L'Éducation sentimentale", null, Format.Physique, null),
|
||||
];
|
||||
|
||||
@@ -95,6 +105,8 @@ public class FiltreLivresLocalTests
|
||||
|
||||
// Ce que l'API expose : l'emprunteur du prêt EN COURS, rien de l'historique clos.
|
||||
PreteA = l.Prets.FirstOrDefault(p => p.DateRetour is null)?.Emprunteur,
|
||||
CoverUrl = l.CoverUrl,
|
||||
Isbn = l.Isbn,
|
||||
Auteurs = l.Auteurs
|
||||
.Select(la => new AuteurDto { Id = la.AuteurId, Nom = la.Auteur!.Nom })
|
||||
.ToList(),
|
||||
@@ -112,6 +124,25 @@ public class FiltreLivresLocalTests
|
||||
new CritereLivres { Recherche = "bete" },
|
||||
new CritereLivres { Recherche = "oeuvres" },
|
||||
new CritereLivres { Recherche = "introuvable" },
|
||||
|
||||
// Recherche par CODE : elle exclut la recherche par mots, et confronte les deux formes
|
||||
// d'ISBN. Un fonds ancien est rangé en 10, une douchette lit toujours en 13.
|
||||
new CritereLivres { Recherche = "9782253004226" }, // 13 scanné → fiche rangée en 10
|
||||
new CritereLivres { Recherche = "2253004227" }, // 10 tapé tel quel
|
||||
new CritereLivres { Recherche = "978-2-07-061275-8" }, // tirets de l'affichage
|
||||
new CritereLivres { Recherche = "9782070612758" },
|
||||
new CritereLivres { Recherche = "97820706" }, // fragment : préfixe d'un ISBN
|
||||
new CritereLivres { Recherche = "9999999999999" }, // aucun livre
|
||||
new CritereLivres { Recherche = "9782070612758", Format = Format.Physique },
|
||||
new CritereLivres { SansCouverture = true },
|
||||
new CritereLivres { SansCouverture = false },
|
||||
|
||||
// Croisements : le critère doit se composer avec les autres, pas les remplacer.
|
||||
new CritereLivres { SansCouverture = true, Format = Format.Physique },
|
||||
new CritereLivres { SansCouverture = true, Recherche = "zola" },
|
||||
new CritereLivres { SansCouverture = false, Statut = Statut.Lu },
|
||||
new CritereLivres { SansCouverture = true, Prete = false },
|
||||
|
||||
new CritereLivres { Format = Format.Physique },
|
||||
new CritereLivres { Format = Format.Numerique },
|
||||
new CritereLivres { TypeDocument = TypeDocument.Roman },
|
||||
@@ -186,6 +217,25 @@ public class FiltreLivresLocalTests
|
||||
l => l.Id == 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_code_barres_retrouve_un_livre_range_sous_son_isbn_10()
|
||||
{
|
||||
// Le geste visé : le livre est en main, la douchette tape son EAN-13 dans la recherche.
|
||||
var resultat = FiltreLivresLocal.Appliquer(
|
||||
Instantane, new CritereLivres { Recherche = "9782253004226" });
|
||||
|
||||
Assert.Equal(1, Assert.Single(resultat).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_titre_numerique_reste_une_recherche_de_mots()
|
||||
{
|
||||
// ⚠️ Sans le seuil de huit chiffres, « 1984 » remonterait tous les livres dont l'ISBN
|
||||
// contient ces quatre chiffres — ici Le Petit Prince, dont l'ISBN se termine par 1275 8.
|
||||
Assert.Empty(FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "0612" }));
|
||||
Assert.Empty(FiltreLivresLocal.Appliquer(Instantane, new CritereLivres { Recherche = "2070612" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_instantane_vide_ne_fait_pas_echouer_la_recherche()
|
||||
=> Assert.Empty(FiltreLivresLocal.Appliquer([], new CritereLivres { Recherche = "zola" }));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using MaBibli.Client.Composants;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Le compte annoncé par un bloc de série au catalogue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Défaut d'origine : le compte suivait le filtre. « 3 tomes » devenait « 1 tome » dès qu'une
|
||||
/// recherche écartait les autres, et se lisait comme « cette série n'a qu'un tome ». Le nombre
|
||||
/// ne disait pas de quoi il parlait.
|
||||
/// </remarks>
|
||||
public class LibellesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Sans_filtre_et_serie_complete_un_seul_nombre_suffit()
|
||||
=> Assert.Equal("3 tomes", Libelles.CompteBloc(affiches: 3, possedes: 3, total: 3));
|
||||
|
||||
[Fact]
|
||||
public void Sans_filtre_la_serie_incomplete_dit_ce_qui_manque()
|
||||
=> Assert.Equal("3 sur 7 tomes", Libelles.CompteBloc(affiches: 3, possedes: 3, total: 7));
|
||||
|
||||
[Fact]
|
||||
public void Sous_filtre_une_serie_complete_distingue_l_affiche_du_possede()
|
||||
=> Assert.Equal("1 affiché sur 3 tomes", Libelles.CompteBloc(affiches: 1, possedes: 3, total: 3));
|
||||
|
||||
[Fact]
|
||||
public void Sous_filtre_une_serie_incomplete_porte_les_trois_nombres()
|
||||
=> Assert.Equal("1 affiché · 3 sur 7 tomes", Libelles.CompteBloc(affiches: 1, possedes: 3, total: 7));
|
||||
|
||||
[Fact]
|
||||
public void Le_pluriel_suit_le_total_pas_l_affiche()
|
||||
{
|
||||
Assert.Equal("1 tome", Libelles.CompteBloc(1, 1, 1));
|
||||
Assert.Equal("2 affichés sur 5 tomes", Libelles.CompteBloc(2, 5, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using MaBibli.Shared.Catalogue;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// La file de catalogage en rafale : découpe de la saisie, et comptes du compte rendu.
|
||||
/// </summary>
|
||||
public class RafaleIsbnTests
|
||||
{
|
||||
[Fact]
|
||||
public void Un_code_par_ligne()
|
||||
{
|
||||
Assert.Equal(
|
||||
["9782070612758", "9782253004226"],
|
||||
SaisieRafale.Decouper("9782070612758\n9782253004226"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_lignes_vides_et_les_retours_windows_sont_tolerés()
|
||||
{
|
||||
Assert.Equal(
|
||||
["1112223334445", "5554443332221"],
|
||||
SaisieRafale.Decouper("1112223334445\r\n\r\n5554443332221\r\n"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_tirets_et_espaces_de_la_douchette_disparaissent()
|
||||
{
|
||||
// Une douchette tape des chiffres, mais une saisie manuelle peut recopier la forme
|
||||
// imprimée : la normalisation est la même que celle d'IsbnUtils.
|
||||
Assert.Equal(["9782070612758"], SaisieRafale.Decouper("978-2-07-061275-8"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_X_final_d_un_isbn10_survit()
|
||||
{
|
||||
Assert.Equal(["222403313X"], SaisieRafale.Decouper("222403313x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_code_scanne_deux_fois_n_entre_qu_une_fois()
|
||||
{
|
||||
// ⚠️ Rescanner par mégarde est le geste le plus banal d'une rafale. Interroger la BnF
|
||||
// deux fois pour le même code ne peut rien apporter.
|
||||
Assert.Equal(
|
||||
["9782070612758", "9782253004226"],
|
||||
SaisieRafale.Decouper("9782070612758\n9782253004226\n9782070612758"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void L_ordre_de_saisie_est_conserve()
|
||||
{
|
||||
// On repasse sur la pile dans l'ordre où on l'a scannée : c'est ce qui permet de
|
||||
// retrouver le livre suivant sur la table.
|
||||
Assert.Equal(["333", "111", "222"], SaisieRafale.Decouper("333\n111\n222"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Une_saisie_vide_ne_donne_aucun_code()
|
||||
{
|
||||
Assert.Empty(SaisieRafale.Decouper(null));
|
||||
Assert.Empty(SaisieRafale.Decouper(" \n\n "));
|
||||
}
|
||||
|
||||
private static Rafale Rafale(params EtatCodeRafale[] etats) => new()
|
||||
{
|
||||
Codes = [.. etats.Select((e, i) => new CodeRafale { Code = $"code{i}", Etat = e })],
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Le_pourcentage_ne_compte_que_ce_qui_est_traite()
|
||||
{
|
||||
var rafale = Rafale(
|
||||
EtatCodeRafale.Cree, EtatCodeRafale.Passe,
|
||||
EtatCodeRafale.ATraiter, EtatCodeRafale.ATraiter);
|
||||
|
||||
Assert.Equal(50, rafale.Pourcentage);
|
||||
Assert.True(rafale.ResteATraiter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Une_rafale_vide_ne_divise_pas_par_zero()
|
||||
{
|
||||
Assert.Equal(0, new Rafale().Pourcentage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_residus_sont_ce_qui_attend_une_decision()
|
||||
{
|
||||
var rafale = Rafale(
|
||||
EtatCodeRafale.Cree, EtatCodeRafale.Passe, EtatCodeRafale.AChoisir,
|
||||
EtatCodeRafale.Introuvable, EtatCodeRafale.Revue, EtatCodeRafale.Echec);
|
||||
|
||||
// ⚠️ Ni les créés ni les passés : ils sont terminés, et les rappeler ferait croire
|
||||
// qu'il reste du travail dessus.
|
||||
Assert.Equal(4, rafale.Residus.Count);
|
||||
Assert.All(rafale.Residus, c => Assert.False(c.Termine));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_passes_se_comptent_a_part_des_crees()
|
||||
{
|
||||
// ⚠️ C'est ce qui empêche de croire qu'on a ajouté cinquante livres quand on en a
|
||||
// ajouté quarante-deux : posséder deux exemplaires est légitime, passer en silence non.
|
||||
var rafale = Rafale(
|
||||
EtatCodeRafale.Cree, EtatCodeRafale.Cree, EtatCodeRafale.Passe);
|
||||
|
||||
Assert.Equal(2, rafale.Crees);
|
||||
Assert.Equal(1, rafale.Passes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Passer_les_doublons_est_coche_par_defaut()
|
||||
{
|
||||
Assert.True(new Rafale().PasserLesDoublons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_code_cree_ou_passe_est_termine_les_autres_non()
|
||||
{
|
||||
Assert.True(new CodeRafale { Code = "x", Etat = EtatCodeRafale.Cree }.Termine);
|
||||
Assert.True(new CodeRafale { Code = "x", Etat = EtatCodeRafale.Passe }.Termine);
|
||||
|
||||
foreach (var etat in new[]
|
||||
{
|
||||
EtatCodeRafale.ATraiter, EtatCodeRafale.AChoisir,
|
||||
EtatCodeRafale.Introuvable, EtatCodeRafale.Revue, EtatCodeRafale.Echec,
|
||||
})
|
||||
{
|
||||
Assert.False(new CodeRafale { Code = "x", Etat = etat }.Termine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using MaBibli.Shared.Isbn;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Reconnaissance d'un code dans le champ de recherche du catalogue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// L'enjeu n'est pas de trouver un ISBN — c'est de <b>ne pas</b> en voir un là où il n'y en a
|
||||
/// pas : « 1984 » est un titre, et le confondre avec un fragment de code remplacerait une
|
||||
/// réponse par du bruit.
|
||||
/// </remarks>
|
||||
public class RechercheIsbnTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Germinal")]
|
||||
[InlineData("Zola 1984")]
|
||||
[InlineData("1984")]
|
||||
[InlineData("2026")]
|
||||
[InlineData("1234567")] // sept chiffres : sous le seuil
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Ce_qui_n_est_pas_un_code_reste_une_recherche_de_mots(string? terme)
|
||||
=> Assert.Null(RechercheIsbn.Formes(terme));
|
||||
|
||||
[Fact]
|
||||
public void Un_ean13_est_cherche_sous_ses_deux_formes()
|
||||
{
|
||||
// ⚠️ Le cœur de la fonctionnalité : la douchette lit toujours un 13, alors qu'un livre
|
||||
// paru avant 2007 est rangé sous son 10.
|
||||
var formes = RechercheIsbn.Formes("9782253004226");
|
||||
|
||||
Assert.NotNull(formes);
|
||||
Assert.Equal(["9782253004226", "2253004227"], formes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_tirets_de_l_affichage_se_recollent()
|
||||
=> Assert.Equal(
|
||||
RechercheIsbn.Formes("9782253004226"),
|
||||
RechercheIsbn.Formes("978-2-25-300422-6"));
|
||||
|
||||
[Fact]
|
||||
public void Un_fragment_assez_long_reste_un_code_mais_sans_seconde_forme()
|
||||
{
|
||||
// La conversion 13 → 10 n'a pas de sens sur un fragment : elle exige un ISBN complet.
|
||||
var formes = RechercheIsbn.Formes("97822530");
|
||||
|
||||
Assert.Equal(["97822530"], formes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_isbn_10_terminant_par_X_est_reconnu()
|
||||
{
|
||||
var formes = RechercheIsbn.Formes("2-266-11156-X");
|
||||
|
||||
Assert.Equal(["226611156X"], formes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("9782253004226", true)]
|
||||
[InlineData("978-2-25-300422-6", true)]
|
||||
[InlineData("2253004227", true)]
|
||||
[InlineData("9782253004227", false)] // clé fausse : ce n'est pas une édition, c'est une faute
|
||||
[InlineData("97822530", false)] // fragment : le résultat unique serait un hasard
|
||||
[InlineData("Germinal", false)]
|
||||
public void Seul_un_code_complet_et_valide_autorise_l_ouverture_directe(string terme, bool attendu)
|
||||
=> Assert.Equal(attendu, RechercheIsbn.EstCodeComplet(terme));
|
||||
}
|
||||
@@ -27,6 +27,9 @@ public class RemonteeRoutesTests
|
||||
[InlineData("/ajout", "/")]
|
||||
[InlineData("/ajout/isbn", "/")]
|
||||
[InlineData("/ajout/manuel", "/")]
|
||||
// ⚠️ La rafale remonte à « /ajout », pas au catalogue : on veut souvent y revenir pour
|
||||
// saisir autrement ce qui a résisté au traitement.
|
||||
[InlineData("/ajout/rafale", "/ajout")]
|
||||
// Revues
|
||||
[InlineData("/revues/ajout", "/revues")]
|
||||
[InlineData("/revues/12/edition", "/revues/12")]
|
||||
|
||||
@@ -201,6 +201,38 @@ public class ServicePretsTests : IDisposable
|
||||
Assert.True((await _prets.RendreAsync(999, new RetourPret())).EstIntrouvable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Supprimer_efface_un_pret_clos_de_l_historique()
|
||||
{
|
||||
// ⚠️ C'est l'inverse exact de « rendre » : on défait une SAISIE (mauvais emprunteur,
|
||||
// mauvaise date, prêt noté deux fois), on ne clôt pas un prêt réel.
|
||||
var livreId = await LivreAsync();
|
||||
var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
|
||||
await _prets.RendreAsync(pret.Id, new RetourPret());
|
||||
|
||||
Assert.True(await _prets.SupprimerAsync(pret.Id));
|
||||
|
||||
Assert.Empty(await _prets.HistoriqueAsync(livreId) ?? []);
|
||||
Assert.Empty(await _db.Prets.ToListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Supprimer_le_pret_en_cours_remet_le_livre_a_la_maison()
|
||||
{
|
||||
var livreId = await LivreAsync();
|
||||
var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!;
|
||||
|
||||
Assert.True(await _prets.SupprimerAsync(pret.Id));
|
||||
|
||||
// L'index unique partiel ne bloque plus : la place est libre, le livre peut repartir.
|
||||
Assert.Empty(await _prets.EnCoursAsync());
|
||||
Assert.Null((await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" })).Erreur);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Supprimer_un_pret_inconnu_ne_fait_rien()
|
||||
=> Assert.False(await _prets.SupprimerAsync(999));
|
||||
|
||||
[Fact]
|
||||
public async Task Rendre_refuse_une_date_anterieure_au_pret()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Le tri des numéros de tome — celui qui range « 2 » avant « 10 ».
|
||||
/// </summary>
|
||||
public class TriNaturelTests
|
||||
{
|
||||
private static List<string?> Trier(params string?[] valeurs) =>
|
||||
valeurs.OrderBy(v => v, TriNaturel.Comparateur).ToList();
|
||||
|
||||
[Fact]
|
||||
public void Les_nombres_se_lisent_comme_des_nombres()
|
||||
{
|
||||
// Le défaut que ce comparateur existe pour corriger : un tri ordinal range
|
||||
// 1, 10, 11, 2, 9 — parce que le caractère « 1 » précède le « 2 ».
|
||||
Assert.Equal(
|
||||
["1", "2", "9", "10", "11"],
|
||||
Trier("11", "2", "10", "1", "9"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_trois_blocs_sont_dans_l_ordre_decide()
|
||||
{
|
||||
// Tranché avec l'utilisateur : vide, puis non-numériques a→z, puis chiffres 0→9.
|
||||
Assert.Equal(
|
||||
[null, "", "Hors-série", "Intégrale 1-3", "1", "2", "10"],
|
||||
Trier("10", "Intégrale 1-3", "2", null, "Hors-série", "1", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_numero_decimal_se_range_entre_ses_voisins()
|
||||
{
|
||||
Assert.Equal(["3", "3.5", "4"], Trier("4", "3.5", "3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_prefixe_passe_avant_ce_qui_le_prolonge()
|
||||
{
|
||||
Assert.Equal(["3", "3 bis"], Trier("3 bis", "3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_zeros_de_tete_ne_changent_pas_l_ordre()
|
||||
{
|
||||
Assert.Equal(0, TriNaturel.Comparer("007", "7"));
|
||||
Assert.Equal(["007", "8"], Trier("8", "007"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_tres_grand_nombre_ne_fait_pas_echouer_le_tri()
|
||||
{
|
||||
// ⚠️ Le comparateur ne convertit jamais en int : rien ne borne un numéro saisi
|
||||
// à la main, et un dépassement serait une exception au milieu d'un affichage.
|
||||
var enorme = new string('9', 40);
|
||||
Assert.True(TriNaturel.Comparer("12", enorme) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_tri_ignore_la_casse_et_les_accents()
|
||||
{
|
||||
// Même normalisation que partout ailleurs : « Édition » et « edition » se rencontrent.
|
||||
Assert.Equal(0, TriNaturel.Comparer("Hors-Série", "hors-serie"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_texte_se_compare_de_a_a_z()
|
||||
{
|
||||
Assert.Equal(
|
||||
["Alpha", "Hors-série", "Zeta"],
|
||||
Trier("Zeta", "Alpha", "Hors-série"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_tri_est_stable_et_symetrique()
|
||||
{
|
||||
// Un comparateur incohérent fait lever OrderBy sur de gros jeux : on vérifie que
|
||||
// comparer(a,b) et comparer(b,a) sont bien opposés sur toutes les paires.
|
||||
string?[] valeurs = [null, "", "1", "2", "10", "3.5", "3 bis", "Hors-série", "007", "Zeta"];
|
||||
|
||||
foreach (var a in valeurs)
|
||||
{
|
||||
foreach (var b in valeurs)
|
||||
{
|
||||
Assert.Equal(
|
||||
Math.Sign(TriNaturel.Comparer(a, b)),
|
||||
-Math.Sign(TriNaturel.Comparer(b, a)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user