- Toolbar de sélection sous le surlignage : ligne de style (T + type de bloc dynamique → sous-menu Turn into), A/B/I/U/T barré/Tx, lien/surlignage/ code/équation/…, Comment + Ask AI, section grise Skills (4 actions) et pied « Edit with AI » (Alt+⇧+E) ; positionnée sous la sélection (au-dessus si manque de place), fermée au clic hors sélection - « … » → menu contextuel de bloc existant (recherche « Search actions… », groupe Text, 10 actions + raccourcis, pied auteur/heure/taille) - Info-bulle claire à droite des éléments du menu : data-tooltip-html + classe .tooltip-light dans le tooltip global (app.js) - Persistance du formatage inline : sérialiseur gtMd() (DOM formaté → source markdown) branché sur sync() ; mdEsc() rend __…__ (u), ==…== (mark) et $$…$$ (équation inline KaTeX, data-tex conservé) - Fixes racine : askAI(idx) écrasé par askAI() → askAIBlock(idx) (menu de bloc, Ctrl+J, Alt+⇧+E) ; ed.__x (Alpine v2) inexistant sous v3 → _xed() via _x_dataStack[0] (showFmt/hideCommentBtn/mentions ne s'exécutaient jamais) ; ancre de sélection nœud élément gérée ; checkMd() appelé avec un id au lieu de l'index (exception à chaque barre d'espace) - Tests : tests/test_v762_selection_toolbar.py (roundtrip gtMd/mdEsc sous node) + e2e/v762_selection_toolbar.spec.js (structure, sous-menus, géométrie, persistance bold/highlight après reload) ; suite complète 1368 passed / 0 failed, ruff OK, OpenAPI régénérée 7.62.0 (525 chemins)
156 lines
7.2 KiB
Python
156 lines
7.2 KiB
Python
"""7.62.0 — menu contextuel au highlight de texte (toolbar façon Notion)."""
|
||
|
||
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")
|
||
TPL = (ROOT / "app" / "templates" / "_page_editor_content.html").read_text(encoding="utf-8")
|
||
CSS = (ROOT / "static" / "css" / "app.css").read_text(encoding="utf-8")
|
||
APPJS = (ROOT / "static" / "js" / "app.js").read_text(encoding="utf-8")
|
||
|
||
|
||
def _slice(src: str, start: str, end: str) -> str:
|
||
i = src.index(start)
|
||
return src[i:src.index(end, i)]
|
||
|
||
|
||
# ── Markup : les 4 lignes + Skills + pied, dans l'ordre de la description ───
|
||
|
||
def test_toolbar_markup_matches_spec():
|
||
chunk = _slice(TPL, 'class="format-toolbar"', "Copy Link Toast")
|
||
order = [
|
||
"fmtStyle($event)", # ligne 1 : Normal Text ›
|
||
"fmtApply('bold')", "fmtApply('italic')", "fmtApply('underline')",
|
||
"fmtApply('strikeThrough')", "fmtCode()", # ligne 2 : A B I U T Tx
|
||
"fmtLink()", "fmtHighlight()", "fmtCodeBlock()", "fmtEquation()",
|
||
"fmtMore($event)", # ligne 3 : 🔗 S </> √x …
|
||
"fmtComment()", "fmtAskAI()", # ligne 4 : 💬 ✨
|
||
"ft-group", "Improve writing", "Proofread", "Explain", "Reformat",
|
||
"Edit with AI", "Alt+⇧+E", # pied de page
|
||
]
|
||
positions = [chunk.index(o) for o in order]
|
||
assert positions == sorted(positions), positions
|
||
# ligne 1 dynamique (type du bloc) + chevron de sous-menu
|
||
assert "fmtStyleName()" in chunk and "ft-caret" in chunk
|
||
# info-bulle HTML (exemple en italique + description)
|
||
assert "data-tooltip-html=" in chunk
|
||
# icônes réellement définies dans le jeu d'icônes
|
||
assert 'fd_icon("message-square",15)' in chunk
|
||
assert 'fd_icon("sparkles",15)' in chunk and 'fd_icon("link",15)' in chunk
|
||
|
||
|
||
# ── Actions : câblées vers de vraies méthodes, sous-menus réutilisés ────────
|
||
|
||
def test_actions_are_wired_to_real_methods():
|
||
for m in ("fmtStyle(ev)", "fmtColor(ev)", "fmtApply(f)", "fmtCode()",
|
||
"fmtHighlight()", "fmtCodeBlock()", "fmtLink()", "fmtEquation()",
|
||
"fmtMore(ev)", "fmtComment()", "fmtAskAI()", "fmtSkill(tpl)",
|
||
"fmtEditAI()", "askAIBlock(idx)"):
|
||
assert m in SRC, m
|
||
# pas de doublon de menu : on rouvre les sous-menus/menus déjà existants
|
||
assert "_blockTurnMenuOpen(p.x,p.y,bid)" in SRC
|
||
assert "_blockColorMenuOpen(p.x,p.y,bid)" in SRC
|
||
assert "blockMenuOpen(" in SRC
|
||
# sous-menu ouvert depuis la toolbar : fermeture au survol sortant
|
||
assert "function _subHoverClose()" in SRC
|
||
# Ctrl+J et Alt+⇧+E ouvrent l'IA du BLOC (l'ancien askAI(idx) était écrasé
|
||
# par askAI() défini plus bas dans le même objet)
|
||
assert "E.askAIBlock(i1)" in SRC
|
||
assert "k === 'e'" in SRC
|
||
assert "askAI(idx)" not in SRC
|
||
|
||
|
||
# ── Persistance du formatage : sync() relit le DOM formaté en markdown ─────
|
||
|
||
def test_formatting_survives_sync():
|
||
assert "function gtMd(" in SRC
|
||
assert "gtMd(el)" in SRC and "gtMd(che)" in SRC # sync() + enfants toggle/columns
|
||
for tok in ("'**'+inner+'**'", "'*'+inner+'*'", "'__'+inner+'__'",
|
||
"'~~'+inner+'~~'", "'`'+inner+'`'", "'=='+inner+'=='"):
|
||
assert tok in SRC, tok
|
||
# mdEsc rend les nouveaux marqueurs (souligné, surlignage, équation inline)
|
||
for re_ in ("/__([^_]+)__/g", "/==([^=]+)==/g", "/\\$\\$([^$]+)\\$\\$/g"):
|
||
assert re_ in SRC, re_
|
||
# les chips wiki gardent leur token brut
|
||
assert "fd-wiki-chip" in SRC
|
||
|
||
|
||
# ── Placement & fermeture ──────────────────────────────────────────────────
|
||
|
||
def test_menu_position_and_close():
|
||
assert "r.bottom+8" in SRC # sous la sélection (bas de l'écran : au-dessus)
|
||
assert "d.fmt.open=false;d._sel=null" in SRC # clic ailleurs → fermeture
|
||
assert "closest('.format-toolbar')" in SRC # clic dans la toolbar ≠ clic ailleurs
|
||
assert "@mousedown.prevent" in TPL # la sélection survit au clic
|
||
|
||
|
||
# ── Styles ────────────────────────────────────────────────────────────────
|
||
|
||
def test_menu_styles():
|
||
for sel in (".format-toolbar .ft-row {", ".format-toolbar .ft-item {",
|
||
".format-toolbar .ft-style {", ".format-toolbar .ft-group {",
|
||
".format-toolbar .ft-sep {", ".format-toolbar .ft-foot {",
|
||
".tooltip.tooltip-light {"):
|
||
assert sel in CSS, sel
|
||
# l'info-bulle claire s'applique aux éléments du menu de sélection
|
||
assert "data-tooltip-html" in APPJS and "tooltip-light" in APPJS
|
||
|
||
|
||
# ── Comportement réel : gtMd (DOM → source markdown) + mdEsc, sous node ────
|
||
|
||
def test_gt_md_and_md_esc_roundtrip_for_real():
|
||
node = shutil.which("node")
|
||
if not node:
|
||
pytest.skip("node absent : test comportemental ignoré")
|
||
gtmd = _slice(SRC, "function gtMd(", " // Enveloppe/désenveloppe")
|
||
mdesc = _slice(SRC, "function mdEsc(", " // v5.11.0: labels of [[fdpage:ID]]")
|
||
script = f"""
|
||
var window = {{}}; // gtMd fait window.gtMd=…
|
||
function gtTok(e){{ return e.__raw || ''; }}
|
||
function fdDateLabel(iso){{ return iso; }}
|
||
{gtmd}
|
||
{mdesc}
|
||
// DOM minimal (gtMd ne lit que childNodes / tagName / classList / attributs)
|
||
function T(s){{ return {{nodeType:3, nodeValue:s}}; }}
|
||
function E(tag, attrs, kids){{
|
||
attrs = attrs || {{}};
|
||
const cls = String(attrs['class'] || '').split(/\\s+/);
|
||
return {{nodeType:1, tagName:tag, childNodes:kids || [],
|
||
classList:{{contains:c=>cls.indexOf(c) >= 0}},
|
||
getAttribute:k=>(k in attrs ? attrs[k] : null),
|
||
hasAttribute:k=>(k in attrs)}};
|
||
}}
|
||
// querySelector truthy → chemin « formaté » de gtMd (gtTok jamais appelé)
|
||
const root = E('DIV', {{'class':''}}, [
|
||
E('B', {{}}, [T('gras')]),
|
||
E('EM', {{}}, [T('italique')]),
|
||
E('U', {{}}, [T('souligne')]),
|
||
E('S', {{}}, [T('barre')]),
|
||
E('CODE', {{}}, [T('code')]),
|
||
E('MARK', {{}}, [T('surligne')]),
|
||
E('A', {{'href':'https://example.com/x'}}, [T('lien')]),
|
||
E('SPAN', {{'class':'fd-wiki-chip', 'data-token':'[[fdpage:42]]'}}),
|
||
E('SPAN', {{'class':'fd-math-inline', 'data-tex':'x^2'}}),
|
||
T(' fin')
|
||
]);
|
||
root.querySelector = () => ({{}}); // force le chemin formaté
|
||
const md = gtMd(root);
|
||
const html = mdEsc(md);
|
||
console.log(JSON.stringify({{md:md, html:html}}));
|
||
"""
|
||
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
|
||
timeout=30, encoding="utf-8")
|
||
assert out.returncode == 0, out.stderr
|
||
data = json.loads(out.stdout)
|
||
assert data["md"] == ("**gras***italique*__souligne__~~barre~~`code`==surligne=="
|
||
"[lien](https://example.com/x)[[fdpage:42]]$$x^2$$ fin"), data["md"]
|
||
for token in ("<strong>gras</strong>", "<u>souligne</u>", "<mark>surligne</mark>",
|
||
"<code>code</code>", 'href="https://example.com/x"',
|
||
'class="fd-wiki-chip"', 'data-tex="x^2"'):
|
||
assert token in data["html"], token
|