Add an Onglets (Tabs) widget
New widget where each tab is a real "conteneur" element posed as a child (see screens/elements/add_tab.py) — this reuses everything that already exists for a normal container (adding a Répéteur/Conteneur/etc. inside via "Ajouter DANS ce conteneur", renaming to change the tab's visible label, deleting via the standard trash icon) instead of inventing a separate storage format for tabs. The widget's own properties panel gets a dedicated "Onglets" section to add a tab, rename one, jump to its content, or delete it. Rendering (render_onglets.py) builds a tab bar + one panel per tab, switched client-side (forgeShowTab, in both screen_edit.html and play.html) with only one panel visible at a time. Distinct from the existing "activer_onglet" flow action (2.3, manual show-one/hide-siblings) — that stays available for custom show/hide wiring; this widget is the turnkey version with tab management built in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2b2619d1c4
commit
17fa4cf087
@@ -6,4 +6,5 @@ from . import (
|
|||||||
element_delete,
|
element_delete,
|
||||||
element_duplicate,
|
element_duplicate,
|
||||||
element_rename,
|
element_rename,
|
||||||
|
element_tab_add,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from flask import redirect, url_for, abort
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/game/<slug>/elements/<int:element_id>/tabs/add", methods=["POST"])
|
||||||
|
def element_tab_add(slug, element_id):
|
||||||
|
onglets = screens.get_element(slug, element_id)
|
||||||
|
if not onglets:
|
||||||
|
abort(404)
|
||||||
|
screens.add_tab(slug, element_id, onglets["screen_id"])
|
||||||
|
return redirect(url_for("screen_edit", slug=slug, screen_id=onglets["screen_id"], selected=element_id))
|
||||||
@@ -73,6 +73,7 @@ from .screens_repo.set_screen_aspect import set_screen_aspect
|
|||||||
from .elements.list_elements import list_elements
|
from .elements.list_elements import list_elements
|
||||||
from .elements.get_element import get_element
|
from .elements.get_element import get_element
|
||||||
from .elements.add_element import add_element
|
from .elements.add_element import add_element
|
||||||
|
from .elements.add_tab import add_tab
|
||||||
from .elements.update_element_geometry import update_element_geometry
|
from .elements.update_element_geometry import update_element_geometry
|
||||||
from .elements.save_element_controls import save_element_controls
|
from .elements.save_element_controls import save_element_controls
|
||||||
from .elements.delete_element import delete_element
|
from .elements.delete_element import delete_element
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import db
|
||||||
|
|
||||||
|
from .add_element import add_element
|
||||||
|
from .rename_element import rename_element
|
||||||
|
|
||||||
|
|
||||||
|
def add_tab(slug, onglets_id, screen_id):
|
||||||
|
"""Ajoute un nouvel onglet à un élément "onglets" : un simple conteneur
|
||||||
|
posé comme enfant (voir render_onglets.py), nommé "Onglet N" par défaut
|
||||||
|
(N = son rang) — ce nom est directement ce qui s'affiche sur l'onglet,
|
||||||
|
modifiable ensuite comme celui de n'importe quel élément."""
|
||||||
|
conn = db.connect(slug)
|
||||||
|
count = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS c FROM _screen_elements WHERE parent_id = ?", (onglets_id,)
|
||||||
|
).fetchone()["c"]
|
||||||
|
conn.close()
|
||||||
|
tab_id = add_element(slug, screen_id, "conteneur", parent_id=onglets_id)
|
||||||
|
rename_element(slug, tab_id, f"Onglet {count + 1}")
|
||||||
|
return tab_id
|
||||||
@@ -13,6 +13,7 @@ from .render_checkbox_or_radio import _render_checkbox_or_radio
|
|||||||
from .render_repeater import _render_repeater
|
from .render_repeater import _render_repeater
|
||||||
from .render_jauge import _render_jauge
|
from .render_jauge import _render_jauge
|
||||||
from .render_overlay import _render_overlay
|
from .render_overlay import _render_overlay
|
||||||
|
from .render_onglets import _render_onglets
|
||||||
|
|
||||||
|
|
||||||
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
|
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
|
||||||
@@ -43,6 +44,8 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
|||||||
return _render_jauge(el, meta, slug, children_map, ctx, parent_flex_direction)
|
return _render_jauge(el, meta, slug, children_map, ctx, parent_flex_direction)
|
||||||
if special == "overlay":
|
if special == "overlay":
|
||||||
return _render_overlay(el, meta, slug, children_map, ctx, parent_flex_direction)
|
return _render_overlay(el, meta, slug, children_map, ctx, parent_flex_direction)
|
||||||
|
if special == "onglets":
|
||||||
|
return _render_onglets(el, meta, slug, children_map, ctx, parent_flex_direction)
|
||||||
|
|
||||||
tag = meta.get("tag")
|
tag = meta.get("tag")
|
||||||
if tag is None: # widget à balise dynamique (ex: "titre" -> h1/h2/h3)
|
if tag is None: # widget à balise dynamique (ex: "titre" -> h1/h2/h3)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import html as html_lib
|
||||||
|
|
||||||
|
from .visible_attrs import _visible_attrs
|
||||||
|
from .style_string import _style_string
|
||||||
|
from .parent_flex_direction import _parent_flex_direction
|
||||||
|
from .auto_id import _auto_id
|
||||||
|
from .attr_string import _attr_string
|
||||||
|
|
||||||
|
|
||||||
|
def _render_onglets(el, meta, slug, children_map=None, ctx=None, parent_flex_direction=None):
|
||||||
|
"""Élément à onglets : chaque onglet est un vrai conteneur posé comme
|
||||||
|
enfant (voir screens/elements/add_tab.py) — ça réutilise tel quel tout
|
||||||
|
ce qui existe déjà pour un conteneur normal (ajouter du contenu dedans
|
||||||
|
via "Ajouter DANS ce conteneur", le renommer pour changer le libellé
|
||||||
|
affiché sur l'onglet, le supprimer via la poubelle standard) plutôt que
|
||||||
|
d'inventer un stockage séparé pour les onglets. Un seul panneau visible
|
||||||
|
à la fois, basculé par un petit script embarqué qui marche aussi bien
|
||||||
|
dans l'éditeur qu'en jeu (voir forgeShowTab dans screen_edit.html et
|
||||||
|
play.html)."""
|
||||||
|
from .render_element_html import render_element_html
|
||||||
|
|
||||||
|
tabs = (children_map or {}).get(el["id"], [])
|
||||||
|
attrs = _visible_attrs(el, meta, ctx)
|
||||||
|
style = _style_string(el, " display:flex; flex-direction:column;", parent_flex_direction)
|
||||||
|
group_id = _auto_id(el)
|
||||||
|
|
||||||
|
if not tabs:
|
||||||
|
return (
|
||||||
|
f'<div {_attr_string(attrs, style)}>'
|
||||||
|
f'<em style="opacity:.6;">Onglets — ajoute au moins un onglet dans ses réglages.</em></div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
flex_dir = _parent_flex_direction(el)
|
||||||
|
bar_items = []
|
||||||
|
panels = []
|
||||||
|
for i, tab in enumerate(tabs):
|
||||||
|
label = tab.get("name") or f"Onglet {i + 1}"
|
||||||
|
active = i == 0
|
||||||
|
tab_html = render_element_html(tab, slug, children_map, ctx, parent_flex_direction=flex_dir)
|
||||||
|
target_id = f"tabpanel-{tab['id']}"
|
||||||
|
bar_items.append(
|
||||||
|
f'<button type="button" class="forgeTabBtn{" active" if active else ""}" '
|
||||||
|
f'data-tab-btn="{group_id}" data-tab-target="{target_id}" '
|
||||||
|
f'onclick="forgeShowTab(this)">{html_lib.escape(str(label))}</button>'
|
||||||
|
)
|
||||||
|
hidden_style = "" if active else ' style="display:none;"'
|
||||||
|
panels.append(
|
||||||
|
f'<div class="forgeTabPanel" id="{target_id}" data-tab-group="{group_id}"{hidden_style}>'
|
||||||
|
f'{tab_html}</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f'<div {_attr_string(attrs, style)}>'
|
||||||
|
f'<div class="forgeTabBar">{"".join(bar_items)}</div>'
|
||||||
|
f'<div class="forgeTabPanels" style="flex:1 1 auto; min-height:0;">{"".join(panels)}</div>'
|
||||||
|
f"</div>"
|
||||||
|
)
|
||||||
@@ -277,4 +277,14 @@ WIDGETS = {
|
|||||||
c_slider("arrondi", "Arrondi des coins de la boîte", "attr:_arrondi", unit="", min=0, max=40, default=12, group="Apparence"),
|
c_slider("arrondi", "Arrondi des coins de la boîte", "attr:_arrondi", unit="", min=0, max=40, default=12, group="Apparence"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"onglets": {
|
||||||
|
# Chaque onglet est un vrai élément "conteneur" posé comme enfant
|
||||||
|
# (voir screens/elements/add_tab.py et render_onglets.py) — pas de
|
||||||
|
# gestion dédiée ici : ajouter/renommer/supprimer un onglet se fait
|
||||||
|
# via le bloc "Onglets" du panneau de propriétés (screen_edit.html),
|
||||||
|
# qui réutilise les routes existantes (element_rename, element_delete).
|
||||||
|
"label": "Onglets", "icon": "🗂️", "tag": "div", "has_content": False, "special_render": "onglets",
|
||||||
|
"w": 60, "h": 40, "default_content": "",
|
||||||
|
"controls": [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,17 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
|||||||
}
|
}
|
||||||
.contextMenu button:hover{ background:var(--panel2); }
|
.contextMenu button:hover{ background:var(--panel2); }
|
||||||
|
|
||||||
|
/* Élément à onglets (voir render_onglets.py) */
|
||||||
|
.forgeTabBar{ display:flex; flex-wrap:wrap; gap:2px; border-bottom:1px solid var(--border); }
|
||||||
|
.forgeTabBtn{
|
||||||
|
background:none; border:none; border-bottom:2px solid transparent; color:var(--text-dim);
|
||||||
|
padding:8px 14px; font-size:14px; font-family:inherit; cursor:pointer; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.forgeTabBtn:hover{ color:var(--text); }
|
||||||
|
.forgeTabBtn.active{ color:var(--text); border-bottom-color:var(--accent); font-weight:600; }
|
||||||
|
.forgeTabPanels{ min-width:0; }
|
||||||
|
.forgeTabPanel{ padding-top:10px; }
|
||||||
|
|
||||||
.propGroup{ border:1px solid var(--border); border-radius:8px; margin-bottom:8px; background:var(--panel); }
|
.propGroup{ border:1px solid var(--border); border-radius:8px; margin-bottom:8px; background:var(--panel); }
|
||||||
.propGroup > summary{
|
.propGroup > summary{
|
||||||
list-style:none; cursor:pointer; padding:9px 12px; font-size:13px; color:var(--accent);
|
list-style:none; cursor:pointer; padding:9px 12px; font-size:13px; color:var(--accent);
|
||||||
|
|||||||
@@ -74,6 +74,20 @@
|
|||||||
let gameData = {{ payload_json|safe }};
|
let gameData = {{ payload_json|safe }};
|
||||||
let screensData = gameData.screens;
|
let screensData = gameData.screens;
|
||||||
|
|
||||||
|
// Élément à onglets (voir render_onglets.py) : bascule le panneau visible
|
||||||
|
// sans aller-retour serveur — même fonction que dans screen_edit.html,
|
||||||
|
// pour un comportement identique éditeur/jeu.
|
||||||
|
function forgeShowTab(btn) {
|
||||||
|
var groupId = btn.dataset.tabBtn;
|
||||||
|
var targetId = btn.dataset.tabTarget;
|
||||||
|
document.querySelectorAll('[data-tab-btn="' + groupId + '"]').forEach(function (b) {
|
||||||
|
b.classList.toggle('active', b === btn);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-tab-group="' + groupId + '"]').forEach(function (p) {
|
||||||
|
p.style.display = (p.id === targetId) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showScreen(id) {
|
function showScreen(id) {
|
||||||
document.querySelectorAll('.playScreen').forEach(s => s.style.display = 'none');
|
document.querySelectorAll('.playScreen').forEach(s => s.style.display = 'none');
|
||||||
const el = document.querySelector('.playScreen[data-screen-id="' + id + '"]');
|
const el = document.querySelector('.playScreen[data-screen-id="' + id + '"]');
|
||||||
|
|||||||
@@ -186,6 +186,38 @@
|
|||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if el.widget == 'onglets' %}
|
||||||
|
{% set tabs = children_of.get(el.id, []) %}
|
||||||
|
<details class="propGroup">
|
||||||
|
<summary>🗂️ Onglets{{ ' (' ~ tabs|length ~ ')' if tabs else '' }}</summary>
|
||||||
|
<div class="propGroupBody">
|
||||||
|
{% if tabs %}
|
||||||
|
<div class="list">
|
||||||
|
{% for tab in tabs %}
|
||||||
|
<div class="listRow" style="display:flex; flex-direction:column; gap:6px;">
|
||||||
|
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=tab.id) }}" class="fieldRow" style="align-items:center; margin:0;">
|
||||||
|
<input type="text" name="name" value="{{ tab.name or ('Onglet ' ~ loop.index) }}" placeholder="Nom de l'onglet" style="flex:1;">
|
||||||
|
<button type="submit" class="smallBtn">Renommer</button>
|
||||||
|
</form>
|
||||||
|
<div class="fieldRow" style="align-items:center; margin:0;">
|
||||||
|
<a class="smallBtn" style="flex:1; text-align:center;" href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=tab.id) }}">📥 Contenu de l'onglet</a>
|
||||||
|
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=tab.id) }}" onsubmit="return confirm('Supprimer cet onglet et tout son contenu ?');">
|
||||||
|
<button type="submit" class="smallBtn danger">🗑️</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="hint">Aucun onglet pour l'instant.</p>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="{{ url_for('element_tab_add', slug=game.slug, element_id=el.id) }}" style="margin-top:8px;">
|
||||||
|
<button type="submit" class="smallBtn primary" style="width:100%;">➕ Ajouter un onglet</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
|
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
|
||||||
{% set size_ctrls = {} %}
|
{% set size_ctrls = {} %}
|
||||||
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
|
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
|
||||||
@@ -675,6 +707,22 @@ function applySelectionHighlight(id) {
|
|||||||
if (node) node.classList.add('elementHighlighted');
|
if (node) node.classList.add('elementHighlighted');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Élément à onglets : bascule d'onglet côté client ----------
|
||||||
|
//
|
||||||
|
// Un seul panneau visible à la fois (voir render_onglets.py) — basculé ici
|
||||||
|
// plutôt que par un aller-retour serveur, pour un rendu instantané aussi
|
||||||
|
// bien dans l'éditeur qu'en jeu (voir la même fonction dans play.html).
|
||||||
|
function forgeShowTab(btn) {
|
||||||
|
var groupId = btn.dataset.tabBtn;
|
||||||
|
var targetId = btn.dataset.tabTarget;
|
||||||
|
document.querySelectorAll('[data-tab-btn="' + groupId + '"]').forEach(function (b) {
|
||||||
|
b.classList.toggle('active', b === btn);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-tab-group="' + groupId + '"]').forEach(function (p) {
|
||||||
|
p.style.display = (p.id === targetId) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!window.__forgeTreeToggleBound) {
|
if (!window.__forgeTreeToggleBound) {
|
||||||
window.__forgeTreeToggleBound = true;
|
window.__forgeTreeToggleBound = true;
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Tests du widget Onglets — ajouter/renommer/supprimer un onglet, contenu
|
||||||
|
par onglet (un vrai conteneur enfant, voir screens/elements/add_tab.py).
|
||||||
|
|
||||||
|
À ne pas confondre avec tests/test_onglets.py, qui teste l'action de flux
|
||||||
|
existante "activer_onglet" (2.3, montrer un élément et masquer ses frères) —
|
||||||
|
un mécanisme manuel plus bas niveau, toujours disponible séparément, que ce
|
||||||
|
widget ne remplace pas."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _create_screen(client, slug, name="Accueil"):
|
||||||
|
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||||
|
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_onglets(client, slug, screen_id):
|
||||||
|
resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": "onglets"}, follow_redirects=False)
|
||||||
|
return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def _tab_rename_ids(html):
|
||||||
|
"""Ids des formulaires de renommage DANS le bloc "Onglets" du panneau de
|
||||||
|
propriétés — pas le premier /elements/<id>/rename de la page, qui est le
|
||||||
|
champ "Nommer" générique de l'élément actuellement sélectionné (pas
|
||||||
|
forcément un onglet, ex. l'élément Onglets lui-même)."""
|
||||||
|
block = html.split("Onglets (", 1)[-1]
|
||||||
|
return [int(x) for x in re.findall(r"/elements/(\d+)/rename", block)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_tabs_widget_has_no_tabs_yet(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
_create_onglets(client, game, screen_id)
|
||||||
|
html = client.get(f"/game/{game}/play").data.decode()
|
||||||
|
assert "forgeTabBtn" not in html
|
||||||
|
assert "ajoute au moins un onglet" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_tab_creates_a_named_child_container(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
|
||||||
|
resp = client.post(f"/game/{game}/elements/{onglets_id}/tabs/add", follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert f"selected={onglets_id}" in resp.headers["Location"] # reste sur le widget Onglets, pas le nouvel onglet
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert ">Onglet 1<" in html # nom par défaut, affiché tel quel comme libellé de l'onglet
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_tab_is_named_onglet_2_and_starts_hidden(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert ">Onglet 1<" in html
|
||||||
|
assert ">Onglet 2<" in html
|
||||||
|
# Seul le premier onglet est visible par défaut (le second est en display:none).
|
||||||
|
assert 'style="display:none;"' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_renaming_a_tab_changes_the_visible_label(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={onglets_id}").data.decode()
|
||||||
|
tab_id = _tab_rename_ids(html)[0]
|
||||||
|
|
||||||
|
client.post(f"/game/{game}/elements/{tab_id}/rename", data={"name": "Statistiques"})
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert ">Statistiques<" in html
|
||||||
|
assert ">Onglet 1<" not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_deleting_a_tab_removes_it_and_reselects_the_tabs_widget(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add", follow_redirects=False)
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={onglets_id}").data.decode()
|
||||||
|
tab_ids = _tab_rename_ids(html)
|
||||||
|
assert len(tab_ids) == 2
|
||||||
|
|
||||||
|
resp = client.post(f"/game/{game}/elements/{tab_ids[1]}/delete", follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert f"selected={onglets_id}" in resp.headers["Location"]
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert html.count("forgeTabBtn") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tab_content_holds_a_real_container_children_can_be_added_to(client, game):
|
||||||
|
"""Régression : chaque onglet doit se comporter comme un vrai conteneur
|
||||||
|
— on doit pouvoir y poser un Répéteur/Conteneur/etc. via "Ajouter DANS
|
||||||
|
ce conteneur", exactement comme pour n'importe quel autre conteneur."""
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={onglets_id}").data.decode()
|
||||||
|
tab_id = _tab_rename_ids(html)[0]
|
||||||
|
|
||||||
|
resp = client.post(f"/game/{game}/elements/{tab_id}/children/add", data={"widget": "titre"}, follow_redirects=False)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
child_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
||||||
|
client.post(f"/game/{game}/elements/{child_id}/save", data={"ctrl_content": "Bienvenue dans cet onglet"})
|
||||||
|
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert "Bienvenue dans cet onglet" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_deleting_the_tabs_widget_cascades_to_its_tabs(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
onglets_id = _create_onglets(client, game, screen_id)
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/tabs/add")
|
||||||
|
|
||||||
|
client.post(f"/game/{game}/elements/{onglets_id}/delete")
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
assert "forgeTabBtn" not in html
|
||||||
|
assert ">Onglet 1<" not in html
|
||||||
Reference in New Issue
Block a user