feat: confirmations destructives de /trash en fenêtre thématisée (v7.45.6)
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:
@@ -1,5 +1,30 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.45.6 (2026-10-03) — Fenêtre de confirmation thématisée pour les suppressions de la poubelle
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Les 3 confirmations destructives de /trash** (suppression définitive
|
||||||
|
unitaire, suppression groupée, **Empty Trash**) remplacent le `confirm()` du
|
||||||
|
navigateur par une **fenêtre thématisée FlowDeck** : classes
|
||||||
|
`flowdeck-modal-overlay` / `.flowdeck-modal` déjà présentes dans `base.html`
|
||||||
|
(variables `--bg-secondary`, `--border`, `--accent`, rayon 12 px), overlay
|
||||||
|
cliquable pour annuler, **Échap** pour annuler, bouton Cancel + bouton de
|
||||||
|
confirmation coloré porteur du libellé exact (`Delete` / `Delete all` /
|
||||||
|
`Empty Trash`), titre + message reprenant le nom de la page ou le nombre de
|
||||||
|
pages concernées, `role="alertdialog"` + `aria-modal`.
|
||||||
|
- Aucun `confirm()` natif ne subsiste dans `trash.html` — le composant est
|
||||||
|
monté dans `trashData()` (`askConfirm` / `cancelConfirm` / `acceptConfirm`,
|
||||||
|
mutations de propriétés individuelles pour la réactivité Alpine).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `e2e/trash_ui.spec.js` : la fenêtre est ouverte, son titre et son message
|
||||||
|
vérifiés, **l'annulation est vérifiée** (page toujours présente), puis la
|
||||||
|
confirmation (suppression réelle : plus dans la poubelle + contenu effacé) et
|
||||||
|
enfin la purge via Empty Trash. Le handler `dialog` du test échoue
|
||||||
|
maintenant si un `confirm()`/`alert()` natif revient (`NATIVE_DIALOG`).
|
||||||
|
|
||||||
## v7.45.5 (2026-10-03) — Poubelle : actions groupées, tri/filtres réels, purge, et 4 bugs
|
## v7.45.5 (2026-10-03) — Poubelle : actions groupées, tri/filtres réels, purge, et 4 bugs
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1008,6 +1008,7 @@ Détails livrés :
|
|||||||
- Suivis restants (mesurés, non traités) : CLS résiduel **0.058** sur /library (états `lib-loading`/`lib-empty` → `lib-table` : le remplacement du spinner par les lignes décale le contenu — nécessiterait un squelette de lignes, pas un réglage CSS) · fenêtre `x-ignore` de **~150–300 ms** sur /settings (coût des scripts de page avant `initTree`, pas de gain sans changer l'ordre de montage) · la **topbar est dans `.main-wrapper`** (base.html) → re-swappée et re-montée à chaque navigation (avec le masquage elle disparaît/revient au lieu de flasher — la sortir demanderait de la swapper séparément).
|
- Suivis restants (mesurés, non traités) : CLS résiduel **0.058** sur /library (états `lib-loading`/`lib-empty` → `lib-table` : le remplacement du spinner par les lignes décale le contenu — nécessiterait un squelette de lignes, pas un réglage CSS) · fenêtre `x-ignore` de **~150–300 ms** sur /settings (coût des scripts de page avant `initTree`, pas de gain sans changer l'ordre de montage) · la **topbar est dans `.main-wrapper`** (base.html) → re-swappée et re-montée à chaque navigation (avec le masquage elle disparaît/revient au lieu de flasher — la sortir demanderait de la swapper séparément).
|
||||||
- Suite v7.45.4 : `x-cloak` posé sur `.app-layout` (racine Alpine) → les **chargements complets** (F5, SW, hors ligne) ne peignent plus la sidebar brute ni la bande rouge hors ligne — le masquage fdLoad ne couvrait que les swaps partiels. `fd-navigating` (classe sans règle CSS) supprimée. Probe étendu à 4 scénarios (nav / clic Home réel / Home hors ligne / plein chargement) avec assertions. **Symptôme Home non rejouable** en navigateur frais (0 frame brut, 0 erreur console) → à confirmer sur le navigateur signalé.
|
- Suite v7.45.4 : `x-cloak` posé sur `.app-layout` (racine Alpine) → les **chargements complets** (F5, SW, hors ligne) ne peignent plus la sidebar brute ni la bande rouge hors ligne — le masquage fdLoad ne couvrait que les swaps partiels. `fd-navigating` (classe sans règle CSS) supprimée. Probe étendu à 4 scénarios (nav / clic Home réel / Home hors ligne / plein chargement) avec assertions. **Symptôme Home non rejouable** en navigateur frais (0 frame brut, 0 erreur console) → à confirmer sur le navigateur signalé.
|
||||||
- [x] **Poubelle** (v7.45.5) — page /trash alignée sur ce que fait ce type de section : **sélection multiple** (select all porté au filtre courant + barre Restore/Delete/Clear), **Empty Trash** (`POST /board/api/trash/empty`, purge en masse en 1 requête), **tri** (récents/anciens/nom) et **filtre emplacement** réels (les deux boutons décoratifs « Last edited by ▾ » / « In ▾ » qui ne faisaient rien sont remplacés), **date de suppression + jours restants** par élément (rétention 30 j), compteur de résultats, états vides distincts, toasts sur chaque action. 4 bugs trouvés en route : (1) **Restore/Delete ne marchaient jamais** (`getCsrf()` = chaîne mais code lu `csrf?.[1]` → 403 CSRF silencieux), (2) **« Move to Trash » de l'éditeur = 404** (route `/board/api/pages/{id}/trash` inexistante, `.then()` naviguait quand même → la page n'allait jamais à la poubelle), (3) **page restaurée invisible** en Library/Recents/Private (`parent_section='Trash'` réécrit au delete, jamais remis → `deleted_at` = source de vérité unique + réparation au boot), (4) **routes trash sans auth** (401 ajouté, vérifié anonyme). Gates : `tests/test_trash_api.py` (5) + `e2e/trash_ui.spec.js` (1) + suite 1099 passed.
|
- [x] **Poubelle** (v7.45.5) — page /trash alignée sur ce que fait ce type de section : **sélection multiple** (select all porté au filtre courant + barre Restore/Delete/Clear), **Empty Trash** (`POST /board/api/trash/empty`, purge en masse en 1 requête), **tri** (récents/anciens/nom) et **filtre emplacement** réels (les deux boutons décoratifs « Last edited by ▾ » / « In ▾ » qui ne faisaient rien sont remplacés), **date de suppression + jours restants** par élément (rétention 30 j), compteur de résultats, états vides distincts, toasts sur chaque action. 4 bugs trouvés en route : (1) **Restore/Delete ne marchaient jamais** (`getCsrf()` = chaîne mais code lu `csrf?.[1]` → 403 CSRF silencieux), (2) **« Move to Trash » de l'éditeur = 404** (route `/board/api/pages/{id}/trash` inexistante, `.then()` naviguait quand même → la page n'allait jamais à la poubelle), (3) **page restaurée invisible** en Library/Recents/Private (`parent_section='Trash'` réécrit au delete, jamais remis → `deleted_at` = source de vérité unique + réparation au boot), (4) **routes trash sans auth** (401 ajouté, vérifié anonyme). Gates : `tests/test_trash_api.py` (5) + `e2e/trash_ui.spec.js` (1) + suite 1099 passed.
|
||||||
|
- [x] **Fenêtre de confirmation thématisée** (v7.45.6) — les 3 confirmations destructives de /trash (suppression unitaire, groupée, **Empty Trash**) ne passent plus par le `confirm()` du navigateur : fenêtre FlowDeck réutilisant les classes `flowdeck-modal*` déjà présentes dans `base.html` (variables du thème, overlay cliquable + Échap pour annuler, Cancel + bouton de confirmation coloré porteur du libellé exact, titre/message avec le nom de la page ou le nombre de pages, `role="alertdialog"`). Composant dans `trashData()` : `askConfirm` / `cancelConfirm` / `acceptConfirm` (mutations de propriétés individuelles, pitfall réactivité Alpine). Gate e2e : ouverture + titre + message + **annulation (page toujours là)** + confirmation (contenu réellement effacé) + purge, et le test échoue si un dialog natif revient (`NATIVE_DIALOG`).
|
||||||
|
|
||||||
### v7.1.0 — Calendar sync + Meeting Notes ✅ (2026-09-28)
|
### v7.1.0 — Calendar sync + Meeting Notes ✅ (2026-09-28)
|
||||||
> **Objectif** : calendrier bidirectionnel + transcription → agents (cf. Notion 07/2026 : Meeting Notes trigger Custom Agents).
|
> **Objectif** : calendrier bidirectionnel + transcription → agents (cf. Notion 07/2026 : Meeting Notes trigger Custom Agents).
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.45.5 (poubelle : sélection groupée + Empty Trash + tri/filtres réels + dates par élément ; fix CSRF restore/delete, fix 404 « Move to Trash » éditeur, fix pages restaurées invisibles, auth sur les routes trash) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.45.6 (confirmations destructives de /trash en fenêtre thématisée FlowDeck — plus de confirm() natif, annulation/Échap testés en e2e) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.45.5",
|
version="7.45.6",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
+71
-12
@@ -106,6 +106,25 @@
|
|||||||
<span>Once a page has been in Trash for 30 days, it will be automatically deleted</span>
|
<span>Once a page has been in Trash for 30 days, it will be automatically deleted</span>
|
||||||
<span style="font-size:16px;">ⓘ</span>
|
<span style="font-size:16px;">ⓘ</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation thématique (remplace confirm() du navigateur) — classes
|
||||||
|
flowdeck-modal définies dans base.html, thème variables du site -->
|
||||||
|
<div id="fd-trash-confirm" class="flowdeck-modal-overlay" x-show="confirm.show" x-transition.opacity
|
||||||
|
@click.self="cancelConfirm()" @keydown.escape.window="cancelConfirm()" style="display:none;">
|
||||||
|
<div class="flowdeck-modal" x-transition role="alertdialog" aria-modal="true" aria-labelledby="fd-trash-confirm-title">
|
||||||
|
<div class="flowdeck-modal-header">
|
||||||
|
<h3 id="fd-trash-confirm-title" x-text="confirm.title"></h3>
|
||||||
|
<button class="flowdeck-modal-close" @click="cancelConfirm()" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="flowdeck-modal-body">
|
||||||
|
<p x-text="confirm.message"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flowdeck-modal-footer">
|
||||||
|
<button class="flowdeck-modal-btn cancel" @click="cancelConfirm()">Cancel</button>
|
||||||
|
<button class="flowdeck-modal-btn confirm" @click="acceptConfirm()" x-text="confirm.okText"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -127,6 +146,8 @@
|
|||||||
locOpen: false,
|
locOpen: false,
|
||||||
selected: [],
|
selected: [],
|
||||||
items: [],
|
items: [],
|
||||||
|
// Confirmation thématique (remplace confirm() du navigateur)
|
||||||
|
confirm: { show: false, title: '', message: '', okText: 'Delete', action: null },
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
@@ -184,6 +205,27 @@
|
|||||||
setSort(v) { this.sort = v; this.sortOpen = false; },
|
setSort(v) { this.sort = v; this.sortOpen = false; },
|
||||||
setLoc(v) { this.locFilter = v; this.locOpen = false; },
|
setLoc(v) { this.locFilter = v; this.locOpen = false; },
|
||||||
|
|
||||||
|
// ── Confirmation (modal thème FlowDeck, classes flowdeck-modal de base) ──
|
||||||
|
// Mutations de propriétés individuelles (remplacer l'objet entier tue la
|
||||||
|
// réactivité Alpine — pitfall connu).
|
||||||
|
askConfirm(title, message, okText, action) {
|
||||||
|
this.confirm.title = title;
|
||||||
|
this.confirm.message = message;
|
||||||
|
this.confirm.okText = okText || 'Delete';
|
||||||
|
this.confirm.action = action;
|
||||||
|
this.confirm.show = true;
|
||||||
|
},
|
||||||
|
cancelConfirm() {
|
||||||
|
this.confirm.show = false;
|
||||||
|
this.confirm.action = null;
|
||||||
|
},
|
||||||
|
acceptConfirm() {
|
||||||
|
const action = this.confirm.action;
|
||||||
|
this.confirm.show = false;
|
||||||
|
this.confirm.action = null;
|
||||||
|
if (action) action();
|
||||||
|
},
|
||||||
|
|
||||||
// ── API ──────────────────────────────────────────────────────────
|
// ── API ──────────────────────────────────────────────────────────
|
||||||
// getCsrf() renvoie la CHAÎNE du jeton (base.html) : l'ancien code faisait
|
// getCsrf() renvoie la CHAÎNE du jeton (base.html) : l'ancien code faisait
|
||||||
// csrf?.[1] → 2e caractère → 403 CSRF sur restore/delete, silencieux.
|
// csrf?.[1] → 2e caractère → 403 CSRF sur restore/delete, silencieux.
|
||||||
@@ -210,13 +252,6 @@
|
|||||||
else window.showToast('Restore failed', 'error');
|
else window.showToast('Restore failed', 'error');
|
||||||
} catch (e) { window.showToast('Restore failed: ' + e.message, 'error'); }
|
} catch (e) { window.showToast('Restore failed: ' + e.message, 'error'); }
|
||||||
},
|
},
|
||||||
async deleteForever(id) {
|
|
||||||
if (!confirm('Permanently delete this page? This cannot be undone.')) return;
|
|
||||||
try {
|
|
||||||
if (await this._delete(id)) window.showToast('Page permanently deleted', 'success');
|
|
||||||
else window.showToast('Delete failed', 'error');
|
|
||||||
} catch (e) { window.showToast('Delete failed: ' + e.message, 'error'); }
|
|
||||||
},
|
|
||||||
async restoreSelected() {
|
async restoreSelected() {
|
||||||
const ids = this.selected.slice();
|
const ids = this.selected.slice();
|
||||||
let ok = 0;
|
let ok = 0;
|
||||||
@@ -225,18 +260,42 @@
|
|||||||
}
|
}
|
||||||
window.showToast(ok + ' of ' + ids.length + ' page(s) restored', ok === ids.length ? 'success' : 'error');
|
window.showToast(ok + ' of ' + ids.length + ' page(s) restored', ok === ids.length ? 'success' : 'error');
|
||||||
},
|
},
|
||||||
async deleteSelected() {
|
async _deleteAndToast(ids) {
|
||||||
const ids = this.selected.slice();
|
|
||||||
if (!confirm('Permanently delete ' + ids.length + ' page(s)? This cannot be undone.')) return;
|
|
||||||
let ok = 0;
|
let ok = 0;
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
try { if (await this._delete(id)) ok++; } catch (e) { /* compté en échec */ }
|
try { if (await this._delete(id)) ok++; } catch (e) { /* compté en échec */ }
|
||||||
}
|
}
|
||||||
window.showToast(ok + ' of ' + ids.length + ' page(s) deleted', ok === ids.length ? 'success' : 'error');
|
const msg = ids.length === 1
|
||||||
|
? (ok ? 'Page permanently deleted' : 'Delete failed')
|
||||||
|
: ok + ' of ' + ids.length + ' page(s) deleted';
|
||||||
|
window.showToast(msg, ok === ids.length ? 'success' : 'error');
|
||||||
|
},
|
||||||
|
deleteForever(id) {
|
||||||
|
const self = this;
|
||||||
|
const item = this.items.find(function (i) { return i.id === id; });
|
||||||
|
const name = item ? item.name : 'this page';
|
||||||
|
this.askConfirm('Delete permanently',
|
||||||
|
'“' + name + '” will be deleted for good. This cannot be undone.',
|
||||||
|
'Delete',
|
||||||
|
function () { self._deleteAndToast([id]); });
|
||||||
|
},
|
||||||
|
deleteSelected() {
|
||||||
|
const self = this;
|
||||||
|
const ids = this.selected.slice();
|
||||||
|
this.askConfirm('Delete ' + ids.length + ' page(s) for good?',
|
||||||
|
'These pages will be permanently deleted. This cannot be undone.',
|
||||||
|
'Delete all',
|
||||||
|
function () { self._deleteAndToast(ids); });
|
||||||
},
|
},
|
||||||
async emptyTrash() {
|
async emptyTrash() {
|
||||||
|
const self = this;
|
||||||
const n = this.items.length;
|
const n = this.items.length;
|
||||||
if (!confirm('Permanently delete ALL ' + n + ' page(s) in the Trash? This cannot be undone.')) return;
|
this.askConfirm('Empty Trash?',
|
||||||
|
'All ' + n + ' page(s) in the Trash will be permanently deleted. This cannot be undone.',
|
||||||
|
'Empty Trash',
|
||||||
|
function () { self._emptyAndToast(n); });
|
||||||
|
},
|
||||||
|
async _emptyAndToast(n) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/board/api/trash/empty', { method: 'POST', headers: this._headers() });
|
const r = await fetch('/board/api/trash/empty', { method: 'POST', headers: this._headers() });
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.45.5"
|
"version": "7.45.6"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
+50
-4
@@ -51,14 +51,16 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
|
|||||||
const errs = [];
|
const errs = [];
|
||||||
page.on('pageerror', (e) => errs.push('PAGEERROR ' + String(e).slice(0, 160)));
|
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('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);
|
await login(page);
|
||||||
|
|
||||||
// DB persistante : nommage unique par exécution, on ne dépend jamais des
|
// DB persistante : nommage unique par exécution, on ne dépend jamais des
|
||||||
// restes des runs précédents.
|
// restes des runs précédents.
|
||||||
const TAG = 'E2E Trash ' + Date.now().toString(36).toUpperCase();
|
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 idA = await mkTrashed(page, nameA);
|
||||||
const idB = await mkTrashed(page, nameB);
|
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);
|
ids = await trashIds(page);
|
||||||
expect(ids).not.toContain(idC);
|
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
|
// 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).
|
// (l'instance de test est partagée : on ne vide jamais la poubelle d'autrui).
|
||||||
await mkTrashed(page, nameD);
|
await mkTrashed(page, nameD);
|
||||||
@@ -116,8 +149,20 @@ test('trash : rendu, dates, sélection groupée, restauration, purge', async ({
|
|||||||
await page.waitForTimeout(900);
|
await page.waitForTimeout(900);
|
||||||
await expect(page.locator('.trash-item', { hasText: nameD })).toHaveCount(1);
|
await expect(page.locator('.trash-item', { hasText: nameD })).toHaveCount(1);
|
||||||
const all = (await api(page, 'GET', '/board/api/trash')).body || [];
|
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'))) {
|
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);
|
await page.waitForTimeout(900);
|
||||||
expect(await trashIds(page)).toHaveLength(0);
|
expect(await trashIds(page)).toHaveLength(0);
|
||||||
await expect(page.locator('.lib-empty', { hasText: 'Trash is empty' })).toBeVisible();
|
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)));
|
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);
|
expect(fatal, fatal.join('\n')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user