/* KANN MAN IM SUCHFELD DER AUSWAHLLISTE TIPPEN? Filipe: „bei suchen kann man nichts reinschreiben." VERDACHT: Die Liste liegt seit dem Popover-Umbau von heute frueh im Top-Layer -- und der Dialog darunter ist MODAL. Ein modaler Dialog sperrt den Fokus auf sich selbst ein; ein Element ausserhalb ist dann zwar sichtbar, aber nicht bedienbar. BESTAETIGT, UND ES WAR MEIN FEHLER VOM SELBEN TAG: Die Liste hing nach dem Popover-Umbau am . Sichtbar (der Popover hebt sie in die Top Layer), aber ausserhalb des Fokus-Kaefigs -- klicken ging, tippen nicht. WARUM DAS EINE DAUERPRUEFUNG IST: Der Fehler war von aussen nicht zu sehen. Die Liste stand da, sie liess sich anklicken, sie filterte auf Knopfdruck. Nur eine Eingabe kam nicht an, und das faellt erst auf, wenn jemand wirklich tippt. Genau so hat Filipe ihn gefunden -- nicht ich. GEPRUEFT WIRD BEIDES GEGENEINANDER: Der Fokus muss ankommen UND die Liste darf nicht abgeschnitten sein. Die beiden Anforderungen haben sich an dieser Stelle schon zweimal gegenseitig kaputt gemacht (draussen: nicht bedienbar, drinnen: beschnitten). */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-sf-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "die Suchfeld-Pruefung"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); notbremse(150_000, "pruef-suchfeld"); await new Promise((r) => setTimeout(r, 900)); const BASIS = `http://127.0.0.1:${PORT}`; let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const ks = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", ks, jetzt); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", ks).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("VanVan", "hand", "CODE-HAND-0001"); d.close(); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); try { const kontext = await browser.newContext({ viewport: { width: 1100, height: 850 } }); const seite = await kontext.newPage(); const konsole = []; seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); await seite.click('.rolle[data-rolle="admin"]'); await seite.fill("#code", "CODE-DOGI-0001"); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(1800); await seite.evaluate(() => document.getElementById("neu-oeffnen")?.click()); await seite.waitForTimeout(700); await seite.evaluate(() => { [...document.querySelectorAll("#neu-art [data-art]")] .find((x) => /kanal/i.test(x.dataset?.art || x.textContent))?.click(); }); await seite.waitForTimeout(500); await seite.evaluate(() => document.querySelector("#kanal-zeile .wahl2__knopf")?.click()); await seite.waitForTimeout(800); const lage = await seite.evaluate(() => { const feld = document.querySelector(".wahl2__suche"); const liste = document.querySelector(".wahl2__liste"); const dlg = document.querySelector("dialog[open]"); return { feldDa: !!feld, listeDa: !!liste, popover: liste ? liste.matches(":popover-open") : null, /* DIE ENTSCHEIDENDE FRAGE: Liegt die Liste INNERHALB des modalen Dialogs? Ein modaler Dialog sperrt den Fokus auf seinen eigenen Baum ein. */ imDialog: dlg && liste ? dlg.contains(liste) : null, dialogModal: dlg ? dlg.matches(":modal") : null, frei: document.getElementById("kanal-kategorie")?.dataset.frei || "(nicht gesetzt)", }; }); console.log(" " + JSON.stringify(lage)); ok(lage.feldDa, "das Suchfeld ist ueberhaupt da"); /* 1. LAESST SICH DER FOKUS SETZEN? */ const fokus = await seite.evaluate(() => { const feld = document.querySelector(".wahl2__suche"); if (!feld) return null; feld.focus(); return document.activeElement === feld || document.activeElement?.className === feld.className; }); ok(fokus === true, `der Fokus landet im Suchfeld (${fokus})`); /* 2. KOMMT EIN GETIPPTES ZEICHEN AN? Ueber die echte Tastatur -- `fill()` setzt den Wert direkt und wuerde auch bei gesperrtem Fokus gelingen. */ await seite.keyboard.type("comm"); await seite.waitForTimeout(400); const getippt = await seite.evaluate(() => document.querySelector(".wahl2__suche")?.value || ""); console.log(` nach dem Tippen steht im Feld: "${getippt}"`); ok(getippt.length > 0, `ein getipptes Zeichen kommt an ("${getippt}")`); /* 3. UND FILTERT ES WIRKLICH? */ const gefiltert = await seite.evaluate(() => document.querySelectorAll(".wahl2__zeilen .wahl2__eintrag").length); console.log(` sichtbare Eintraege nach dem Tippen: ${gefiltert}`); ok(getippt.length === 0 || gefiltert > 0, "und die Liste filtert danach"); /* 4. GIBT ES DEN WEG FUER EINEN EIGENEN NAMEN? */ ok(lage.frei === "ja", `das Feld erlaubt freie Namen (data-frei="${lage.frei}")`); /* 5. UND DAS ALTE PROBLEM DARF NICHT ZURUECK SEIN. Die Liste hing frueher am , WEIL sie als Kind des Dialogs vom clip-path der abgeschraegten Ecke abgeschnitten wurde ("Die Liste endete mitten im Wort Events"). Jetzt haengt sie wieder im Dialog -- die Annahme ist, dass der Popover sie in die Top Layer hebt und der Beschnitt sie dort nicht erreicht. DAS IST EINE ANNAHME, ALSO WIRD SIE GEMESSEN: Ist der letzte Eintrag der Liste wirklich sichtbar, oder liegt er unter dem Beschnitt? Gefragt wird nicht das Rechteck (das stimmt auch bei abgeschnittenem Inhalt), sondern was an seiner Stelle wirklich zu sehen ist. */ await seite.evaluate(() => { const f = document.querySelector(".wahl2__suche"); if (f) { f.value = ""; f.dispatchEvent(new Event("input", { bubbles: true })); } }); await seite.waitForTimeout(400); const beschnitt = await seite.evaluate(() => { const zeilen = [...document.querySelectorAll(".wahl2__zeilen .wahl2__eintrag")]; if (!zeilen.length) return { keine: true }; const letzter = zeilen[zeilen.length - 1]; letzter.scrollIntoView({ block: "nearest" }); const r = letzter.getBoundingClientRect(); const mitte = document.elementFromPoint( Math.round(r.left + r.width / 2), Math.round(r.top + r.height / 2)); return { anzahl: zeilen.length, hoehe: Math.round(r.height), /* Trifft ein Klick auf die letzte Zeile wirklich sie selbst? */ trifft: !!(mitte && (mitte === letzter || letzter.contains(mitte))), was: mitte ? (mitte.className || mitte.tagName) : "(nichts)", /* Und liegt sie ueberhaupt im Bild? */ imBild: r.top >= 0 && r.bottom <= window.innerHeight + 1, }; }); console.log(" letzte Zeile: " + JSON.stringify(beschnitt)); ok(!beschnitt.keine && beschnitt.hoehe > 0, `die Liste hat sichtbare Zeilen (${beschnitt.anzahl})`); ok(beschnitt.trifft === true, `die LETZTE Zeile ist wirklich anklickbar -- kein Beschnitt (${beschnitt.was})`); ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze"); await seite.screenshot({ path: "pruef-suchfeld.png", fullPage: false }); await kontext.close(); } finally { await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } } console.log(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); process.exit(fehler ? 1 : 0);