import { test } from 'node:test' import assert from 'node:assert/strict' import { LUFT, MAX_VERSUCHE, PRUEF_MS, beobachteHoehe, haengt, hoeheZustand } from '../public/neu/hoehe.js' import { lies } from './support/knHuelle.js' test('hoeheZustand: größte Höhe je Breite, Drehen fängt neu an', () => { let z = hoeheZustand(null, { hoehe: 932, breite: 430 }) z = hoeheZustand(z, { hoehe: 873, breite: 430 }); assert.deepEqual(z, { voll: 932, breite: 430 }) z = hoeheZustand(z, { hoehe: 430, breite: 932 }); assert.deepEqual(z, { voll: 430, breite: 932 }) }) test('haengt: nur bei gleicher Breite, ohne Textfeld und mehr als LUFT kleiner (iOS-Fall 932 → 873)', () => { const z = { voll: 932, breite: 430 } assert.equal(haengt(z, { hoehe: 873, breite: 430, textfeldAktiv: false }), true) assert.equal(haengt(z, { hoehe: 873, breite: 430, textfeldAktiv: true }), false) // Tastatur offen assert.equal(haengt(z, { hoehe: 932 - LUFT, breite: 430, textfeldAktiv: false }), false) assert.equal(haengt(z, { hoehe: 400, breite: 932, textfeldAktiv: false }), false) // gedreht assert.equal(haengt(null, { hoehe: 1, breite: 1 }), false) }) /** Kleine Attrappe: Fenster, Dokument und eine Hülle, die mitschreibt, wann sie aus- und eingeblendet wird. */ function aufbau({ aktiv = () => true } = {}) { const sender = (felder = {}) => { const hoerer = {} return Object.assign({ addEventListener(typ, fn) { (hoerer[typ] ??= []).push(fn) }, removeEventListener(typ, fn) { hoerer[typ] = (hoerer[typ] ?? []).filter((x) => x !== fn) }, senden(typ) { for (const fn of hoerer[typ] ?? []) fn({ type: typ }) }, anzahl: (typ) => (hoerer[typ] ?? []).length, }, felder) } const spaeter = [] const protokoll = [] const win = sender({ innerHeight: 932, innerWidth: 430, scrollX: 0, scrollY: 0, setTimeout: (fn, ms) => { spaeter.push({ fn, ms }); return spaeter.length }, clearTimeout: (id) => { if (spaeter[id - 1]) spaeter[id - 1].fn = () => {} }, scrollTo: (x, y) => { win.scrollX = x; win.scrollY = y; protokoll.push(['scrollTo', x, y]) }, }) const verlauf = { scrollTop: 1200, scrollLeft: 0 } const haken = {} const style = {} let display = '' Object.defineProperty(style, 'display', { get: () => display, set: (v) => { display = v; protokoll.push(['display', v]); if (v === 'none') { verlauf.scrollTop = 0; win.scrollY = 0; haken.beimAusblenden?.() } } }) const knopf = { focus: () => protokoll.push(['focus']) } const element = { style, get offsetHeight() { protokoll.push(['umbruch']); return 100 }, querySelectorAll: () => [verlauf, { scrollTop: 0, scrollLeft: 0 }], contains: (el) => el === knopf || el === verlauf, } const doc = sender({ activeElement: null, body: {}, visibilityState: 'visible' }) const istFeld = (el) => el?.feld === true const h = beobachteHoehe({ win, doc, element, aktiv, istTextfeld: istFeld }) const laufe = () => { while (spaeter.length) spaeter.shift().fn() } return { win, doc, element, verlauf, knopf, protokoll, spaeter, h, laufe, haken } } test('beobachteHoehe: Tastatur zu und Höhe hängt → Hülle einmal aus und ein, Scrollstände zurück; danach Ruhe', () => { const t = aufbau() t.win.scrollY = 40 t.doc.activeElement = { feld: true } t.win.innerHeight = 873; t.win.senden('resize') // iOS: die Höhe fällt mit der Tastatur und bleibt unten assert.equal(t.spaeter.length, 0) // solange das Feld den Fokus hat, wird nicht geprüft t.doc.activeElement = t.doc.body; t.doc.senden('focusout') assert.deepEqual(t.spaeter.map((s) => s.ms), PRUEF_MS) t.spaeter.shift().fn() assert.deepEqual(t.protokoll, [['display', 'none'], ['umbruch'], ['display', ''], ['umbruch'], ['scrollTo', 0, 40]]) assert.equal(t.verlauf.scrollTop, 1200) t.win.innerHeight = 932 // WebKit hat neu gemessen t.protokoll.length = 0; t.laufe() assert.deepEqual(t.protokoll, []) assert.deepEqual(t.h.stand(), { voll: 932, breite: 430, versuche: 1 }) }) test('beobachteHoehe: hilft es nicht, höchstens MAX_VERSUCHE je Anlass; App wieder vorne ist ein neuer Anlass', () => { const t = aufbau() t.win.innerHeight = 873 t.doc.senden('focusout'); t.laufe() t.win.senden('resize'); t.laufe() assert.equal(t.protokoll.filter((p) => p[0] === 'display' && p[1] === 'none').length, MAX_VERSUCHE) t.doc.visibilityState = 'hidden'; t.doc.senden('visibilitychange'); assert.equal(t.spaeter.length, 0) t.doc.visibilityState = 'visible'; t.doc.senden('visibilitychange'); t.laufe() assert.equal(t.protokoll.filter((p) => p[0] === 'display' && p[1] === 'none').length, 2 * MAX_VERSUCHE) }) test('beobachteHoehe: Fokus auf einem Knopf in der Hülle bleibt, ohne Scrollen', () => { const t = aufbau() t.win.innerHeight = 873 t.doc.activeElement = t.knopf t.haken.beimAusblenden = () => { t.doc.activeElement = t.doc.body } // beim Ausblenden verliert der Knopf den Fokus (wie im Browser) assert.equal(t.h.pruefe(), true) assert.deepEqual(t.protokoll.at(-1), ['focus']) }) test('beobachteHoehe: nicht aktiv (kein iOS-App-Modus), keine Hülle oder Höhe passt → nichts passiert; beende räumt auf', () => { const aus = aufbau({ aktiv: () => false }) aus.win.innerHeight = 873; assert.equal(aus.h.pruefe(), false); assert.deepEqual(aus.protokoll, []) const ok = aufbau() assert.equal(ok.h.pruefe(), false) ok.win.innerHeight = 932 - LUFT; assert.equal(ok.h.pruefe(), false); assert.deepEqual(ok.protokoll, []) ok.doc.senden('focusout'); ok.h.beende(); ok.win.innerHeight = 873; ok.laufe() assert.deepEqual(ok.protokoll, []) assert.deepEqual([ok.win.anzahl('resize'), ok.doc.anzahl('focusout'), ok.doc.anzahl('visibilitychange')], [0, 0, 0]) }) test('huelle.js: Höhen-Wächter nur in der iOS-App vom Home-Bildschirm am Handy, auf der Hülle', () => { const js = lies('huelle.js') assert.match(js, /import \{ beobachteHoehe \} from '\.\/hoehe\.js'/) assert.match(js, /beobachteHoehe\(\{ win, doc, element: doc\.querySelector\('\.kn-huelle'\), aktiv: \(\) => istIos\(\) && standalone\(\) && istHandy\(\), istTextfeld \}\)/) })