La fiche d'une revue portait tout à la fois : ses numéros, le formulaire d'ajout, les champs de la revue et sa suppression. Elle prend la forme déjà tenue par la fiche d'un livre et par celle d'une série — une route pour regarder, une route pour changer. « Ajouter un numéro » et « Modifier la revue » remontent à droite du titre, dans le bloc d'en-tête partagé avec la fiche d'une série : les deux actions sont visibles sans descendre au bas d'une collection de trente numéros. ⚠️ Modifier et retirer un numéro n'existent plus QUE dans /revues/{id}/edition. En consultation, ces deux boutons se déclenchaient sous le pouce en faisant défiler la liste — même raison que les flèches d'ordre d'une série, reléguées sur leur propre écran. ⚠️ Ajouter un numéro, lui, RESTE en consultation, et ce n'est pas une entorse : le geste ne touche pas à la fiche de la revue, il range un objet de plus, comme noter un prêt depuis la fiche d'un livre. Le formulaire se replie derrière son bouton, et s'ouvre de lui-même quand on arrive du scanner avec un numéro lu sur l'add-on EAN-2 : demander un clic de plus pour saisir ce qu'on tient en main serait un détour. Un retrait de numéro se confirme désormais, comme la suppression d'un livre ou d'une revue : il ne se défait pas, et la liste s'égrène sous le pouce. ⚠️ Les deux routes partagent le paramètre {id} : le routeur ne redessine rien en passant de l'une à l'autre, d'où l'abonnement à LocationChanged — piège déjà rencontré sur la fiche livre. Quitter l'édition referme au passage ce qui n'a de sens que là : formulaire de numéro ouvert, retrait ou suppression en attente de confirmation. /revues/{id}/edition entre dans la table de RemonteeRoutes : elle retombait jusqu'ici sur le repli de branche, donc sur la liste des revues au lieu de la fiche qu'on venait de quitter. Deux tests de service viennent avec, sur ce dont l'écran dépend sans le recalculer : la revue rendue par la modification d'un numéro revient déjà rangée (corriger une parution déplace le numéro, les numéros sans date fermant toujours la liste), et un retrait ne touche qu'au numéro visé. 605 tests au vert. Pas de vérification en navigateur : le rendu repose sur la relecture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
3.9 KiB
C#
105 lines
3.9 KiB
C#
using MaBibli.Client.Services;
|
|
|
|
namespace MaBibli.Tests;
|
|
|
|
/// <summary>
|
|
/// La flèche de retour du bandeau remonte d'un cran de route, et ne sort jamais de
|
|
/// l'application. Ces cas sont ceux qui ont été écrits noir sur blanc avant de coder.
|
|
/// </summary>
|
|
public class RemonteeRoutesTests
|
|
{
|
|
[Theory]
|
|
// Fiche livre
|
|
[InlineData("/livres/3/edition", "/livres/3")]
|
|
[InlineData("/livres/3", "/")]
|
|
// Séries
|
|
[InlineData("/series/7/edition", "/series/7")]
|
|
[InlineData("/series/7/ordre", "/series/7")]
|
|
[InlineData("/series/7", "/series")]
|
|
[InlineData("/series", "/")]
|
|
// Envies
|
|
[InlineData("/souhaits/5/edition", "/souhaits")]
|
|
[InlineData("/souhaits/revues/5/edition", "/souhaits")]
|
|
[InlineData("/souhaits/ordre", "/souhaits")]
|
|
[InlineData("/souhaits/ajout", "/souhaits")]
|
|
[InlineData("/souhaits", "/")]
|
|
// Ajout d'un ouvrage
|
|
[InlineData("/ajout", "/")]
|
|
[InlineData("/ajout/isbn", "/")]
|
|
[InlineData("/ajout/manuel", "/")]
|
|
// Revues
|
|
[InlineData("/revues/ajout", "/revues")]
|
|
[InlineData("/revues/12/edition", "/revues/12")]
|
|
[InlineData("/revues/12", "/revues")]
|
|
[InlineData("/revues", "/")]
|
|
// Auteurs
|
|
[InlineData("/auteurs/5/bibliographie", "/auteurs")]
|
|
[InlineData("/auteurs", "/")]
|
|
// Le reste du menu
|
|
[InlineData("/prets", "/")]
|
|
[InlineData("/", "/")]
|
|
public void Remonte_dun_cran(string chemin, string attendu) =>
|
|
Assert.Equal(attendu, RemonteeRoutes.Parent(chemin));
|
|
|
|
/// <summary>
|
|
/// « ajout » n'est pas un identifiant : sans l'ordre de lecture de la table, il serait pris
|
|
/// pour la fiche d'une revue.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Ajout_de_revue_nest_pas_une_fiche() =>
|
|
Assert.Equal("/revues", RemonteeRoutes.Parent("/revues/ajout"));
|
|
|
|
/// <summary>La requête d'un filtre ou d'un tri ne change pas la parenté.</summary>
|
|
[Theory]
|
|
[InlineData("/?auteur=3", "/")]
|
|
[InlineData("/livres/3/edition?onglet=pret", "/livres/3")]
|
|
[InlineData("/auteurs/5/bibliographie#bas", "/auteurs")]
|
|
public void La_requete_et_le_fragment_sont_ignores(string chemin, string attendu) =>
|
|
Assert.Equal(attendu, RemonteeRoutes.Parent(chemin));
|
|
|
|
/// <summary>Le chemin peut arriver relatif à la base, sans barre oblique de tête.</summary>
|
|
[Theory]
|
|
[InlineData("livres/3/edition", "/livres/3")]
|
|
[InlineData("series/7/ordre", "/series/7")]
|
|
[InlineData("", "/")]
|
|
public void Un_chemin_relatif_est_accepte(string chemin, string attendu) =>
|
|
Assert.Equal(attendu, RemonteeRoutes.Parent(chemin));
|
|
|
|
/// <summary>Une barre oblique finale ne doit pas faire manquer la ligne de table.</summary>
|
|
[Fact]
|
|
public void La_barre_finale_est_ignoree() =>
|
|
Assert.Equal("/livres/3", RemonteeRoutes.Parent("/livres/3/edition/"));
|
|
|
|
/// <summary>
|
|
/// Repli d'une route inconnue : la destination de menu de sa branche, jamais un chemin
|
|
/// deviné, et jamais rien d'externe.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("/revues/12/inconnu", "/revues")]
|
|
[InlineData("/series/7/inconnu", "/series")]
|
|
[InlineData("/inconnu/total", "/")]
|
|
[InlineData("/livres", "/")]
|
|
public void Une_route_inconnue_retombe_sur_sa_branche(string chemin, string attendu) =>
|
|
Assert.Equal(attendu, RemonteeRoutes.Parent(chemin));
|
|
|
|
/// <summary>
|
|
/// ⚠️ Le garde-fou du lot S : quoi qu'on lui donne, le retour reste dans l'application.
|
|
/// Une URL absolue ou un chemin protocolaire ne doit jamais ressortir tel quel.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("https://exemple.test/livres/3")]
|
|
[InlineData("//exemple.test/")]
|
|
[InlineData("/livres/abc/edition")]
|
|
public void Ne_sort_jamais_de_lapplication(string? chemin)
|
|
{
|
|
var parent = RemonteeRoutes.Parent(chemin);
|
|
|
|
Assert.StartsWith("/", parent);
|
|
Assert.DoesNotContain("//", parent);
|
|
Assert.DoesNotContain(":", parent);
|
|
}
|
|
}
|