100 lines
7.1 KiB
JavaScript
100 lines
7.1 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { starteRouter, geheZu, oeffneOverlay, beendeOverlay, zurueck, stempeln } from '../public/neu/router.js'
|
|
import { hashFuerRoute } from '../public/neu/routen.js'
|
|
|
|
// Verlauf des Browsers im Kleinen: Einträge { url, state } mit Index; pushState schneidet Vorwärtseinträge ab,
|
|
// zurueck()/vor() springen und rufen die popstate-Handler synchron; location und history.state kommen aus dem aktuellen Eintrag.
|
|
function baueVerlauf(url, state = null) {
|
|
const eintraege = [{ url, state }]
|
|
let index = 0
|
|
const handler = new Set()
|
|
const teile = () => {
|
|
const u = eintraege[index].url; const h = u.indexOf('#'); const vorHash = h === -1 ? u : u.slice(0, h); const q = vorHash.indexOf('?')
|
|
return { pathname: q === -1 ? vorHash : vorHash.slice(0, q), search: q === -1 ? '' : vorHash.slice(q), hash: h === -1 ? '' : u.slice(h) }
|
|
}
|
|
const springe = (neu) => { if (neu < 0 || neu >= eintraege.length) return; index = neu; for (const f of [...handler]) f({ type: 'popstate' }) }
|
|
const location = { get pathname() { return teile().pathname }, get search() { return teile().search }, get hash() { return teile().hash } }
|
|
const history = {
|
|
get state() { return eintraege[index].state },
|
|
pushState(s, _t, u) { eintraege.splice(index + 1); eintraege.push({ url: u ?? eintraege[index].url, state: s }); index += 1 },
|
|
replaceState(s, _t, u) { eintraege[index] = { url: u ?? eintraege[index].url, state: s } },
|
|
back() { springe(index - 1) },
|
|
}
|
|
const umgebung = { location, history, addEventListener(typ, f) { if (typ === 'popstate') handler.add(f) }, removeEventListener(typ, f) { handler.delete(f) } }
|
|
return {
|
|
umgebung, urls: () => eintraege.map((e) => e.url), aktuell: () => eintraege[index].url, zurueck: () => springe(index - 1), vor: () => springe(index + 1),
|
|
// Klick auf einen Fragment-Link (<a href="#…">): neuer Eintrag ohne Stempel, der Browser feuert popstate
|
|
link: (u) => { eintraege.splice(index + 1); eintraege.push({ url: u, state: null }); index += 1; for (const f of [...handler]) f({ type: 'popstate' }) },
|
|
}
|
|
}
|
|
function start(url, state = null) {
|
|
const v = baueVerlauf(url, state); const log = []
|
|
starteRouter({ zeige: (route, { nav }) => log.push([hashFuerRoute(route), nav]), umgebung: v.umgebung })
|
|
return { v, log }
|
|
}
|
|
|
|
test('Start auf Chat legt den Anker an; Zurück führt zur Website (Review Focus 2)', () => {
|
|
const { v, log } = start('/dashboard.html#chat')
|
|
assert.deepEqual(v.urls(), ['/dashboard.html', '/dashboard.html#chat']); assert.deepEqual(log, [['#chat', 'tab']])
|
|
v.zurueck(); assert.deepEqual(log.at(-1), ['#', 'back'])
|
|
})
|
|
|
|
test('Start in Etage 2: Kette Website, Funktionen, Terminplaner; Query bleibt (Review Focus 1)', () => {
|
|
assert.deepEqual(start('/dashboard.html#termine?bestellt=1&projekt=x').v.urls(), ['/dashboard.html', '/dashboard.html#funktionen', '/dashboard.html#termine?bestellt=1&projekt=x'])
|
|
})
|
|
|
|
test('Alias wird kanonisch (#wartung wird #abo, #abo bleibt #abo), ?abo= führt zum Reiter Abo und bleibt am Zieleintrag; Neuladen mit Stempel baut keine zweite Kette', () => {
|
|
assert.equal(start('/dashboard.html#wartung').v.aktuell(), '/dashboard.html#abo'); assert.equal(start('/dashboard.html#abo').v.aktuell(), '/dashboard.html#abo')
|
|
const a = start('/dashboard.html?abo=success#chat')
|
|
assert.deepEqual([a.v.urls(), a.log[0]], [['/dashboard.html', '/dashboard.html?abo=success#abo'], ['#abo', 'tab']])
|
|
assert.equal(start('/dashboard.html#termine', { kn: { tiefe: 2, overlay: false } }).v.urls().length, 1)
|
|
})
|
|
|
|
test('Reiter und Bereiche ersetzen, #abo wird nie #vertraege, Etage 2 schiebt, same, ersetzen erzwingt replaceState', () => {
|
|
const { v, log } = start('/dashboard.html#vertraege'); const n = v.urls().length
|
|
geheZu('#rechnungen'); geheZu('#chat'); assert.equal(v.urls().length, n); assert.deepEqual(log.slice(1), [['#rechnungen', 'tab'], ['#chat', 'tab']])
|
|
geheZu('#chat'); assert.equal(log.at(-1)[1], 'same')
|
|
geheZu('#vertraege'); geheZu('#abo'); assert.deepEqual([v.aktuell(), log.at(-1), v.urls().length], ['/dashboard.html#abo', ['#abo', 'tab'], n]) // Reiterklick Abo: ausdrücklicher Hash (Review Focus 1)
|
|
geheZu('#funktionen'); geheZu('#termine'); assert.equal(v.urls().length, n + 1); assert.deepEqual(log.at(-1), ['#termine', 'push'])
|
|
zurueck(); assert.deepEqual(log.at(-1), ['#funktionen', 'back']); assert.equal(v.urls().length, n + 1)
|
|
geheZu('#termine', { ersetzen: true }); assert.equal(v.urls().length, n + 1)
|
|
})
|
|
|
|
test('Stempel: Modul löscht ihn mit replaceState(null), stempeln() stellt ihn her, Zurück bleibt history.back', () => {
|
|
const { v } = start('/dashboard.html#funktionen'); geheZu('#termine')
|
|
v.umgebung.history.replaceState(null, '', '/dashboard.html#termine') // wie termine.js:40
|
|
assert.equal(v.umgebung.history.state, null); stempeln(); assert.ok(v.umgebung.history.state.kn.tiefe >= 1)
|
|
const n = v.urls().length; zurueck(); assert.deepEqual([v.urls().length, v.aktuell()], [n, '/dashboard.html#funktionen'])
|
|
})
|
|
|
|
test('Zurück ohne eigenen Eintrag ersetzt durch das Elternziel', () => {
|
|
const { v, log } = start('/dashboard.html#termine', { kn: { tiefe: 0, overlay: false } }); const n = v.urls().length
|
|
zurueck(); assert.deepEqual([v.aktuell(), v.urls().length, log.at(-1)], ['/dashboard.html#funktionen', n, ['#funktionen', 'back']])
|
|
})
|
|
|
|
test('Overlay: Zurück schließt zuerst das Sheet; Navigation ersetzt den Overlay-Eintrag; Neuladen mit Overlay-Stempel', () => {
|
|
const { v, log } = start('/dashboard.html#chat'); let zu = 0; const sch = { schliesse: () => { zu += 1 } }
|
|
oeffneOverlay(sch); assert.equal(v.urls().length, 3)
|
|
v.zurueck(); assert.deepEqual([zu, log.length], [1, 1]) // zeige nicht erneut gerufen
|
|
oeffneOverlay(sch); geheZu('#passwort')
|
|
assert.deepEqual([zu, v.aktuell(), v.urls().length, log.at(-1)[1]], [2, '/dashboard.html#passwort', 3, 'push'])
|
|
const b = start('/dashboard.html#chat', { kn: { tiefe: 1, overlay: true } }); assert.equal(b.v.umgebung.history.state.kn.overlay, false)
|
|
oeffneOverlay(sch); beendeOverlay(); assert.equal(zu, 3)
|
|
})
|
|
|
|
test('Overlay auf der gleichen Route: geheZu schließt genau einmal und lässt keinen Eintrag zurück', () => {
|
|
const { v, log } = start('/dashboard.html#chat'); let zu = 0
|
|
oeffneOverlay({ schliesse: () => { zu += 1 } }); geheZu('#chat')
|
|
assert.deepEqual([zu, v.aktuell(), log.at(-1)], [1, '/dashboard.html#chat', ['#chat', 'same']])
|
|
geheZu('#vertraege'); assert.equal(v.umgebung.history.state.kn.tiefe, 1) // der nächste Bereichswechsel ersetzt den Chat-Eintrag, kein Rest vom Overlay
|
|
})
|
|
|
|
test('Vorwärts in die Etage ist push; ein Fragment-Link ohne Stempel bekommt Stempel, Ebene und nav', () => {
|
|
const { v, log } = start('/dashboard.html#funktionen'); geheZu('#termine'); v.zurueck(); v.vor()
|
|
assert.deepEqual(log.slice(-2), [['#funktionen', 'back'], ['#termine', 'push']])
|
|
v.link('/dashboard.html#zeiten')
|
|
assert.deepEqual([log.at(-1), v.umgebung.history.state.kn.tiefe], [['#zeiten', 'push'], 3])
|
|
v.link('/dashboard.html#wartung'); assert.deepEqual([v.aktuell(), log.at(-1)], ['/dashboard.html#abo', ['#abo', 'tab']]) // kanonisch umgeschrieben
|
|
})
|