fix: aide (bouton ✕), Insert below qui perdait le format, listes numérotées invisibles (v7.59.2)
FlowDeck CI / lint (push) Successful in 2m16s
FlowDeck CI / test (push) Failing after 18m25s
FlowDeck CI / docker (push) Skipped

- aide : bouton ✕ en haut à droite du panneau (classe partagée
  settings-close, 44 px mobile) + helpInit.close() (retour en arrière,
  sinon /workspaces) ; .settings-panel en position:relative pour ancrer
  hamburger + ✕ au panneau
- éditeur « Insert below » (/Write with AI) : insère le markdown source
  conservé à la génération (AIC._md) au lieu du texte extrait du HTML rendu
  (_resultText() reste seulement en repli) → titres/listes/gras conservés
- listes numérotées : aucun numéro n'était affiché (CSS = retrait seul) ;
  le rang des blocs numbered_list contigus est calculé au rendu (data-num)
  et rendu via content: attr(data-num) — /numbered list et l'insertion
  agent affichent 1, 2, 3
- md2b : cases à cocher - [ ] / - [x] testées avant la branche des puces
  (aligné sur paste2b) + séparateur 1) accepté (CommonMark)
- menu slash : requête tapée dans le bloc (/numbered list) répercutée dans
  le filtre du menu quand le focus n'a pas rejoint l'entrée
- tests : tests/test_v7592_ui_fixes.py (5 tests, dont 1 comportemental node
  qui exécute le vrai md2b) ; ruff I001 préexistant sur test_agent.py
