Signaler les envies déjà entrées au catalogue, sans les supprimer
La demande initiale était de retirer l'envie ; c'est refusé. Le catalogue est commun et la liste d'envies personnelle : supprimer modifierait la liste d'un autre en silence, avec sa note. Et le rapprochement par clé d'œuvre est faillible, alors qu'une suppression ne se rattrape pas. L'étiquette « Déjà au catalogue » mène à la fiche, pour vérifier avant de retirer. Le critère est celui des doublons, la convention d'auteur commun étant désormais partagée par les deux (RapprochementAuteurs). Au passage : accolade manquante sur .etiquette-souhaite, qui avalait la règle suivante — le bandeau de mise à jour perdait son style. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1049,6 +1049,50 @@ ISBN est connu, y compris pour les résultats d'une recherche par titre (l'ISBN
|
||||
vignette retombe sur son substitut à initiale. Décidé de **ne pas** offrir de champ « URL de
|
||||
couverture » : il inviterait à coller des liens morts pour un gain nul.
|
||||
|
||||
## Une envie déjà au catalogue est signalée, jamais supprimée (2026-08-19)
|
||||
|
||||
Demande d'origine : quand un livre souhaité entre au catalogue, retirer l'envie correspondante.
|
||||
**Décidé : on signale.** `SouhaitDto.Possede` / `LivreId` portent le rapprochement, et l'écran
|
||||
des envies affiche une étiquette « Déjà au catalogue » **cliquable vers la fiche**.
|
||||
|
||||
Deux raisons, dont la première est structurelle :
|
||||
|
||||
- **le catalogue est commun, la liste d'envies personnelle.** L'envie n'est pas forcément celle
|
||||
de la personne qui a saisi le livre : la supprimer modifierait la liste d'un autre en silence,
|
||||
et lui ferait perdre sa note (« demandé à Noël »). L'API ne sait pas — délibérément — écrire
|
||||
dans la liste d'autrui, et lui ouvrir cette porte percerait un invariant tenu partout ailleurs ;
|
||||
- **le rapprochement est faillible** (`CleOeuvre` : titre retraduit, tome, intégrale). Une
|
||||
suppression fondée sur un rapprochement faux est irréversible ; un signalement se corrige d'un
|
||||
coup d'œil — d'où l'étiquette qui **mène à la fiche**, pour vérifier avant de retirer.
|
||||
|
||||
C'est la même règle que le grisage de la bibliographie, dans l'autre sens : on **marque**, on ne
|
||||
masque ni ne supprime jamais une ligne.
|
||||
|
||||
### Le critère est celui des doublons, et c'est voulu
|
||||
|
||||
ISBN identique (il tranche seul, désignant une édition précise), sinon même clé d'œuvre **et**
|
||||
un auteur commun. La convention « deux jeux de noms tous deux vides concordent » vit désormais
|
||||
en un seul endroit, `RapprochementAuteurs.PartagentUnAuteur`, partagée avec la détection de
|
||||
doublons du catalogue — deux règles voisines qui divergeraient en silence seraient pires qu'une
|
||||
règle imparfaite.
|
||||
|
||||
⚠️ `LivreSouhaite.TitreNormalise` **est déjà** la clé d'œuvre (voir l'entité) : rien à
|
||||
recalculer de ce côté, contrairement au catalogue dont `TitreNormalise` garde le titre entier.
|
||||
|
||||
⚠️ **Le catalogue est chargé en entier** (projection minimale) pour ce rapprochement. Assumé à
|
||||
l'échelle d'un foyer, et cohérent avec l'instantané hors-ligne qui l'emporte déjà tout entier
|
||||
dans le navigateur. Le jour où la bibliothèque compterait des milliers de fiches, c'est là qu'il
|
||||
faudrait dégrossir en SQL — pas renoncer au signalement.
|
||||
|
||||
⚠️ Piège rencontré, et pris par les tests seuls : `GetValueOrDefault` sur un
|
||||
`Dictionary<int, int>` rend **zéro**, pas `null`. Toute envie se disait donc possédée, en
|
||||
pointant vers un livre inexistant. Le compilateur ne pouvait rien dire — `0` est un `int`
|
||||
parfaitement valide.
|
||||
|
||||
**Ce qui n'est délibérément pas fait** : les exports `.txt` et `.csv` ne portent pas la mention.
|
||||
Ce sont des instantanés qu'on emporte, alors que le signalement appelle une action — vérifier,
|
||||
puis retirer — qui a lieu dans l'application.
|
||||
|
||||
## Bibliographie par auteur — SRU BnF, validé le 2026-08-18
|
||||
|
||||
Le déclencheur décrit dans IDEES.md : depuis un auteur déjà présent, voir tout ce qu'il a écrit,
|
||||
|
||||
@@ -80,24 +80,6 @@ rendu en canvas, pas sur une image de caméra.
|
||||
|
||||
# Retours d'usage du 2026-08-19 (3ᵉ série)
|
||||
|
||||
## À l'ajout d'un livre, signaler l'envie correspondante
|
||||
|
||||
**Décidé le 2026-08-19 : on ne supprime rien, on signale.** La liste d'envies marque l'entrée
|
||||
« déjà au catalogue ».
|
||||
|
||||
Pourquoi pas la suppression, qui était la demande initiale :
|
||||
|
||||
- le catalogue est **commun**, la liste d'envies **personnelle**. Supprimer l'envie d'un autre
|
||||
modifierait sa liste en silence et lui ferait perdre sa note (« demandé à Noël ») ; l'API ne
|
||||
sait pas — délibérément — écrire dans la liste d'autrui, et l'ouvrir serait une brèche dans un
|
||||
invariant tenu partout ailleurs ;
|
||||
- le rapprochement se fait par `CleOeuvre` + auteur, donc **faillible** (titre retraduit, tome,
|
||||
intégrale — voir `CLAUDE.md`). Une suppression fondée sur un rapprochement faux est
|
||||
irréversible ; un signalement se corrige d'un coup d'œil.
|
||||
|
||||
Reste à implémenter : le marquage dans `SouhaitDto`, calculé comme l'est déjà `Possede` dans la
|
||||
bibliographie, et son rendu sur l'écran des envies.
|
||||
|
||||
## Bandes dessinées et magazines
|
||||
|
||||
Deux demandes distinctes, de coût très différent.
|
||||
|
||||
@@ -285,27 +285,9 @@ public sealed class ServiceCatalogue(MaBibliDbContext db, IServiceAuteurs auteur
|
||||
private IQueryable<Livre> AvecAuteurs() =>
|
||||
db.Livres.AsNoTracking().Include(l => l.Auteurs).ThenInclude(la => la.Auteur);
|
||||
|
||||
/// <summary>
|
||||
/// Un des auteurs du livre catalogué est-il l'un de ceux qu'on est en train de saisir ?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deux fiches sans aucun auteur comptent comme concordantes : le titre est alors la seule
|
||||
/// information disponible, et refuser de le signaler reviendrait à ne rien dire du tout.
|
||||
/// </remarks>
|
||||
private static bool MemeAuteur(Livre livre, IReadOnlyList<string> saisis)
|
||||
{
|
||||
var connus = livre.Auteurs
|
||||
.Select(la => la.Auteur?.Nom)
|
||||
.Where(nom => !string.IsNullOrWhiteSpace(nom))
|
||||
.ToList();
|
||||
|
||||
if (connus.Count == 0 && saisis.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return connus.Any(connu => saisis.Any(saisi => RapprochementAuteurs.SontLeMemeAuteur(connu, saisi)));
|
||||
}
|
||||
/// <summary>Un des auteurs du livre catalogué est-il l'un de ceux qu'on est en train de saisir ?</summary>
|
||||
private static bool MemeAuteur(Livre livre, IReadOnlyList<string> saisis) =>
|
||||
RapprochementAuteurs.PartagentUnAuteur(livre.Auteurs.Select(la => la.Auteur?.Nom), saisis);
|
||||
|
||||
/// <summary>Phrase d'avertissement, écrite pour être lue telle quelle par l'utilisateur.</summary>
|
||||
/// <remarks>
|
||||
|
||||
@@ -72,7 +72,9 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
.ThenBy(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return souhaits.Select(Projeter).ToList();
|
||||
var possedes = await CorrespondancesAsync(souhaits, ct);
|
||||
|
||||
return souhaits.Select(s => Projeter(s, Correspondant(possedes, s.Id))).ToList();
|
||||
}
|
||||
|
||||
public async Task<ResultatSouhait> AjouterAsync(
|
||||
@@ -146,7 +148,9 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
db.LivresSouhaites.Add(souhait);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return ResultatSouhait.Ok(Projeter(souhait));
|
||||
var possede = await CorrespondancesAsync([souhait], ct);
|
||||
|
||||
return ResultatSouhait.Ok(Projeter(souhait, Correspondant(possede, souhait.Id)));
|
||||
}
|
||||
|
||||
public async Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default)
|
||||
@@ -220,6 +224,97 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pour chaque envie, le livre du catalogue qui lui correspond — quand il y en a un.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Ce qui se croise ici, ce sont deux portées différentes</b> : la liste d'envies est
|
||||
/// personnelle, le catalogue est commun. Le signalement dit donc « ce livre est <i>dans la
|
||||
/// maison</i> », pas « vous l'avez acheté » — et c'est bien l'information utile avant
|
||||
/// d'acheter à nouveau.
|
||||
/// <para>
|
||||
/// Deux critères, comme pour les doublons du catalogue : un ISBN identique tranche seul (il
|
||||
/// désigne une édition précise), sinon il faut la même clé d'œuvre <b>et</b> un auteur
|
||||
/// commun. Le titre seul confondrait deux recueils homonymes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ⚠️ Le catalogue est chargé en entier, en projection minimale. C'est assumé à l'échelle
|
||||
/// d'un foyer et cohérent avec l'instantané hors-ligne, qui l'emporte déjà tout entier dans
|
||||
/// le navigateur. Le jour où la bibliothèque compterait des milliers de fiches, c'est ici
|
||||
/// qu'il faudrait dégrossir en SQL — pas renoncer au signalement.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private async Task<Dictionary<int, int>> CorrespondancesAsync(
|
||||
IReadOnlyList<LivreSouhaite> souhaits, CancellationToken ct)
|
||||
{
|
||||
if (souhaits.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var livres = await db.Livres
|
||||
.AsNoTracking()
|
||||
.Select(l => new
|
||||
{
|
||||
l.Id,
|
||||
l.Titre,
|
||||
l.Isbn,
|
||||
Auteurs = l.Auteurs.OrderBy(la => la.Position).Select(la => la.Auteur!.Nom).ToList(),
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
var parCle = livres
|
||||
.Select(l => (Cle: CleOeuvre.Cle(l.Titre), Livre: l))
|
||||
.Where(x => x.Cle.Length > 0)
|
||||
.GroupBy(x => x.Cle)
|
||||
.ToDictionary(g => g.Key, g => g.Select(x => x.Livre).ToList());
|
||||
|
||||
var parIsbn = new Dictionary<string, int>();
|
||||
foreach (var livre in livres.Where(l => !string.IsNullOrWhiteSpace(l.Isbn)))
|
||||
{
|
||||
parIsbn.TryAdd(livre.Isbn!, livre.Id);
|
||||
}
|
||||
|
||||
var correspondances = new Dictionary<int, int>();
|
||||
|
||||
foreach (var souhait in souhaits)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(souhait.Isbn) && parIsbn.TryGetValue(souhait.Isbn, out var idIsbn))
|
||||
{
|
||||
correspondances[souhait.Id] = idIsbn;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TitreNormalise EST la clé d'œuvre pour une envie (voir LivreSouhaite) : rien à
|
||||
// recalculer de ce côté-là.
|
||||
if (!parCle.TryGetValue(souhait.TitreNormalise, out var homonymes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trouve = homonymes.FirstOrDefault(
|
||||
l => RapprochementAuteurs.PartagentUnAuteur(l.Auteurs, [souhait.Auteur]));
|
||||
|
||||
if (trouve is not null)
|
||||
{
|
||||
correspondances[souhait.Id] = trouve.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return correspondances;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifiant du livre correspondant, ou <c>null</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Surtout pas <c>GetValueOrDefault</c> : sur un dictionnaire de <c>int</c>, l'absence
|
||||
/// rend <b>zéro</b>, pas <c>null</c> — et toute envie se serait dite possédée. Le défaut a
|
||||
/// été pris par les tests, pas par le compilateur.
|
||||
/// </remarks>
|
||||
private static int? Correspondant(IReadOnlyDictionary<int, int> correspondances, int souhaitId) =>
|
||||
correspondances.TryGetValue(souhaitId, out var livreId) ? livreId : null;
|
||||
|
||||
private static string? Vide(string? valeur) =>
|
||||
string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim();
|
||||
|
||||
@@ -227,7 +322,7 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
/// Projection vers le DTO. <b>Sans le champ <c>Utilisateur</c></b> : il ne sort jamais de
|
||||
/// l'API, pour qu'aucun client ne puisse croire qu'il désigne de qui il parle.
|
||||
/// </summary>
|
||||
internal static SouhaitDto Projeter(LivreSouhaite souhait) => new()
|
||||
internal static SouhaitDto Projeter(LivreSouhaite souhait, int? livreId = null) => new()
|
||||
{
|
||||
Id = souhait.Id,
|
||||
Titre = souhait.Titre,
|
||||
@@ -238,5 +333,7 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
||||
CoverUrl = souhait.CoverUrl,
|
||||
Note = souhait.Note,
|
||||
DateAjout = souhait.DateAjout,
|
||||
Possede = livreId is not null,
|
||||
LivreId = livreId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,6 +147,21 @@ else
|
||||
}
|
||||
|
||||
<p class="carte-details">
|
||||
@*
|
||||
⚠️ On SIGNALE, on ne supprime pas : la liste d'envies est
|
||||
personnelle et le catalogue est commun, donc l'envie n'est
|
||||
peut-être pas de celui qui a ajouté le livre — et la note
|
||||
(« demandé à Noël ») disparaîtrait avec elle. Le rapprochement
|
||||
est en outre faillible : l'étiquette mène à la fiche pour qu'on
|
||||
puisse vérifier avant de retirer quoi que ce soit.
|
||||
*@
|
||||
@if (souhait is { Possede: true, LivreId: { } livreId })
|
||||
{
|
||||
<a class="etiquette etiquette-possede" href="@($"livres/{livreId}")">
|
||||
Déjà au catalogue
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(souhait.Editeur))
|
||||
{
|
||||
<span>@souhait.Editeur</span>
|
||||
|
||||
@@ -839,6 +839,8 @@ body {
|
||||
background: #e6e6f5;
|
||||
color: #3b3b7a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --- Mise à jour de l'application --- */
|
||||
|
||||
/* Injecté par js/mise-a-jour.js, hors composants Blazor : ce bandeau doit pouvoir s'afficher
|
||||
|
||||
@@ -29,6 +29,21 @@ public record SouhaitDto
|
||||
public string? Note { get; init; }
|
||||
|
||||
public required DateTime DateAjout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vrai si un livre du catalogue correspond à cette envie.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>C'est un signalement, jamais une suppression.</b> Le catalogue est commun, la liste
|
||||
/// d'envies personnelle : retirer l'envie de quelqu'un d'autre modifierait sa liste en
|
||||
/// silence et lui ferait perdre sa note (« demandé à Noël »). Le rapprochement est en outre
|
||||
/// faillible (voir <c>CleOeuvre</c>) — un signalement erroné s'ignore d'un coup d'œil, une
|
||||
/// suppression erronée ne se rattrape pas.
|
||||
/// </remarks>
|
||||
public bool Possede { get; init; }
|
||||
|
||||
/// <summary>Le livre du catalogue en cause, s'il y en a un : de quoi ouvrir sa fiche.</summary>
|
||||
public int? LivreId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Charge utile d'ajout d'une envie.</summary>
|
||||
|
||||
@@ -90,6 +90,33 @@ public static class RapprochementAuteurs
|
||||
public static bool SontLeMemeAuteur(string? a, string? b) =>
|
||||
Comparer(a, b) == NiveauRapprochement.Certain;
|
||||
|
||||
/// <summary>
|
||||
/// Deux jeux de noms ont-ils au moins un auteur en commun ?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sert partout où l'on rapproche deux fiches par leur titre : le titre seul ne suffit
|
||||
/// jamais — deux recueils « Nouvelles » sans rapport se confondraient — mais un auteur
|
||||
/// commun tranche.
|
||||
/// <para>
|
||||
/// <b>Convention</b> : deux jeux <b>tous deux vides</b> concordent. Le titre est alors la
|
||||
/// seule information disponible des deux côtés, et exiger un auteur reviendrait à ne jamais
|
||||
/// rien rapprocher. Un jeu vide face à un jeu renseigné, en revanche, ne concorde pas :
|
||||
/// l'information manque d'un seul côté, ce qui n'est pas une raison de conclure.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static bool PartagentUnAuteur(IEnumerable<string?> a, IEnumerable<string?> b)
|
||||
{
|
||||
var gauche = a.Where(n => !string.IsNullOrWhiteSpace(n)).ToList();
|
||||
var droite = b.Where(n => !string.IsNullOrWhiteSpace(n)).ToList();
|
||||
|
||||
if (gauche.Count == 0 && droite.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return gauche.Any(g => droite.Any(d => SontLeMemeAuteur(g, d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cherche à placer chaque mot de <paramref name="aPlacer"/> sur un mot distinct de
|
||||
/// <paramref name="cible"/>, par égalité ou par initiale.
|
||||
|
||||
@@ -52,6 +52,101 @@ public class ServiceSouhaitsTests : IDisposable
|
||||
new EnregistrementSouhait { Titre = titre, Auteur = auteur, Isbn = isbn, Note = note },
|
||||
utilisateur);
|
||||
|
||||
// ── « Déjà au catalogue » : on signale, on ne supprime jamais ────────────
|
||||
//
|
||||
// Le catalogue est COMMUN, la liste d'envies PERSONNELLE : c'est ce croisement de portées
|
||||
// que ces tests protègent. Retirer l'envie d'un autre, comme le demandait la version
|
||||
// initiale de l'idée, modifierait sa liste en silence.
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_deja_au_catalogue_est_signalee_avec_sa_fiche()
|
||||
{
|
||||
var livre = (await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu)).Livre!;
|
||||
|
||||
// Sous-titre et ordre du nom différents : ce que la clé d'œuvre et le rapprochement
|
||||
// d'auteurs doivent absorber.
|
||||
await AjouterAsync("Les fourmis : roman", "Werber, Bernard");
|
||||
|
||||
var souhait = (await _souhaits.ListerAsync(Mathieu)).Single();
|
||||
|
||||
Assert.True(souhait.Possede);
|
||||
Assert.Equal(livre.Id, souhait.LivreId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_signalee_reste_dans_la_liste_avec_sa_note()
|
||||
{
|
||||
await AjouterAsync("Les fourmis", note: "demandé à Noël");
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Camille);
|
||||
|
||||
var souhait = (await _souhaits.ListerAsync(Mathieu)).Single();
|
||||
|
||||
// L'ajout au catalogue par QUELQU'UN D'AUTRE ne touche pas à la liste de Mathieu.
|
||||
Assert.True(souhait.Possede);
|
||||
Assert.Equal("demandé à Noël", souhait.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_titre_identique_dun_autre_auteur_nest_pas_signale()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Nouvelles", Auteur = "Guy de Maupassant" }, Mathieu);
|
||||
|
||||
await AjouterAsync("Nouvelles", "Anton Tchekhov");
|
||||
|
||||
Assert.False((await _souhaits.ListerAsync(Mathieu)).Single().Possede);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Le_meme_isbn_suffit_a_signaler_malgre_un_titre_different()
|
||||
{
|
||||
// L'ISBN désigne une édition précise : il tranche seul, et rattrape un titre saisi
|
||||
// autrement qu'à la source.
|
||||
var livre = (await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre
|
||||
{
|
||||
Titre = "Le Horla et autres contes",
|
||||
Auteur = "Guy de Maupassant",
|
||||
Isbn = "9782080704092",
|
||||
},
|
||||
Mathieu)).Livre!;
|
||||
|
||||
await AjouterAsync("Le Horla", "Maupassant", isbn: "9782080704092");
|
||||
|
||||
var souhait = (await _souhaits.ListerAsync(Mathieu)).Single();
|
||||
|
||||
Assert.Equal(livre.Id, souhait.LivreId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Une_envie_sans_equivalent_nest_pas_signalee()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
await AjouterAsync("La prophétie des abeilles");
|
||||
|
||||
var souhait = (await _souhaits.ListerAsync(Mathieu)).Single();
|
||||
|
||||
Assert.False(souhait.Possede);
|
||||
Assert.Null(souhait.LivreId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Le_signalement_apparait_des_lajout_de_lenvie()
|
||||
{
|
||||
await _catalogue.CreerAsync(
|
||||
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
|
||||
|
||||
var ajout = await AjouterAsync("Les fourmis");
|
||||
|
||||
// Sinon l'écran d'ajout renverrait une fiche muette, et il faudrait recharger la liste
|
||||
// pour apprendre qu'on possède déjà le livre.
|
||||
Assert.True(ajout.Souhait!.Possede);
|
||||
}
|
||||
|
||||
// ── Ordre choisi à la main ───────────────────────────────────────────────
|
||||
|
||||
private async Task<IReadOnlyList<string>> TitresAsync(string utilisateur = Mathieu) =>
|
||||
|
||||
Reference in New Issue
Block a user