import { test } from 'node:test' import assert from 'node:assert/strict' import { haltFokus } from '../public/neu/ansicht-chat.js' import { lies, handyTeil } from './support/knHuelle.js' // Chat als Vollbild-Ansicht (Pilot-Feedback 17.10.: „durchsichtig und verrutscht“). Die Lage prüfen die Ansichten kn-p2-chat-* des Werkzeugs im Browser; // diese Pins lesen nur den Quelltext und halten die Regeln fest, an denen die Lage hängt. const ANSICHTEN = lies('ansichten.css') const HUELLE = lies('huelle.css') const regel = (css, selektor) => { const m = css.match(new RegExp(`(?:^|\\n)${selektor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`)) return m ? m[1] : '' } const chatBlock = () => HUELLE.slice(HUELLE.indexOf('Chat: Vollbild-Ansicht')) test('Chat: der Verlauf füllt die Höhe und scrollt in sich, ohne feste Höhengrenze (kein 60vh/55vh mehr)', () => { const verlauf = regel(ANSICHTEN, '.kn .kn-chat .chat-messages') assert.match(verlauf, /flex:\s*1 1 0/) assert.match(verlauf, /min-height:\s*0/) assert.match(verlauf, /overflow-y:\s*auto/) assert.doesNotMatch(verlauf, /max-height/) assert.doesNotMatch(ANSICHTEN, /\.chat-messages\s*\{[^}]*(?:min-height:\s*(?:240|280)px|max-height:\s*\d+vh)/) assert.match(ANSICHTEN, /\.kn \.kn-chat \.chat-messages > :first-child\s*\{\s*margin-top:\s*auto/) // kurze Verläufe stehen unten bei der Eingabe }) test('Chat: Spalte höchstens 760 px, mittig; Eingabe fest unten', () => { const spalte = ANSICHTEN.match(/\n\.kn-chat \{[^}]*\}/)[0] assert.match(spalte, /max-width:\s*760px/) assert.match(spalte, /margin-inline:\s*auto/) assert.match(regel(ANSICHTEN, '.kn .kn-chat .kn-chat-composer'), /flex:\s*none/) assert.match(regel(HUELLE, '.kn-haupt > #bereich-chat:not([hidden])'), /max-width:\s*760px[^}]*margin:\s*0 auto/) }) test('Chat: Blasen ruhig (Fläche #f8f9fb für fremde, #ebeef1 für eigene), über Tokens', () => { assert.match(ANSICHTEN, /--chat-bubble-in:\s*var\(--kn-bg-subtle\)/) assert.match(ANSICHTEN, /--chat-bubble-out:\s*var\(--kn-grau-bg\)/) const tokens = lies('tokens.css') assert.match(tokens, /--kn-bg-subtle:\s*#f8f9fb/) assert.match(tokens, /--kn-grau-bg:\s*#ebeef1/) }) test('Chat: „Senden“ bleibt der schwarze Hauptknopf, auch bei leerem Feld; gesperrt (Ansichtsmodus) und beim Senden bleibt er gedämpft', () => { assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-composer:not(.is-disabled) .chat-send:disabled:not(.is-busy)'), /opacity:\s*1\b/) assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-icon-btn:disabled, .kn .kn-chat .chat-send:disabled'), /opacity:\s*\.55/) assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-send.is-busy:disabled'), /opacity:\s*\.8/) }) test('Chat in der Hülle: Fensterhöhe am Desktop, am Handy fest zwischen Kopf und Leiste, „Chat“ klein im Kopf statt große Überschrift', () => { const block = chatBlock() assert.match(regel(block, '.kn-spalte:has(#bereich-chat:not([hidden]))'), /display:\s*flex[^}]*height:\s*calc\(100dvh - var\(--kn-banner-h\)\)[^}]*min-height:\s*0/) assert.match(regel(block, '#chat-body'), /flex:\s*1 1 auto[^}]*min-height:\s*0/) const handy = handyTeil(block) assert.match(handy, /\.kn-haupt:has\(#bereich-chat:not\(\[hidden\]\)\)\s*\{[^}]*position:\s*fixed[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ var\(--kn-kopf-h\) \+ env\(safe-area-inset-top\)\) 0 var\(--kn-unten, var\(--kn-zeile-h\)\)/) assert.match(handy, /\.kn-haupt:has\(#bereich-chat:not\(\[hidden\]\)\)\s*\{[^}]*padding:\s*0 max\(16px, env\(safe-area-inset-right\)\) 6px max\(16px, env\(safe-area-inset-left\)\)/) // höchstens 8 px Luft über der Leiste assert.match(handy, /#bereich-chat > \.kn-titel\s*\{[^}]*position:\s*absolute[^}]*width:\s*1px[^}]*height:\s*1px[^}]*clip:\s*rect\(0 0 0 0\)/) // nur noch für Screenreader assert.match(handy, /\.kn:has\(#bereich-chat:not\(\[hidden\]\)\) \.kn-kopf\s*\{[^}]*height:[^}]*border-bottom-color:\s*var\(--kn-line\)/) // deckender Kopf mit Haarlinie assert.match(handy, /\.kn:has\(#bereich-chat:not\(\[hidden\]\)\) \.kn-kopf-titel\s*\{[^}]*opacity:\s*1[^}]*transform:\s*none/) assert.doesNotMatch(block, /backdrop-filter|gradient|rgba\(/) // deckend, kein Glas }) test('haltFokus: Druck auf einen Knopf im Composer nimmt dem Eingabefeld den Fokus nicht (sonst springt die Eingabe, wenn die Leiste zurückkommt, und der Tipp auf „Senden“ geht verloren)', () => { const hoerer = new Map() const knopf = { matches: (s) => s === 'button' } const wurzel = { addEventListener: (n, f) => hoerer.set(n, f), removeEventListener: (n) => hoerer.delete(n), contains: (el) => el === knopf || el === symbol, } const symbol = { closest: (s) => (s === 'button' ? knopf : null) } // das Symbol im Knopf ist das Ziel des Drucks const feld = { closest: () => null } const draussen = { closest: () => ({}) } // ein Knopf, der nicht im Composer liegt const h = haltFokus(wurzel) assert.deepEqual([...hoerer.keys()], ['mousedown']) const druck = (target) => { let verhindert = false; hoerer.get('mousedown')({ target, preventDefault: () => { verhindert = true } }); return verhindert } assert.equal(druck(symbol), true, 'Knopf im Composer: Fokus bleibt') assert.equal(druck(feld), false, 'Textfeld: normal') assert.equal(druck(draussen), false, 'Knopf außerhalb: normal') assert.equal(druck(undefined), false) h.abbau() assert.equal(hoerer.size, 0) })