Files
flowdeck/tests/test_v761_block_menu.py
T
bruno 033490c464
FlowDeck CI / lint (push) Successful in 2m16s
FlowDeck CI / test (push) Failing after 20m32s
FlowDeck CI / docker (push) Skipped
feat: menu contextuel de bloc refondu façon Notion — 10 actions, sous-menus et raccourcis (v7.61.0)
- panneau de gauche : recherche « Search actions... », section « Text », les
  10 actions dans l'ordre Notion (Turn into, Color, Copy link Alt+⇧+L,
  Duplicate Ctrl+D, Move to Ctrl+⇧+P, Delete Del, Comment Ctrl+⇧+M,
  Suggest edits Ctrl+⇧+Alt+X, Ask AI Ctrl+J, Skills) + pied métadonnées
  (auteur / heure de dernière édition / mots-caractères du bloc)
- panneau de droite au survol (fermeture différée) : Turn into avec ✓ du
  type actuel + Heading 1-4, listes, To-do, Toggle, Code, Quote, Callout,
  Block equation, Synced block, 2/3/4/5 columns (info-bulle « Create N
  columns of blocks ») ; Color avec Last used / Text color / Background
  color (10 couleurs nommées, ✓ courante, carré de couleur) ; Skills =
  compétences réelles de l'espace appliquées au bloc via l'IA
- turnInto(idx,type,cols) : crée N colonnes, sélecteur de source pour les
  blocs synchronisés ; setBlockColor mémorise la dernière couleur (Ctrl+⇧+H)
- nouvelles actions éditeur : commentBlock (sélection + tiroir), askAI
  (composeur sans effacer le bloc), suggestEdit / runSkill (IA, insertion
  sous le bloc), applyLastColor
- raccourcis : handler clavier dédié (Ctrl+J, Ctrl+⇧+H, Ctrl+⇧+M,
  Ctrl+⇧+Alt+X, Alt+⇧+L, Ctrl+⇧+P, Ctrl+D sans double, Del seulement avec le
  menu ouvert, Échap) — les entrées Copy/Cut/Paste du menu sont retirées
  (Ctrl+C/X/V conservés)
- styles : .bm-group, .bm-foot, .bm-check, .bm-current, .bm-dot, sous-menu
  60vh, recherche focalisée à l'ouverture
- tests : tests/test_v761_block_menu.py (9 tests, dont node : colonnes) +
  test_v7593 adapté ; suite complète 1362 passed / 0 failed, ruff 0, eslint 0
- livraison : VERSION + app/main = 7.61.0, OpenAPI 525 chemins, CHANGELOG
2026-10-07 23:09:21 -04:00

170 lines
8.0 KiB
Python

