Files
flowdeck/tests/test_v762_selection_toolbar.py
T
bruno d58c321c07
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
feat: réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection (v7.67.0)
Tiroir Comments
- Section « Reactions » au-dessus des fils : émoji, compte (si >1) et texte
  ancré lu dans le DOM (reactionList() / _anchorText(), offsets source hors
  pastilles) ; l'état vide ne s'affiche plus s'il reste des réactions.
- Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au
  survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets
  intacts). Deux entrées → removeReaction() → POST toggle → loadReactions().

Menu de sélection (maquette ASCII)
- Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à
  droite (.ft-row-split) ; la réaction sort de la ligne 3.
- Entête Skills avec ≡ à droite (::after), liste Skills scrollable.
- ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied.
- Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé.

Fix racine (flake de gate)
- _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne
  lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans
  son except Exception → repli silencieux → AssertionError.
- fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) :
  zéro vrai DNS dans les tests OG.

Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ;
ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression
A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081
(image flowdeck:v7.67.0 rebuild + redéployée).
2026-10-08 14:25:48 -04:00

172 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()", "fmtReact()", # ligne 4 : 💬 Comment | 🙂 réaction
"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("link",15)' in chunk
def test_toolbar_row4_matches_reference_layout():
"""v7.67 — ligne Comment à gauche, réaction à droite, Skills avec ≡."""
chunk = _slice(TPL, 'class="format-toolbar"', "Copy Link Toast")
# ligne 4 = une seule rangée scannée, boutons aux 2 extrémités
assert 'ft-row-split' in chunk
assert '<span>Comment</span>' in chunk # libellé, pas juste l'icône
assert chunk.index("fmtComment()") < chunk.index("fmtReact()")
# l'✨ Ask AI a sauté : doublon exact du « Edit with AI » du pied
assert '"fmtAskAI()"' not in chunk
# entête Skills (≡ en CSS) + liste scrollable
assert 'ft-skills-head' in chunk and 'ft-skills' in chunk
for sel in (".format-toolbar .ft-row-split {", ".format-toolbar .ft-comment {",
".format-toolbar .ft-skills-head::after {", ".format-toolbar .ft-skills {"):
assert sel in CSS, sel
# ── 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