feat(diagram): das Knoten-Fenster löst den nativen Tooltip überall ab

Das Fenster aus D52 war für Touch gebaut — damit standen zwei Darstellungen
desselben Inhalts nebeneinander, und ausgerechnet die schlechtere bekam der
Zeiger. Jetzt zeigt es überall: am Zeiger nach 350 ms beim Überfahren, bei
Tastaturfokus sofort, auf Touch unverändert beim Tipp.

Was ein `title` prinzipiell nicht kann:
- keine Absätze (die Beschreibungen im Plan brechen bei ~76 Zeichen, das sah
  ausgefranst aus),
- keine Linie (der Trennstrich war aus 24 `─` GEMALT, D40-Nachtrag),
- nichts bei Tastaturfokus — wer mit Tab durch das Diagramm geht, sah bisher
  keine Beschreibung. Nachgemessen mit ECHTEM Tastendruck: Fokus von
  #not.line auf #not.status, Fenster folgt, 10 px unter dem Knoten, Ring am
  fokussierten Knoten.

- Inhalt zieht von `title` nach `data-tip` (sonst zeigte der Browser seinen
  eigenen zusätzlich) — dasselbe Attribut unter anderem Namen, keine
  Verdopplung der 20 kB DOM-Text.
- Die letzten drei nativen Tooltips IN Knoten mussten weichen: Das
  Warndreieck war redundant (Statusname steht in den Fakten), die Annahme
  „Größe fehlt, gilt als M" wandert in die Faktenzeile. Nachgemessen: 0 von
  161 Knoten und 0 Kinder tragen noch einen `title`.
- Absicht-Erkennung: 350 ms Verzögerung, beim Wechsel zwischen Knoten ohne
  Warten, 120 ms Nachfrist beim Verlassen (der Weg ins Fenster führt über
  einen Zwischenraum).
- `aria-hidden` am Fenster: Der Inhalt steht schon im aria-label, sonst würde
  er doppelt vorgelesen; × und ↗ deshalb `tabindex="-1"`.

Werkzeuggrenze notiert: `element.focus()` feuert ohne Fensterfokus keine
Fokus-Ereignisse (`document.hasFocus()` false) — der erste Prüflauf zeigte
fälschlich „öffnet nichts".