- livraison : VERSION + app/main = 7.59.2, OpenAPI 525 chemins, CHANGELOG
This commit is contained in:
2026-10-07 20:29:02 -04:00
parent 39d34ac866
commit a67bd252aa
11 changed files with 181 additions and 16 deletions
+29
View File
@@ -1,5 +1,34 @@
# Changelog - FlowDeck # Changelog - FlowDeck
## v7.59.2 (2026-10-07) — Corrections : aide, « Insert below », listes numérotées
### Fixed
- **Aide** — bouton **✕** en haut à droite du panneau (classe partagée
`settings-close` : 32 px desktop / **44 px mobile**) + méthode `close()` dans
`help.js` (retour à la page précédente, sinon `/workspaces`) ;
`.settings-panel` passé en `position:relative` pour ancrer les boutons au
panneau (hamburger + ✕) plutôt qu'à la page.
- **Éditeur — « Insert below » (/Write with AI)** : on insère le **markdown
source** conservé à la génération (`AIC._md`) au lieu du texte extrait du
HTML rendu (`_resultText()` ne reste qu'en repli pour les messages d'erreur) —
titres, listes, gras et numéros survivent à l'insertion.
- **Listes numérotées** — elles n'avaient **aucun numéro au rendu** : le CSS
ne déclarait que le retrait, jamais le marqueur. Le rang des blocs
`numbered_list` contigus est désormais calculé au rendu (`data-num`) et
affiché via `content: attr(data-num) ". "` → **`/numbered list` et
l'insertion depuis l'assistant affichent enfin 1, 2, 3**.
- **`md2b`** (markdown → blocs, chemin agent) : les cases à cocher `- [ ]` /
`- [x]` sont testées **avant** la branche des puces (une ligne `- [ ]`
était avalée par cette branche) — aligné sur l'ordre déjà correct de
`paste2b` ; le séparateur `1)` est accepté (CommonMark).
- **Menu slash** — une requête tapée **dans le bloc** (`/numbered list`, focus
qui n'a pas rejoint l'entrée du menu) est répercutée dans le filtre au lieu
d'être ignorée.
- **Tests** — `tests/test_v7592_ui_fixes.py` : **5 tests**, dont 1 test
comportemental **node** qui exécute le vrai `md2b` extrait de la source.
- `ruff` : ordre d'imports de `tests/test_agent.py` (I001 préexistant au pull).
## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed) ## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed)
### Fixed ### Fixed
+1 -1
View File
@@ -1 +1 @@
7.59.1 7.59.2
+1 -1
View File
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI( app = FastAPI(
title="FlowDeck", title="FlowDeck",
version="7.59.1", version="7.59.2",
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc", redoc_url="/redoc",
lifespan=lifespan, lifespan=lifespan,
+1
View File
@@ -35,6 +35,7 @@
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu"> <button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button> </button>
<button class="settings-close" @click="close()" title="Close" aria-label="Close help">×</button>
<!-- Mobile drawer backdrop --> <!-- Mobile drawer backdrop -->
<div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div> <div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div>
+2 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "FlowDeck", "title": "FlowDeck",
"version": "7.58.0" "version": "7.59.2"
}, },
"paths": { "paths": {
"/auth/register": { "/auth/register": {
@@ -1098,7 +1098,7 @@
"dashboard" "dashboard"
], ],
"summary": "Help Page", "summary": "Help Page",
"description": "Comprehensive help & documentation page.", "description": "Help & documentation page — settings-style panel with side navigation.",
"operationId": "help_page_help_get", "operationId": "help_page_help_get",
"responses": { "responses": {
"200": { "200": {
+11 -2
View File
@@ -2476,11 +2476,20 @@ button.breadcrumb-current { max-width: 260px; }
font-size: 16px; font-size: 16px;
} }
/* Numbered list */ /* Numbered list — data-num est calculé au rendu (rangée contiguë) */
.block-numbered { .block-numbered {
padding-left: 22px; padding-left: 32px;
position: relative; position: relative;
} }
.block-numbered[data-num]::before {
content: attr(data-num) ". ";
position: absolute;
left: 0;
top: 0;
min-width: 24px;
color: var(--text-primary);
font-size: 16px;
}
/* To-do */ /* To-do */
.block-todo { .block-todo {
+1 -1
View File
@@ -1,6 +1,6 @@
/* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */ /* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */
.settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);} .settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);}
.settings-panel{display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;} .settings-panel{position:relative;display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;}
.settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;} .settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;}
.settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;} .settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
.settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;} .settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;}
+5
View File
@@ -7,6 +7,11 @@ function helpInit() {
section: 'start', section: 'start',
navOpen: false, navOpen: false,
closeNavOnMobile() { this.navOpen = false; }, closeNavOnMobile() { this.navOpen = false; },
// Bouton ✕ (haut droite, visible aussi en mobile) : ferme l'aide.
close() {
if (document.referrer && document.referrer !== location.href) { history.back(); }
else { location.href = '/workspaces'; }
},
go(sec) { go(sec) {
this.section = sec; this.section = sec;
this.navOpen = false; this.navOpen = false;
+29 -8
View File
@@ -419,7 +419,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
// ═══════════ Inline AI composer (Notion AI-style) ═══════════ // ═══════════ Inline AI composer (Notion AI-style) ═══════════
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…" // Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
// → framed result → "Insert below" → applyAIBlocks. // → framed result → "Insert below" → applyAIBlocks.
const AIC={_open:false,_idx:-1,_stage:'',_html:'', const AIC={_open:false,_idx:-1,_stage:'',_html:'',_md:'',
ROOT:null, ROOT:null,
mk(){ if(this.ROOT) return this.ROOT; mk(){ if(this.ROOT) return this.ROOT;
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none'; const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
@@ -432,7 +432,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest(); this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } }); document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } });
}, },
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; if(window.E)window.E.dirty=true; this._stage=''; }, close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; this._md=''; if(window.E)window.E.dirty=true; this._stage=''; },
_anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420}; _anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420};
const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; }, const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; },
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} }, _focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
@@ -478,12 +478,17 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):''; const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; } if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
window.fdAgent.generate(prompt, ctx).then(function(text){ window.fdAgent.generate(prompt, ctx).then(function(text){
self._html=self._mdToHtml(text||''); self._stage='result'; self.paint(); // On conserve le MARKDOWN source : c'est lui qui est inséré (md2b),
// pas le texte extrait du HTML rendu qui perd titres/listes/gras.
self._md=String(text||'');
self._html=self._mdToHtml(self._md); self._stage='result'; self.paint();
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); }); }).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
}, },
_err(msg){ this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); }, _err(msg){ this._md=''; this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync(); insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync();
const result=this._resultText(); if(!result)return; E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); }, // Markdown source d'abord ; repli sur le texte visible (erreurs).
const result=String(this._md||'').trim()||this._resultText(); if(!result)return;
E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); }, _resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); },
toast(m){ if(window.showToast)window.showToast(m,'info'); } toast(m){ if(window.showToast)window.showToast(m,'info'); }
}; };
@@ -595,11 +600,19 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`; const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:''); const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:''; const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
// Numéro d'ordre : rang des blocs `numbered_list` contigus (1,2,3…) ;
// passé en attribut data-num → le CSS le rend hors du contenu éditable.
let numAttr='';
if(b.type==='numbered_list'&&idx>=0){
const bs=(window.E&&window.E.blocks)||[];
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
numAttr=` data-num="${idx-s+1}"`;
}
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:''; const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
return `<div class="block-wrapper" data-id="${b.id}"> return `<div class="block-wrapper" data-id="${b.id}">
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div> <div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
${actions} ${actions}
<${tag}${style}${bidAttr} class="${cls}">${inner}</${tag}> <${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
</div>`; </div>`;
} }
@@ -1970,6 +1983,12 @@ applyAIBlocks(text){
if(e.key==='Escape'){e.preventDefault();SM.close();return;} if(e.key==='Escape'){e.preventDefault();SM.close();return;}
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;} if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;} if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
// La requête peut être tapée DANS le bloc (« /numbered list ») si le
// focus n'a pas rejoint l'entrée du menu → on la répercute au filtre.
if(SM._in&&(e.key.length===1||e.key==='Backspace')){
const q=(el.textContent||'').replace(/^\s*\//,'');
if(SM._in.value!==q){SM._in.value=q;SM.flt();}
}
return; return;
} }
if(window.AIAC&&window.AIAC.active){ if(window.AIAC&&window.AIAC.active){
@@ -2486,10 +2505,12 @@ applyAIBlocks(text){
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4))); else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5))); else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
else if(t==='---'||t==='***')bl.push(this.mkB('divider','')); else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2))); // AVANT les puces : « - [ ] … » commence aussi par « - » →
else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,''))); // sinon la case à cocher de l'agent devenait une puce.
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6))); else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true})); else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
else if(/^\d+[.)] /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+[.)] /,'')));
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2))); else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
else bl.push(this.mkB('paragraph',t)); else bl.push(this.mkB('paragraph',t));
}return bl;}, }return bl;},
+1 -1
View File
@@ -93,7 +93,7 @@ def test_mistral_defaults_are_tier_safe(client):
"""La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large. """La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large.
Le modèle par défaut et le premier de la liste du test de connexion doivent Le modèle par défaut et le premier de la liste du test de connexion doivent
être des modèles servis par tous les plans.""" être des modèles servis par tous les plans."""
from app.services.llm_client import PROVIDERS, PROVIDER_MODELS from app.services.llm_client import PROVIDER_MODELS, PROVIDERS
tier_blocked = {"mistral-large-latest", "pixtral-large-latest"} tier_blocked = {"mistral-large-latest", "pixtral-large-latest"}
assert PROVIDERS["mistral"][1] not in tier_blocked assert PROVIDERS["mistral"][1] not in tier_blocked
assert PROVIDER_MODELS["mistral"][0] not in tier_blocked assert PROVIDER_MODELS["mistral"][0] not in tier_blocked
+100
View File
@@ -0,0 +1,100 @@
"""7.59.2 — corrections : bouton ✕ de l'aide, « Insert below » qui perd le
format, listes numérotées invisibles (agent + /numbered list)."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
HELP_HTML = ROOT / "app" / "templates" / "help.html"
HELP_JS = ROOT / "static" / "js" / "help.js"
EDITOR_JS = ROOT / "static" / "js" / "page_editor_scripts.js"
APP_CSS = ROOT / "static" / "css" / "app.css"
SETTINGS_CSS = ROOT / "static" / "css" / "settings.css"
# ── 1. Aide : fermeture en haut à droite (mobile compris) ───────────────────
def test_help_page_has_close_button():
html = HELP_HTML.read_text(encoding="utf-8")
js = HELP_JS.read_text(encoding="utf-8")
css = SETTINGS_CSS.read_text(encoding="utf-8")
assert 'class="settings-close"' in html # bouton ✕ présent
assert '@click="close()"' in html
assert "close()" in js and "history.back()" in js # méthode réelle
assert ".settings-close{" in css # style partagé (32/44px)
assert "position:relative" in css.split(".settings-panel{")[1][:40]
# ── 2. Insert below : on insère le MARKDOWN, pas le texte du HTML rendu ─────
def test_ai_insert_below_keeps_source_markdown():
src = EDITOR_JS.read_text(encoding="utf-8")
assert "self._md=String(text||'')" in src # source conservée
assert "String(this._md||'').trim()||this._resultText()" in src # md d'abord
assert src.count("this._md='';") >= 2 # close() + _err()
# l'ancien chemin (texte extrait du DOM rendu) n'est plus que le repli
assert "_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result')" in src
# ── 3. Listes numérotées : numéros réellement affichés ──────────────────────
def test_numbered_list_renders_its_number():
src = EDITOR_JS.read_text(encoding="utf-8")
css = APP_CSS.read_text(encoding="utf-8")
assert "let numAttr=''" in src and "data-num=" in src # rangée contiguë → rang
assert ".block-numbered[data-num]::before" in css
assert 'content: attr(data-num) ". "' in css
# md2b accepte « 1. » et « 1) » (CommonMark) pour l'insertion agent
assert r"/^\d+[.)] /" in src
def test_slash_query_typed_in_block_is_synced_to_filter():
src = EDITOR_JS.read_text(encoding="utf-8")
# « /numbered list » tapé dans le bloc → répercuté dans l'entrée du menu
assert "SM._in.value=q" in src
assert "SM.flt();" in src
# ── md2b : test comportemental sur la source réelle (node) ──────────────────
def _md2b_source() -> str:
src = EDITOR_JS.read_text(encoding="utf-8")
i = src.index("md2b(md){")
j = src.index("return bl;},", i) + len("return bl;},")
snippet = src[i:j].rstrip().rstrip(",") # « md2b(md){…} »
return snippet
def test_md2b_converts_numbered_lists_for_real():
node = shutil.which("node")
if not node:
pytest.skip("node absent : test comportemental md2b ignoré")
md = ("# Titre\n"
"1. premier\n"
"2. second\n"
"3) troisième\n"
"- puce\n"
"- [ ] à faire\n"
"> citation\n")
script = (
"function _tblIsData(){return false;}\n"
"function _tblSplitLine(){return [];}\n"
"const editor={mkB(t,c,e){const o={type:t,content:c||''};"
"if(e)Object.assign(o,e);return o;},\n"
f"{_md2b_source()}}};\n"
f"console.log(JSON.stringify(editor.md2b({json.dumps(md)})));\n"
)
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
timeout=30, encoding="utf-8")
assert out.returncode == 0, out.stderr
blocks = json.loads(out.stdout)
assert [b["type"] for b in blocks] == [
"heading_1",
"numbered_list", "numbered_list", "numbered_list", # 1. 2. 3)
"bulleted_list", "to_do", "quote",
]
assert [b["content"] for b in blocks[1:4]] == ["premier", "second", "troisième"]