Three static modals had no entry in the fallback Esc handler, so Esc over
them fell through and closed the editor behind (or did nothing when no
editor was open — reauth and the autofill picker could only be dismissed by
mouse).
Also scopes the stray-modal catch added in a95becd to [data-dynamic]. As
written it matched ANY open .modal, so an Esc over reauth / quick-search /
the autofill picker would have removed those static nodes from the DOM for
good, breaking the feature until restart. Only the runtime-built folder
modal carries the marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.5 KiB
Scénarios de test — écriture concurrente & fermeture de l'éditeur
Sessions du 2026-07-22. Tous les tests sont manuels (ils portent sur des
interactions UI que le harness node:test ne couvre pas). Rebuild obligatoire
avant de commencer : delphi-backend\assets\BuildAssets.cmd puis build Delphi.
Convention : PRÉ = état de départ, ACTION, ATTENDU. Un test qui échoue = régression, noter le commit courant.
Les 4 classes de bug
| # | Classe | Cause racine | Corrigé par |
|---|---|---|---|
| A | Écriture extérieure écrasée au Save | L'éditeur garde le payload chargé à l'ouverture et le re-poste entier ; toute écriture faite ailleurs entre-temps est perdue | editorAbsorbChange (d146fbe) |
| B | Éditeur fermé sans garde-fou | closeSlideOver() appelé sans condition + click-outside non filtré |
closeEditorForDeleted (3dbce78), whitelist (e1ddc65) |
| C | PUT /entries sans withEncryptedMeta |
Les colonnes en clair sont droppées côté serveur → le body ne contient rien de stockable et blanchit les *_enc |
d146fbe |
| D | UI construite sans regarder la vue | Le menu kebab est partagé entre vues ; la corbeille n'était pas testée | d146fbe |
A — Éditeur ouvert vs écriture extérieure
Préparer : 3 entrées A, B, C, dont A dans le dossier Work avec le tag
old.
A1 · batch Add tag sur l'entrée éditée
- PRÉ :
Aouverte dans l'éditeur, taper du texte dans NOTE/PASSWORD (ne pas sauver) - ACTION : cocher
A+B→ barre de sélection → Add tag →shared→ puis Save dans l'éditeur - ATTENDU : le chip
sharedapparaît immédiatement dans l'éditeur ; après Save,Aporteold, sharedet la modif tapée. Le bouton Save reste visible (l'entrée était déjà dirty) mais le tag seul ne rend pas dirty.✅
A2 · batch Add tag sur une autre entrée
- PRÉ :
Aouverte + dirty - ACTION : cocher
B+Cseulement → Add tag →x→ Save dans l'éditeur - ATTENDU : l'éditeur ne bouge pas (aucun chip ajouté),
Aconserve ses tags,B/Ctaguées.✅
A3 · batch Move to folder sur l'entrée éditée
- ACTION :
Aouverte + dirty → cocherA→ Move to folder… →Perso→ Save - ATTENDU : le select Folder de l'éditeur bascule sur
Persotout seul ; après Save l'entrée est bien dansPerso.✅
A4 · dossier choisi dans l'éditeur puis move extérieur
- ACTION :
Aouverte, changer Folder →Archive(ne pas sauver) → cocherA→ Move to folder… →Perso→ Save - ATTENDU :
Persogagne (action extérieure = intention la plus récente). Comportement documenté, pas un bug. ✅
A5 · glisser une entrée sur un dossier (moveEntryToFolder)
- ACTION :
Aouverte + dirty → glisser la ligneAsurPersodans la sidebar → Save - ATTENDU : identique à A3.✅
A6 · glisser une multi-sélection sur un dossier (moveEntriesToFolder) ⚠️
- ACTION : cocher
A+B→ glisser surPerso→ recharger (F5 impossible : relancer l'app) - ATTENDU : dossier changé et nom / username / site / tags intacts.
- Historique : ce chemin n'appelait pas
withEncryptedMeta→ il blanchissait les métadonnées chiffrées. Si des entrées ont été déplacées ainsi avant le fix, elles sont peut-être déjà vides — vérifier.✅
A7 · suppression du dossier de l'entrée éditée
- ACTION :
A(dossierWork) ouverte + dirty → sidebar → supprimer le dossierWork→ Save - ATTENDU : le select de l'éditeur passe à
All; après Save l'entrée est dansAll, le dossier supprimé ne réapparaît pas.✅
A8 · tags édités des deux côtés
- ACTION :
Aouverte, ajouter le chipminedans l'éditeur (ne pas sauver) → batch Add tagsharedsurA→ Save - ATTENDU :
old, mine, shared— aucun des trois perdu.✅
A9 · favori / épingle pendant l'édition (non-régression)
- ACTION :
Aouverte + dirty → cliquer ★ et 📌 sur la ligneA→ Save - ATTENDU : l'entrée reste favorite et épinglée (ces colonnes ne sont pas dans le PUT de l'éditeur).✅
A10 · icône / pièce jointe pendant l'édition (non-régression)
- ACTION :
Aouverte + dirty → Settings → Fetch missing favicons (ou upload d'icône) → Save - ATTENDU : l'icône survit au Save (endpoint dédié, hors payload éditeur).✅ icon not refreshed in edit
A11 · sync / import pendant que l'éditeur est ouvert — trou connu
- ACTION :
Aouverte + dirty → lancer Sync now (ou importer un JSON en mode overwrite) qui metAà jour → Save - ATTENDU AUJOURD'HUI : le Save de l'éditeur écrase la version venue du sync/import. Non corrigé — voir « Reste à traiter » plus bas.
B — Fermeture de l'éditeur & message de discard
Le message n'apparaît que si confirmOnUnsaved est actif et que l'entrée
est dirty. Tester avec askBeforeDelete ON puis OFF (Settings → Security).
B1 · ✕ sur une autre ligne
- PRÉ :
Aouverte + dirty - ACTION : cliquer ✕ (Move to trash) sur la ligne
B - ATTENDU :
Bpart à la corbeille, l'éditeur deAreste ouvert avec les modifs. Vrai dans les deux réglages deaskBeforeDelete.
B2 · ✕ sur la ligne éditée, askBeforeDelete = OFF
- ACTION :
Aouverte + dirty → ✕ surA - ATTENDU : modale « Discard unsaved changes? ». Annuler ⇒ rien n'est supprimé et l'éditeur reste ouvert. Confirmer ⇒
Aà la corbeille + éditeur fermé.
B3 · ✕ sur la ligne éditée, askBeforeDelete = ON
- ATTENDU : une seule modale (« Move to trash »). Pas de double prompt.
B4 · batch Delete incluant l'entrée éditée
- ACTION :
Aouverte + dirty → cocherA+B→ Delete - ATTENDU : une seule modale (« 2 entries will be moved to trash »), puis l'éditeur se ferme.
B5 · batch Delete excluant l'entrée éditée
- ATTENDU : l'éditeur reste ouvert, modifs intactes.
B6 · corbeille
- ACTION : ouvrir une entrée de la corbeille via Trash actions, puis : (a) Delete forever dessus, (b) batch purge la contenant, (c) Empty trash
- ATTENDU : dans les trois cas l'éditeur se ferme s'il portait une entrée purgée, sinon il reste.
B7 · boutons de la barre de sélection
- ACTION :
Aouverte + dirty → cocherB→ cliquer successivement Move to folder…, Add tag, Clear - ATTENDU : aucun de ces clics ne ferme l'éditeur ni n'affiche de discard.
B8 · bouton Empty trash (barre de titre)
- ATTENDU : idem, pas de fermeture parasite de l'éditeur.
B9 · chemins de fermeture légitimes (non-régression)
- Esc, ✕ de l'éditeur, clic sur le fond de la grille, ouverture d'une autre entrée
- ATTENDU : message de discard à chaque fois si dirty ; annuler garde l'éditeur ouvert sur la bonne entrée.
B10 · verrouillage (comportement assumé)
- ACTION :
Aouverte + dirty → auto-lock ou Lock vault - ATTENDU : fermeture sans message, modifs perdues. Volontaire : la sécurité prime.
C — withEncryptedMeta sur tout write /entries
Test générique, à rejouer après toute modif touchant un body POST/PUT
/entries : faire l'action, relancer l'app (pour relire depuis SQLite),
vérifier que nom / site / username / tags s'affichent encore.
Chemins à couvrir : Save éditeur (neuf + existant), duplicate, import
(bulk + overwrite), sync (applyRemoteSnapshot), addTagToEntry,
batchAddTag, moveEntryToFolder, moveEntriesToFolder, batchMoveToFolder,
migrateMetadataAtRest (au unlock), rotation du master password.
Symptôme du bug : après relance, lignes au nom vide / (no name).
D — Vue corbeille
D1 · menu kebab en table view
- ACTION : corbeille + vue table → kebab d'une ligne
- ATTENDU :
Restore/Copy…/Delete foreveruniquement. Pas de Pin / Favorite / Edit / Duplicate / Move to trash.
D2 · vues cartes et liste
- ATTENDU : boutons restore + purge (déjà le cas avant), pas de drag possible.
D3 · clic sur une ligne de corbeille
- ATTENDU : ouvre Trash actions, jamais l'éditeur (une entrée supprimée ne doit pas pouvoir être sauvegardée).
Reste à traiter (constaté pendant l'audit, non corrigé)
-
Sync / import pendant l'édition (scénario A11).
editorAbsorbChangene couvre que les écritures locales tag/dossier. Un vrai filet serait de mémoriserupdated_atà l'ouverture de l'éditeur et, au Save, de comparer avec la valeur courante en mémoire : si elle a bougé, demander « cette entrée a changé ailleurs — écraser ? ». Ça couvre d'un coup sync, import, et tout futur chemin d'écriture. -
Modale d'entrée héritée = code mort.
openEntryModaln'a aucun appelant ;saveEntry,closeEntryModal, le markup#entryModalet leurs listeners ne sont plus atteignables. Danger :saveEntryfait un PUT partiel (nikind, nitotp_secret, nicustom_fields, nitemplate) — exactement le bug quemoveEntryToFolderdocumente comme à ne jamais refaire. À supprimer (JS + HTML) tant qu'il n'est pas recâblé par erreur. -
Click-outside :
#busyOverlayet.toastne sont pas dans la liste blanche. Cliquer un toast ou l'overlay d'attente pendant une opération longue ferme l'éditeur. Impact faible, correctif = 2 lignes dans la même liste.