SPEC §9 + D57; Plan: #ed.tipwindow [x] (161 Knoten, 0 Warnungen).
293 Tests grün, 5 Snapshots/Zusicherungen auf `data-tip` nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 12:07:00 +02:00
co-authored by Claude Opus 5
parent f0fe877d45
commit 3fbc772d81
12 changed files with 176 additions and 29 deletions
+71
View File
@@ -3974,3 +3974,74 @@ Rechnung wie D17-Nachtrag 5.** Nachgemessen statt geschätzt:
Ein `overflow` an der Kopfzeile bleibt weiterhin ausgeschlossen — es klippt die Ein `overflow` an der Kopfzeile bleibt weiterhin ausgeschlossen — es klippt die
beiden Aufklapp-Menüs (D50). beiden Aufklapp-Menüs (D50).
## D57 — Das Knoten-Fenster löst den nativen Tooltip überall ab
Das Fenster aus D52 war für Touch gebaut: Ohne Zeiger gibt es keinen `title`,
und die Beschreibungen (D40) wären dort gar nicht zu sehen gewesen. Damit
standen zwei Darstellungen desselben Inhalts nebeneinander — und ausgerechnet
die schlechtere bekam der Zeiger. Jetzt zeigt das Fenster überall: am Zeiger
nach kurzer Verzögerung beim Überfahren, bei **Tastaturfokus** sofort, auf
Touch unverändert beim einfachen Tipp.
**Was der `title` nicht konnte, und zwar prinzipiell:**
- **Keine Absätze.** Die Beschreibungen im mitgelieferten Plan sind bei ~76
Zeichen umgebrochen; ein `title` zeigt genau diese harten Umbrüche, in einem
schmalen Fenster sah der Text ausgefranst aus. Das Fenster bricht um, wie es
die Breite hergibt (D52 hatte das für Touch schon gelöst).
- **Keine Linie.** Der Trennstrich zwischen Beschreibung und Kurz-Fakten musste
aus 24 `─`-Zeichen **gemalt** werden (D40-Nachtrag) — im Fenster ist er eine
echte Kante, und ohne Beschreibung entfällt er ganz.
- **Nichts bei Tastaturfokus.** Kein Browser zeigt einen `title`, wenn man mit
Tab auf ein Element springt. Wer das Diagramm mit der Tastatur durchgeht, sah
bisher keine Beschreibung. Das ist der eigentliche Zugewinn und kein Beiwerk —
nachgemessen mit einem **echten** Tastendruck: Fokus wandert von `#not.line`
auf `#not.status`, das Fenster folgt mit dessen Text, 10 px unter dem Knoten,
Petrol-Ring am fokussierten Knoten.
**Der Inhalt zieht von `title` nach `data-tip` um.** Bliebe er im `title`,
zeigte der Browser seinen eigenen Tooltip **zusätzlich** — unterdrücken lässt er
sich nicht. Es ist kein zweites Attribut, sondern dasselbe unter anderem Namen;
die Sorge aus D52 („keine 20 kB DOM-Text verdoppeln") bleibt gewahrt.
**Dabei mussten die letzten drei nativen Tooltips *innerhalb* der Knoten
weichen** — sonst wären sie neben dem Fenster ein zweites Mal erschienen:
- Das **Warndreieck** (`[!]`) trug „High Risk Aufwand unklar". Das ist der
Statusname und steht ohnehin in der Faktenzeile — der `title` war redundant
und ist ersatzlos weg.
- Das **implizite M-Badge** trug die Kostenannahme „Größe fehlt, gilt als M"
(D18). Die ist nicht redundant, also wandert sie in die Faktenzeile und der
`title` entfällt. Nachgemessen: `data-tip="st_geplant · implicitSizeTooltip ·
jumpHint"`, Badge ohne Attribut.
Damit trägt im ganzen Diagramm kein Element mehr einen nativen Tooltip
(nachgemessen: 0 von 160 Knoten, 0 Kinder) — außer dem Geister-Knoten, der kein
`.node` ist.
**Absicht-Erkennung statt sofortigem Aufpoppen.** Über einen dichten Baum fährt
man hinweg, ohne etwas wissen zu wollen: 350 ms Verzögerung. Steht schon ein
Fenster offen, zeigt der nächste Knoten **ohne** Warten — wer liest, wartet
nicht noch einmal. Beim Verlassen wird 120 ms gewartet, bevor zugemacht wird:
Der Weg vom Knoten ins Fenster führt über einen Zwischenraum, in dem der Zeiger
über keinem von beiden steht. Wer im Fenster ist, hält es offen (Text lässt
sich markieren).
**Der ↗-Knopf bleibt Touch vorbehalten.** Am Zeiger ist der ganze Knoten der
Link (§6) — ein Knopf im Fenster wäre ein zweiter Weg zum selben Ziel. Ebenso
nennt die Faktenzeile nur auf Touch den langen Druck statt Alt+Klick.
**Für Screenreader ändert sich nichts, und das ist Absicht.** Das Fenster ist
`aria-hidden`: Sein ganzer Inhalt steht bereits im `aria-label` des Knotens
(SPEC §9), er würde sonst doppelt vorgelesen. Damit dürfen darin keine
fokussierbaren Elemente liegen — × und ↗ tragen deshalb `tabindex="-1"`;
erreichbar bleibt beides über den Knoten selbst und über Esc.
**Werkzeuggrenze, wieder dieselbe Sorte:** `element.focus()` setzt in einem
nicht fokussierten Automatisierungsfenster zwar `document.activeElement`, feuert
aber **keine** Fokus-Ereignisse (`document.hasFocus() === false`). Der erste
Prüflauf zeigte deshalb „Tastaturfokus öffnet nichts", obwohl die Logik stimmte;
mit synthetischem `focusin` öffnete es sofort. Erst das Fronten des Tabs samt
echtem Klick und echter Tab-Taste hat es bewiesen. Wie D25 (synthetische
`TouchEvent`s), D17-Nachtrag 4 (Bildschirmtastatur) und D53 (synthetisches
Strg+Z): Was die Umgebung stellt, stellt der Emulator nicht.
+6 -5
View File
@@ -335,11 +335,12 @@ spiegelt das `"`-Zeichen der Notation. Die Marke erscheint **nicht** im
Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke
ohne Ziel wäre Rauschen. Siehe D40. ohne Ziel wäre Rauschen. Siehe D40.
**Auf Touch-Geräten zeigt der einfache Tipp auf einen Knoten dasselbe als **Gezeigt wird das immer im eigenen Knoten-Fenster**, nicht im Tooltip des
Fenster** — ohne Zeiger gibt es keinen Tooltip, die Beschreibung wäre dort sonst Browsers: am Zeiger nach kurzer Verzögerung beim Überfahren, bei Tastaturfokus
gar nicht zu sehen. Es ist derselbe Inhalt, nur mit dem, was ein `title` nicht sofort, auf Touch beim einfachen Tipp (§6). Ein `title` kann weder Absätze noch
kann: Die Beschreibung steht als Absatz, die Kurz-Fakten dahinter durch eine eine Linie und erscheint nie beim Tastaturfokus; das Fenster kann beides. Der
echte Linie abgesetzt. Inhalt ist derselbe — die Beschreibung als Absatz, die Kurz-Fakten dahinter
durch eine echte Linie abgesetzt. Siehe D57.
- Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht - Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht
nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen
+6
View File
@@ -61,6 +61,7 @@
- [^] #ed.a11y: Accessibility: aria labels, focus order (S) - [^] #ed.a11y: Accessibility: aria labels, focus order (S)
- [^] #ed.mobile: Works on a phone (S) - [^] #ed.mobile: Works on a phone (S)
- [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip - [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip
- [x] #ed.tipwindow: The same window at the pointer and on keyboard focus (XS) %% a title has no paragraphs and never shows on focus
- [^] #ed.docs: Several documents in one editor (M) - [^] #ed.docs: Several documents in one editor (M)
- [^] #ed.docs.picker: Switcher in the editor title bar (S) - [^] #ed.docs.picker: Switcher in the editor title bar (S)
- [^] #ed.docs.url: Load a document from ?sourceUrl= (S) - [^] #ed.docs.url: Load a document from ?sourceUrl= (S)
@@ -462,6 +463,11 @@
at all. A tap opens the same content as a window instead; on a linked node at all. A tap opens the same content as a window instead; on a linked node
the link becomes a button inside it, because one tap cannot do both. the link becomes a button inside it, because one tap cannot do both.
#ed.tipwindow
A native title has no paragraphs, cannot draw a line, and never appears on
keyboard focus. The window built for touch can do all three, so it now shows
everywhere and the title is gone.
#ed.docs #ed.docs
Several notation texts side by side, kept in the browser. Each is only text Several notation texts side by side, kept in the browser. Each is only text
plus a name — no invented structure format, so this maps onto a server later plus a name — no invented structure format, so this maps onto a server later
+7 -2
View File
@@ -256,8 +256,13 @@
Es liegt auf <body> und ist `position:fixed` — in `#out` hinein würde es Es liegt auf <body> und ist `position:fixed` — in `#out` hinein würde es
dessen CSS-`zoom` erben (der Text skalierte mit) und von dessen `overflow` dessen CSS-`zoom` erben (der Text skalierte mit) und von dessen `overflow`
beschnitten (die Falle aus D50). Inhalt wird bei jedem Öffnen ersetzt. --> beschnitten (die Falle aus D50). Inhalt wird bei jedem Öffnen ersetzt. -->
<div id="nodeTip" class="nodetip" role="dialog" aria-modal="false" hidden> <!-- `aria-hidden`: Das Fenster ist eine rein visuelle Hilfe — sein ganzer Inhalt
<button type="button" class="nodetip-x" id="nodeTipClose" data-i18n-title="tipClose" data-i18n-aria="tipClose" title="Schließen" aria-label="Schließen">×</button> steht bereits im `aria-label` des Knotens (SPEC §9), ein Screenreader läse
ihn sonst doppelt. Die Knöpfe darin tragen deshalb `tabindex="-1"`
(fokussierbare Elemente in einem `aria-hidden`-Bereich wären ein Fehler);
erreichbar bleibt beides über den Knoten selbst und über Esc. Siehe D57. -->
<div id="nodeTip" class="nodetip" aria-hidden="true" hidden>
<button type="button" class="nodetip-x" id="nodeTipClose" tabindex="-1" data-i18n-title="tipClose" title="Schließen">×</button>
<div class="nodetip-body" id="nodeTipBody"></div> <div class="nodetip-body" id="nodeTipBody"></div>
<span class="nodetip-arrow" aria-hidden="true"></span> <span class="nodetip-arrow" aria-hidden="true"></span>
</div> </div>
+59 -6
View File
@@ -1246,15 +1246,21 @@ function closeNodeTip(){
function toggleNodeTip(el){ function toggleNodeTip(el){
if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */ if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */
showNodeTip(el, true);
}
/* `touch` unterscheidet die beiden Anlässe: Am Zeiger ist der ganze Knoten der
Link (§6), ein -Knopf wäre dort ein zweiter Weg zum selben Ziel; und der
Sprung-Hinweis nennt Alt+Klick, den es auf dem Telefon nicht gibt. */
function showNodeTip(el, touch){
closeNodeTip(); closeNodeTip();
const title = el.getAttribute('title') || ''; const title = el.getAttribute('data-tip') || '';
const sep = '\n\n' + TIP_RULE + '\n'; const sep = '\n\n' + TIP_RULE + '\n';
const i = title.indexOf(sep); const i = title.indexOf(sep);
const desc = i >= 0 ? title.slice(0, i) : ''; const desc = i >= 0 ? title.slice(0, i) : '';
/* Der Sprung-Hinweis nennt im Tooltip Alt+Klick; hier gibt es kein Alt. */ const roh = i >= 0 ? title.slice(i + sep.length) : title;
const facts = (i >= 0 ? title.slice(i + sep.length) : title) const facts = touch ? roh.replace(t('jumpHint'), t('jumpHintTouch')) : roh;
.replace(t('jumpHint'), t('jumpHintTouch')); const href = touch && el.tagName === 'A' ? el.getAttribute('href') : null;
const href = el.tagName === 'A' ? el.getAttribute('href') : null;
/* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß /* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß
der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das
nicht er zeigt die harten Umbrüche und sah im schmalen Fenster nicht er zeigt die harten Umbrüche und sah im schmalen Fenster
@@ -1265,7 +1271,7 @@ function toggleNodeTip(el){
nodeTipBody.innerHTML = nodeTipBody.innerHTML =
(paras.length ? `<div class="nodetip-desc">${paras.map(p => `<p>${esc(p)}</p>`).join('')}</div>` : '') + (paras.length ? `<div class="nodetip-desc">${paras.map(p => `<p>${esc(p)}</p>`).join('')}</div>` : '') +
(facts ? `<div class="nodetip-facts">${esc(facts)}</div>` : '') + (facts ? `<div class="nodetip-facts">${esc(facts)}</div>` : '') +
(href ? `<a class="nodetip-link" href="${esc(href)}" target="_blank" rel="noopener">↗ ${esc(t('tipOpenLink'))}</a>` : ''); (href ? `<a class="nodetip-link" tabindex="-1" href="${esc(href)}" target="_blank" rel="noopener">↗ ${esc(t('tipOpenLink'))}</a>` : '');
tipNode = el; tipNode = el;
el.classList.add('tipped'); el.classList.add('tipped');
nodeTip.hidden = false; nodeTip.hidden = false;
@@ -1293,6 +1299,53 @@ function placeNodeTip(el){
Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px'); Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px');
} }
/* ---------- Dasselbe Fenster am Zeiger und an der Tastatur (D57) ----------
Es löst den nativen `title` ab: Der konnte weder Absätze noch eine Linie
(der Trennstrich war aus 24 `` gemalt, D40-Nachtrag) und erschien nie beim
Tastaturfokus. Beides kann das Fenster ohnehin schon es wurde nur für
Touch gebaut (D52).
Die Verzögerung ist Absicht-Erkennung: Über einen dichten Baum fährt man
hinweg, ohne etwas wissen zu wollen. Beim Wechsel von Knoten zu Knoten
zeigt es sofort weiter wer schon liest, wartet nicht noch einmal. */
const TIP_DELAY = 350;
let tipTimer = null, tipLeave = null;
const finePointer = () => window.matchMedia('(hover:hover) and (pointer:fine)').matches;
function cancelTipTimers(){
clearTimeout(tipTimer); clearTimeout(tipLeave);
tipTimer = tipLeave = null;
}
out.addEventListener('pointerover', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
const el = e.target.closest && e.target.closest('.node');
if(!el || el === tipNode) return;
cancelTipTimers();
/* Steht schon eines offen, ist die Absicht erwiesen — dann ohne Warten. */
tipTimer = setTimeout(() => showNodeTip(el, false), tipNode ? 0 : TIP_DELAY);
});
out.addEventListener('pointerout', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
clearTimeout(tipTimer);
/* Nicht sofort zumachen: Der Weg ins Fenster führt über den Zwischenraum,
und dort ist der Zeiger kurz über keinem von beiden. */
clearTimeout(tipLeave);
tipLeave = setTimeout(() => {
if(!nodeTip.matches(':hover')) closeNodeTip();
}, 120);
});
nodeTip.addEventListener('pointerenter', cancelTipTimers);
nodeTip.addEventListener('pointerleave', () => { closeNodeTip(); });
/* Tastaturfokus: ohne Verzögerung er ist bereits die ausdrückliche Absicht,
und ein `title` hat hier noch nie etwas gezeigt. */
out.addEventListener('focusin', e => {
const el = e.target.closest && e.target.closest('.node');
if(el && el !== tipNode) showNodeTip(el, false);
});
out.addEventListener('focusout', e => {
const el = e.target.closest && e.target.closest('.node');
if(el && el === tipNode) closeNodeTip();
});
document.getElementById('nodeTipClose').addEventListener('click', closeNodeTip); document.getElementById('nodeTipClose').addEventListener('click', closeNodeTip);
/* Tipp/Klick daneben schließt — der Link-Knopf im Fenster aber nicht. */ /* Tipp/Klick daneben schließt — der Link-Knopf im Fenster aber nicht. */
document.addEventListener('pointerdown', e => { document.addEventListener('pointerdown', e => {
+16 -5
View File
@@ -131,25 +131,36 @@ function nodeHtml(n, extra, opts, fold){
effKey effKey
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)}) ? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
: (n.status ? t('st_' + n.status.key) : ''), : (n.status ? t('st_' + n.status.key) : ''),
n.optional ? t('a11yOptional') : '', t('jumpHint')] n.optional ? t('a11yOptional') : '',
/* Die Kostenannahme Größe fehlt, gilt als M" (D18) hatte einen
eigenen `title` am Badge der erschiene neben dem Fenster
ein zweites Mal. Sie gehört ohnehin zu den Kurz-Fakten. */
(!n.size && cheapPath && !isDone(n)) ? t('implicitSizeTooltip') : '',
t('jumpHint')]
.filter(Boolean).join(' · '); .filter(Boolean).join(' · ');
const tip = n.desc && facts ? n.desc + '\n\n' + TIP_RULE + '\n' + facts const tip = n.desc && facts ? n.desc + '\n\n' + TIP_RULE + '\n' + facts
: (n.desc || facts); : (n.desc || facts);
const title = ` title="${attr(tip)}"`; /* `data-tip` statt `title`: Der Inhalt wird vom eigenen Knoten-Fenster
dargestellt (D57), und ein `title` daneben zeigte der Browser zusätzlich
als zweiten, dürftigeren Tooltip. Kein zweites Attribut mit demselben Text
es ist dasselbe, nur unter anderem Namen. */
const title = ` data-tip="${attr(tip)}"`;
const tagsHtml = n.tags && n.tags.length const tagsHtml = n.tags && n.tags.length
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>` ? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
: ''; : '';
/* Das implizite M-Badge macht eine KOSTENANNAHME sichtbar (D18). An einem /* Das implizite M-Badge macht eine KOSTENANNAHME sichtbar (D18). An einem
erledigten Knoten wird keine getroffen er kostet nichts mehr (D46) , erledigten Knoten wird keine getroffen er kostet nichts mehr (D46) ,
dort bleibt es deshalb weg. */ dort bleibt es deshalb weg. */
const implicitTip = attr(t('implicitSizeTooltip')); const implicitSize = !n.size && cheapPath && !isDone(n);
const sizeBadge = n.size const sizeBadge = n.size
? `<span class="size" aria-hidden="true">${n.size}</span>` ? `<span class="size" aria-hidden="true">${n.size}</span>`
: (cheapPath && !isDone(n) ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</span>` : ''); : (implicitSize ? `<span class="size implicit" aria-hidden="true">M</span>` : '');
/* High-Risk: Warndreieck (, Textpräsentation via VS15) an der oberen linken /* High-Risk: Warndreieck (, Textpräsentation via VS15) an der oberen linken
Ecke. aria-hidden die Information steckt bereits im Status des aria-label. */ Ecke. aria-hidden die Information steckt bereits im Status des aria-label. */
/* Kein eigener `title` mehr: Er zeigte sonst zusätzlich zum Knoten-Fenster
(D57), und seine Aussage High Risk" steht dort ohnehin als Status. */
const riskMark = n.status && n.status.key === 'highrisk' const riskMark = n.status && n.status.key === 'highrisk'
? `<span class="risk" aria-hidden="true" title="${attr(t('riskTooltip'))}">⚠︎</span>` ? `<span class="risk" aria-hidden="true">⚠︎</span>`
: ''; : '';
/* Falt-Zeichen (SPEC §9/D38): offen, n" eingeklappt das Klickziel /* Falt-Zeichen (SPEC §9/D38): offen, n" eingeklappt das Klickziel
fürs Umklappen (der einfache Klick auf den Knoten bleibt der Link, §6). fürs Umklappen (der einfache Klick auf den Knoten bleibt der Link, §6).
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`; exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" data-tip="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" data-tip="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" data-tip="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" data-tip="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" data-tip="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" data-tip="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" data-tip="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`; exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" data-tip="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" data-tip="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" data-tip="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" data-tip="st_geplant · implicitSizeTooltip · jumpHint">WordPress<span class="size implicit" aria-hidden="true">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" data-tip="st_idee · implicitSizeTooltip · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" data-tip="implicitSizeTooltip · jumpHint">Cloud<span class="size implicit" aria-hidden="true">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" data-tip="implicitSizeTooltip · jumpHint">On-Premise<span class="size implicit" aria-hidden="true">M</span></div></li></ul></li></ul></li>"`;
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`; exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node done st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node done st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node done st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" data-tip="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" data-tip="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" data-tip="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" data-tip="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" data-tip="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" data-tip="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" data-tip="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" data-tip="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" data-tip="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" data-tip="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
+1 -1
View File
@@ -70,7 +70,7 @@ describe('Unbekannte IDs warnen — Zyklen nie', () => {
describe('Darstellung — Tooltip und aria, kein eigenes Zeichen', () => { describe('Darstellung — Tooltip und aria, kein eigenes Zeichen', () => {
it('zeigt die Abhängigkeiten als → im Tooltip und als a11yDeps', () => { it('zeigt die Abhängigkeiten als → im Tooltip und als a11yDeps', () => {
const {html} = render(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`); const {html} = render(`[ ] A #a\n[ ] B #b\n[ ] C :#a,#b`);
expect(html).toContain('title="→ #a, #b · st_geplant · jumpHint"'); expect(html).toContain('data-tip="→ #a, #b · st_geplant · jumpHint"');
expect(html).toContain('aria-label="C, a11yStatus, a11yDeps"'); expect(html).toContain('aria-label="C, a11yStatus, a11yDeps"');
}); });
+3 -3
View File
@@ -102,7 +102,7 @@ describe('Langform — ID-Blöcke hinter `---`', () => {
describe('Darstellung — Tooltip, ”-Marke, aria', () => { describe('Darstellung — Tooltip, ”-Marke, aria', () => {
it('stellt die Beschreibung an den Anfang des Tooltips und setzt die Marke', () => { it('stellt die Beschreibung an den Anfang des Tooltips und setzt die Marke', () => {
const {html} = render(`[ ] Auth #auth\n " Kapselt Login.`); const {html} = render(`[ ] Auth #auth\n " Kapselt Login.`);
expect(html).toContain('title="Kapselt Login.\n\n' + '─'.repeat(24) expect(html).toContain('data-tip="Kapselt Login.\n\n' + '─'.repeat(24)
+ '\n#auth · st_geplant · jumpHint"'); + '\n#auth · st_geplant · jumpHint"');
expect(html).toContain('<span class="desc-mark" aria-hidden="true">”</span>'); expect(html).toContain('<span class="desc-mark" aria-hidden="true">”</span>');
expect(html).toContain('aria-label="Auth, a11yStatus, a11yId, Kapselt Login."'); expect(html).toContain('aria-label="Auth, a11yStatus, a11yId, Kapselt Login."');
@@ -112,7 +112,7 @@ describe('Darstellung — Tooltip, ”-Marke, aria', () => {
der Übergang unter. Getrennt wird nur, wenn es etwas zu trennen gibt. */ der Übergang unter. Getrennt wird nur, wenn es etwas zu trennen gibt. */
it('trennt Beschreibung und Kurz-Fakten durch Leerzeile und Strich', () => { it('trennt Beschreibung und Kurz-Fakten durch Leerzeile und Strich', () => {
const {html} = render(`[ ] Auth #auth\n " Erste Zeile.\n " Zweite Zeile.`); const {html} = render(`[ ] Auth #auth\n " Erste Zeile.\n " Zweite Zeile.`);
const tip = html.match(/title="([^"]*)"/)[1]; const tip = html.match(/data-tip="([^"]*)"/)[1];
const [text, rest] = tip.split('\n\n'); const [text, rest] = tip.split('\n\n');
expect(text).toBe('Erste Zeile.\nZweite Zeile.'); /* Fließtext bleibt zusammen */ expect(text).toBe('Erste Zeile.\nZweite Zeile.'); /* Fließtext bleibt zusammen */
expect(rest.split('\n')[0]).toMatch(/^─+$/); /* Trennstrich als eigene Zeile */ expect(rest.split('\n')[0]).toMatch(/^─+$/); /* Trennstrich als eigene Zeile */
@@ -121,7 +121,7 @@ describe('Darstellung — Tooltip, ”-Marke, aria', () => {
it('setzt keinen Trennstrich, wenn es keine Beschreibung gibt', () => { it('setzt keinen Trennstrich, wenn es keine Beschreibung gibt', () => {
const {html} = render(`[ ] Ohne #auth`); const {html} = render(`[ ] Ohne #auth`);
expect(html.match(/title="([^"]*)"/)[1]).not.toContain('─'); expect(html.match(/data-tip="([^"]*)"/)[1]).not.toContain('─');
}); });
}); });
+1 -1
View File
@@ -82,7 +82,7 @@ describe('Darstellung — Farbe effektiv, Marke intrinsisch', () => {
it('benennt beide Status in Tooltip und aria-label', () => { it('benennt beide Status in Tooltip und aria-label', () => {
const {html} = render(TXT); const {html} = render(TXT);
expect(html).toContain('title="→ #api · heldTooltip · jumpHint"'); expect(html).toContain('data-tip="→ #api · heldTooltip · jumpHint"');
expect(html).toContain('aria-label="Deploy, a11yStatus, a11yEffective, a11yDeps"'); expect(html).toContain('aria-label="Deploy, a11yStatus, a11yEffective, a11yDeps"');
}); });
+2 -2
View File
@@ -115,13 +115,13 @@ describe('Doppelte IDs — Warnung an der späteren Zeile', () => {
describe('Darstellung — Tooltip und aria, kein Badge', () => { describe('Darstellung — Tooltip und aria, kein Badge', () => {
it('zeigt die ID im Tooltip und im aria-label', () => { it('zeigt die ID im Tooltip und im aria-label', () => {
const {html} = render(`[ ] Auth #auth`); const {html} = render(`[ ] Auth #auth`);
expect(html).toContain('title="#auth · st_geplant · jumpHint"'); expect(html).toContain('data-tip="#auth · st_geplant · jumpHint"');
expect(html).toContain('aria-label="Auth, a11yStatus, a11yId"'); expect(html).toContain('aria-label="Auth, a11yStatus, a11yId"');
}); });
it('ändert Knoten ohne ID nicht', () => { it('ändert Knoten ohne ID nicht', () => {
const {html} = render(`[ ] Ohne`); const {html} = render(`[ ] Ohne`);
expect(html).not.toContain('a11yId'); expect(html).not.toContain('a11yId');
expect(html).toContain('title="st_geplant · jumpHint"'); expect(html).toContain('data-tip="st_geplant · jumpHint"');
}); });
}); });
+1 -1
View File
@@ -71,7 +71,7 @@ describe('renderTreeHtml — „Untergliederung fehlt" (Geister-Knoten, SPEC §5
const {html} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()}); const {html} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
expect(count(html, 'ghost-node')).toBe(1); expect(count(html, 'ghost-node')).toBe(1);
expect(html).toContain('title="ghostTooltip"'); expect(html).toContain('title="ghostTooltip"');
expect(html).toMatchInlineSnapshot(`"<li><div class="node root-node st-geplant" tabindex="0" data-line="1" aria-label="Großes Paket, a11yStatus, a11ySize" title="st_geplant · jumpHint">Großes Paket<span class="size" aria-hidden="true">L</span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li>"`); expect(html).toMatchInlineSnapshot(`"<li><div class="node root-node st-geplant" tabindex="0" data-line="1" aria-label="Großes Paket, a11yStatus, a11ySize" data-tip="st_geplant · jumpHint">Großes Paket<span class="size" aria-hidden="true">L</span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li>"`);
}); });
it('verworfenes M+ löst die Regel nicht aus', () => { it('verworfenes M+ löst die Regel nicht aus', () => {