"""7.61.0 — menu contextuel de bloc refondu façon Notion (actions + raccourcis)."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
SRC = (ROOT / "static" / "js" / "page_editor_scripts.js").read_text(encoding="utf-8")
CSS = (ROOT / "static" / "css" / "app.css").read_text(encoding="utf-8")
def _slice(start: str, end: str) -> str:
i = SRC.index(start)
j = SRC.index(end, i)
return SRC[i:j].rstrip()
# ── Panneau de gauche ───────────────────────────────────────────────────────
def test_left_panel_matches_spec():
# recherche, groupe « Text », les 10 actions dans l'ordre, pied métadonnées
assert 'placeholder="Search actions..."' in SRC
assert "'<div class=\"bm-group\">Text</div>'" in SRC
order = ["'↩','Turn into'", "'🎨','Color'", "'🔗','Copy link to block'",
"'▣','Duplicate'", "'↪','Move to…'", "'✕','Delete'",
"'💬','Comment'", "'✎','Suggest edits'", "'✨','Ask AI'",
"'📚','Skills'"]
chunk = SRC[src_index("d.innerHTML='<input"):src_index("+_blockMenuMeta(b);")]
positions = [chunk.index(o) for o in order]
assert positions == sorted(positions), positions
# pied de page : auteur / heure / taille
meta = _slice("function _blockMenuMeta(", "\n function blockMenuOpen(")
for token in ("Last edited by ", "word", "character", "Today at ",
".um-name", "updatedAt"):
assert token in meta, token
def src_index(needle: str) -> int:
return SRC.index(needle)
def test_actions_are_all_dispatched():
dispatch = _slice("function _blockMenuClick(", "\n function _blockMenuItem(")
for act in ("submenu", "duplicate", "copy-link", "move-to", "delete",
"comment", "suggest", "ai", "skill", "color", "turn-type"):
assert f"act==='{act}'" in dispatch, act
# méthodes réelles côté éditeur
for m in ("commentBlock(idx)", "askAI(idx)", "suggestEdit(idx)",
"runSkill(idx,tpl)", "applyLastColor(idx)",
"copyBlockLink(bid)", "deleteSelected(idx)", "duplicateBlock("):
assert m in SRC, m
# menu : pas d'entrée « copier/couper/coller les blocs » (raccourcis conservés)
assert "act==='copy-blocks'" not in dispatch
# ── Sous-menus ──────────────────────────────────────────────────────────────
def test_turn_into_submenu():
turn = _slice("function _blockTurnMenuOpen(", "\n function _blockColorMenuOpen(")
assert "_BLOCK_TURN_MENU" in turn and "check:on" in turn # ✓ du type actuel
assert "'<span class=\"bm-check\">✓</span>'" in SRC
for name in ("Text", "Heading 1", "Heading 4", "Bulleted list",
"Numbered list", "To-do list", "Toggle list", "Code", "Quote",
"Callout", "Block equation", "Synced block",
"2 columns", "5 columns"):
assert f"'{name}'" in SRC, name
assert "Create '+t.cols+' columns of blocks" in SRC # info-bulle
assert "data-cols" in SRC and "cols>=2" in SRC # 2..5 colonnes
# Page / Page in / Toggle heading : types inexistants dans FlowDeck → absents
assert "'Page in'" not in SRC and "'Toggle heading 1'" not in SRC
def test_color_submenu():
color = _slice("function _blockColorMenuOpen(", "\n function _blockSkillsMenuOpen(")
for group in ("Last used", "Text color", "Background color"):
assert f">{group}</div>" in color, group
names = ["'Default'", "'Gray'", "'Brown'", "'Orange'", "'Yellow'",
"'Green'", "'Blue'", "'Purple'", "'Pink'", "'Red'"]
arr = _slice("const _BLOCK_COLOR_NAMES=", "];")
for n in names:
assert n in arr, n
assert "bm-dot" in color # carré de couleur (fond)
assert "_BM_SHORTCUTS.color" in color # raccourci « Last used »
assert "fdLastBlockColor" in SRC # mémoire de la dernière couleur
def test_skills_submenu_uses_real_skills():
skills = _slice("function _blockSkillsMenuOpen(", "\n function _rtMac(")
assert "fetch('/api/agent/skills'" in skills
assert "act==='skill'" in SRC or "'skill'" in skills
assert "prompt_template" in skills
assert "_aiWritingRun('write',idx" in SRC # exécution réelle via l'IA
# ── Survol / fermeture ──────────────────────────────────────────────────────
def test_hover_submenus_and_close():
for token in ("_blockSubOpen(kind,bid,anchor)", "data-sub",
"closest('[data-sub]')", "mouseleave", "_blockSubCloseLater",
"function _blockMenuClose()", "_blockDocDown"):
assert token in SRC, token
assert "si.focus()" in SRC # recherche focalisée à l'ouverture
# ── Raccourcis ──────────────────────────────────────────────────────────────
def test_shortcuts_implemented():
keys = _slice("// ── v7.61 : raccourcis du menu contextuel de bloc ──",
"document.addEventListener('keydown', function (e) {\n if (!(e.ctrlKey || e.metaKey)")
combos = {
"k === 'j'": "Ctrl+J (Ask AI)",
"k === 'h'": "Ctrl+⇧+H (dernière couleur)",
"k === 'm'": "Ctrl+⇧+M (commenter)",
"k === 'x'": "Ctrl+⇧+Alt+X (suggest edits)",
"k === 'l'": "Alt+⇧+L (copier le lien)",
"k === 'p'": "Ctrl+⇧+P (déplacer vers…)",
"k === 'd'": "Ctrl+D (dupliquer)",
"k === 'delete'": "Del (supprimer, menu ouvert uniquement)",
"k === 'escape'": "Échap (fermer)",
}
for token, label in combos.items():
assert token in keys, label
# Del n'agit jamais pendant la saisie
assert "tgt.isContentEditable" in keys
# le libellé affiché dans le menu correspond aux combinaisons réelles
shortcuts = _slice("const _BM_SHORTCUTS=", ";")
for hint in ("Alt+⇧+L", "Ctrl+D", "Ctrl+⇧+P", "Del", "Ctrl+⇧+M",
"Ctrl+⇧+Alt+X", "Ctrl+J", "Ctrl+⇧+H"):
assert hint in shortcuts, hint
# ── Comportement réel : conversion en colonnes (node) ───────────────────────
def test_turn_into_columns_creates_children_for_real():
node = shutil.which("node")
if not node:
pytest.skip("node absent : test comportemental ignoré")
types = _slice("const _BLOCK_TURN_TYPES=[", "];")
method = _slice("turnInto(idx,type", "setBlockColor(idx,key,val){")
script = f"""
function _rtSync(){{}}
function _selClear(){{}}
var _n=0; function genId(){{return 'g'+(++_n);}}
{types}];
const E = {{
blocks:[{{id:'b1',type:'paragraph',content:'Titre'}}],
_blockTargets(i){{return [i];}}, sync(){{}}, pushHistory(){{}}, dirty:false,
autoSave(){{}}, render(){{}}, getEl(){{return null;}}, _pickSyncedBlock(){{}},
{method}
}};
E.turnInto(0,'columns',3);
console.log(JSON.stringify({{type:E.blocks[0].type, kids:E.blocks[0].children.length}}));
"""
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
timeout=30, encoding="utf-8")
assert out.returncode == 0, out.stderr
assert json.loads(out.stdout) == {"type": "columns", "kids": 3}
# ── Styles ──────────────────────────────────────────────────────────────────
def test_menu_styles():
for sel in (".block-menu .bm-group {", ".block-menu .bm-foot {",
".block-menu .bm-check {", ".block-menu .bm-dot {",
".block-menu-sub { min-width: 230px; max-height: 60vh;"):
assert sel in CSS, sel