feat: confirmations destructives de /trash en fenêtre thématisée (v7.45.6)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 1m59s
FlowDeck CI / test (push) Successful in 15m20s

Les 3 confirmations destructives de la poubelle (suppression définitive
unitaire, suppression groupée, Empty Trash) remplacent le confirm() du
navigateur par une fenêtre FlowDeck :

- réutilise les classes flowdeck-modal-overlay / .flowdeck-modal déjà
  présentes dans base.html (variables --bg-secondary / --border / --accent,
  rayon 12 px, boutons Cancel + confirmation coloré) → aucun nouveau CSS ;
- overlay cliquable pour annuler, Échap pour annuler, × pour annuler ;
- bouton de confirmation porteur du libellé exact (Delete / Delete all /
  Empty Trash) et message reprenant le nom de la page ou le nombre de pages ;
- role="alertdialog" aria-modal + aria-labelledby ;
- composant dans trashData() : askConfirm / cancelConfirm / acceptConfirm,
  mutations de propriétés individuelles (pitfall réactivité Alpine) ;
- id #fd-trash-confirm (la classe flowdeck-modal-overlay sert déjà à
  #fd-global-folder-modal → locator unique côté test) ;
- aucun confirm() natif ne subsiste dans trash.html.

Gates : e2e/trash_ui.spec.js étendu — ouverture + titre + message de la
fenêtre, ANNULATION vérifiée (page toujours présente), confirmation
(suppression réelle : plus dans la poubelle + contenu effacé), fenêtre Empty
Trash ouverte/annulée quel que soit le contenu, purge réelle seulement si la
poubelle ne contient que des pages E2E (instance partagée) ; le handler
dialog du test échoue si un dialog natif revient (NATIVE_DIALOG). Suite e2e
11 passed · pytest 1099 passed · ruff OK · OpenAPI 510 chemins / 7.45.6.
This commit is contained in:
2026-10-03 10:16:45 -04:00
parent 6582df3bcb
commit 894004a1f5
8 changed files with 151 additions and 20 deletions
+50 -4
View File
@@ -51,14 +51,16 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
const errs = [];
page.on('pageerror', (e) => errs.push('PAGEERROR ' + String(e).slice(0, 160)));
page.on('console', (m) => { if (m.type() === 'error') errs.push('CONSOLE ' + m.text().slice(0, 160)); });
page.on('dialog', (d) => d.accept()); // confirm() des actions destructives
// Aucun dialog système acceptable : on échoue si le navigateur ouvre un
// confirm()/alert() natif (c'est tout l'objet du changement).
page.on('dialog', (d) => { errs.push('NATIVE_DIALOG ' + d.message()); d.dismiss().catch(() => {}); });
await login(page);
// DB persistante : nommage unique par exécution, on ne dépend jamais des
// restes des runs précédents.
const TAG = 'E2E Trash ' + Date.now().toString(36).toUpperCase();
const nameA = TAG + ' A', nameB = TAG + ' B', nameC = TAG + ' C', nameD = TAG + ' D';
const nameA = TAG + ' A', nameB = TAG + ' B', nameC = TAG + ' C', nameD = TAG + ' D', nameE = TAG + ' E';
const idA = await mkTrashed(page, nameA);
const idB = await mkTrashed(page, nameB);
@@ -109,6 +111,37 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
ids = await trashIds(page);
expect(ids).not.toContain(idC);
// Suppression définitive unitaire → fenêtre de confirmation THÉMATIQUE
// (plus de confirm() du navigateur)
const idE = await mkTrashed(page, nameE);
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(900);
const rowE = page.locator('.trash-item', { hasText: nameE });
await expect(rowE).toHaveCount(1);
await rowE.first().locator('button[title="Delete permanently"]').click();
const modal = page.locator('#fd-trash-confirm'); // id sinon 2e match #fd-global-folder-modal
await expect(modal).toBeVisible();
await expect(modal.locator('h3')).toHaveText('Delete permanently');
await expect(modal.locator('.flowdeck-modal-body p')).toContainText(nameE);
// Annulation → la page est toujours là
await modal.locator('button.cancel').click();
await expect(modal).toBeHidden();
await expect(rowE).toHaveCount(1);
// Confirmation → suppression réelle (plus dans la poubelle, contenu effacé)
await rowE.first().locator('button[title="Delete permanently"]').click();
await expect(modal).toBeVisible();
await modal.locator('button.confirm').click();
await page.waitForTimeout(900);
await expect(rowE).toHaveCount(0);
expect(await trashIds(page)).not.toContain(idE);
// Le SW transforme les réponses API non-ok (404 ici) en erreur réseau →
// `api()` peut rejeter ; dans les deux cas la page n'est plus servable.
const content = await api(page, 'GET', '/api/pages/' + idE + '/content').catch(() => ({ ok: false, status: 0 }));
expect(content.ok).toBeFalsy();
// Purge totale — seulement si la poubelle ne contient QUE des pages E2E
// (l'instance de test est partagée : on ne vide jamais la poubelle d'autrui).
await mkTrashed(page, nameD);
@@ -116,8 +149,20 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
await page.waitForTimeout(900);
await expect(page.locator('.trash-item', { hasText: nameD })).toHaveCount(1);
const all = (await api(page, 'GET', '/board/api/trash')).body || [];
// Fenêtre Empty Trash : ouverture + titre + annulation — testée quel que soit
// le contenu (l'instance est partagée, on n'a pas le droit de tout vider).
await page.locator('button.btn-sm', { hasText: 'Empty Trash' }).click();
await expect(modal).toBeVisible();
await expect(modal.locator('h3')).toHaveText('Empty Trash?');
await expect(modal.locator('.flowdeck-modal-body p')).toContainText(String(all.length));
await modal.locator('button.cancel').click();
await expect(modal).toBeHidden();
await expect(page.locator('.trash-item', { hasText: nameD })).toHaveCount(1);
if (all.every((i) => i.name.startsWith('E2E Trash'))) {
await page.getByRole('button', { name: 'Empty Trash' }).click();
await page.locator('button.btn-sm', { hasText: 'Empty Trash' }).click();
await expect(modal).toBeVisible();
await modal.locator('button.confirm').click();
await page.waitForTimeout(900);
expect(await trashIds(page)).toHaveLength(0);
await expect(page.locator('.lib-empty', { hasText: 'Trash is empty' })).toBeVisible();
@@ -126,6 +171,7 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
JSON.stringify(all.filter((i) => !i.name.startsWith('E2E Trash')).map((i) => i.name)));
}
const fatal = errs.filter((e) => e.startsWith('PAGEERROR') || e.includes('Alpine Expression Error'));
const fatal = errs.filter((e) =>
e.startsWith('PAGEERROR') || e.includes('Alpine Expression Error') || e.startsWith('NATIVE_DIALOG'));
expect(fatal, fatal.join('\n')).toHaveLength(0);
});