Bisher gab es genau ein Suchfeld, und es durchsuchte nur die Anfragenliste. Wer den Namen eines Kunden im Kopf hatte, musste raten, in welchem Reiter er nachsehen muss: War das eine Anfrage, ein laufendes Projekt, eine offene Rechnung? Bei drei Vorgaengen merkt man sich das, bei dreissig nicht mehr. Jetzt: Strg+K von ueberall, auf dem Handy der Lupenknopf oben. Ein Aufruf durchsucht Anfragen, Kunden, Projekte und Zahlungen; jeder Treffer traegt seinen Zusammenhang (Nummer, Kunde, Betrag, Liefertermin) und fuehrt per Enter in den passenden Reiter, bei einer Anfrage direkt in die Detailansicht. Gebaut nach dem ARIA-Muster 'Combobox mit Listbox-Popup' aus den W3C Authoring Practices -- nachgeschlagen, nicht aus dem Gedaechtnis: role=combobox am Eingabefeld, aria-expanded, aria-controls, aria-activedescendant, role=listbox, role=option mit aria-selected. Der Fokus bleibt dabei im Eingabefeld, damit man weitertippen kann; die Auswahl wandert ueber aria-activedescendant. Ohne diese Auszeichnung waere ein Feld, das Vorschlaege einblendet, fuer einen Screenreader stumm -- das sieht man beim Testen mit den Augen nie. Vier Fallen ausdruecklich behandelt: - Nicht bei jedem Tastendruck suchen (180 ms Wartezeit): 'Musterbau' haette sonst neun Abfragen ausgeloest, acht davon veraltet. - Das Wettrennen der Antworten: Jede Abfrage bekommt eine laufende Nummer, nur die neueste darf zeichnen. Sonst ueberschreibt eine spaet eintreffende alte Antwort die neue. - Leer, laedt und Fehler sind eigene Zustaende. Ein Kasten, der bei einem Serverfehler leer bleibt, sieht aus wie 'nichts gefunden'. - LIKE-Sonderzeichen: '%' waere ein Platzhalter, '_' ein beliebiges Zeichen -- und Unterstriche stehen regelmaessig in E-Mail-Adressen. Der Fehler ist tueckisch, weil die Suche trotzdem Treffer liefert, nur die falschen. Ausserdem zum dritten Mal dieselbe Spezifitaetsfalle gefunden: '.wd p' schlug die eigene Regel, der Tastaturhinweis erschien in 17,9px statt 11,5px -- so gross wie der Inhalt, den er erklaert. Jetzt festgenagelt durch eine Pruefung, die Groessenverhaeltnisse vergleicht. Geprueft: 28 gegen eine echte Datenbank (darunter alle LIKE-Sonderzeichen), 64 im Browser auf Computer und Handy inkl. der ARIA-Vorgaben, des Wettrennens und des Fehlerzustands. Co-Authored-By: Claude Opus 5 <[email protected]>
161 lines
7.9 KiB
JavaScript
161 lines
7.9 KiB
JavaScript
/* =====================================================================
|
|
test-suche.mjs — die Suche über alles
|
|
|
|
Der heikelste Teil sind die Sonderzeichen von LIKE. Ein Prozentzeichen
|
|
im Suchbegriff wäre ohne Behandlung ein Platzhalter, ein Unterstrich
|
|
ebenfalls -- und Unterstriche stehen regelmässig in E-Mail-Adressen.
|
|
Der Fehler ist tückisch, weil die Suche trotzdem Treffer liefert, nur
|
|
die falschen. Man merkt es nie.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, writeSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { randomBytes, createHmac } from "node:crypto";
|
|
|
|
const L = (...a) => writeSync(1, a.join(" ") + "\n");
|
|
const tmp = mkdtempSync(join(tmpdir(), "wd-suche-"));
|
|
process.env.DB_PATH = join(tmp, "t.db");
|
|
process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64");
|
|
process.env.WEBDESIGN_API_SECRET = randomBytes(24).toString("hex");
|
|
|
|
const { initDb, db } = await import("./db.js");
|
|
initDb();
|
|
const geheim = await import("./lib/webdesign-geheimnisse.js");
|
|
await geheim.geheimnisseLaden();
|
|
const { suche } = await import("./routes/webdesign-suche.js");
|
|
|
|
let ok = 0, fehler = 0;
|
|
const pruefe = (n, b, z) => b
|
|
? (ok++, L(" ok " + n))
|
|
: (fehler++, L(" FEHL " + n + (z !== undefined ? " -> " + z : "")));
|
|
|
|
function ausweis(gueltigMs = 60_000) {
|
|
const n = Buffer.from(JSON.stringify({
|
|
bereich: "wd-admin", rolle: "dogfather", exp: Date.now() + gueltigMs,
|
|
})).toString("base64url");
|
|
return n + "." + createHmac("sha256", process.env.WEBDESIGN_API_SECRET).update(n).digest("base64url");
|
|
}
|
|
async function such(q, kopf) {
|
|
let out = null;
|
|
await suche(
|
|
{ headers: kopf || { authorization: "Bearer " + ausweis() }, cookies: {}, query: { q }, params: {} },
|
|
{ status() { return this; }, json(o) { out = o; return this; }, setHeader() {}, end() {} }
|
|
);
|
|
return out;
|
|
}
|
|
|
|
const jetzt = new Date().toISOString();
|
|
db.prepare(`INSERT INTO wd_kunden (id,email,name,firma,erstellt_am) VALUES ('k1','[email protected]','Anna Muster','Musterbau GmbH',?)`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_kunden (id,email,name,erstellt_am) VALUES ('k2','[email protected]','Bert Meier',?)`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_kunden (id,email,name,erstellt_am,gesperrt) VALUES ('k3','[email protected]','Clara Sperr',?,1)`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,erstellt_am)
|
|
VALUES ('a1','A-2608-0001','Anna Muster','[email protected]','website','neu',?)`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,erstellt_am)
|
|
VALUES ('a2','A-2608-0002','Dora 50% Rabatt','[email protected]','shop','neu',?)`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_projekte (id,nummer,kunde_id,titel,paket,status,erstellt_am,termin_am)
|
|
VALUES ('p1','P-2608-0001','k1','Website für Musterbau','website','design',?, '2026-10-15')`).run(jetzt);
|
|
db.prepare(`INSERT INTO wd_zahlungen (id,nummer,kunde_id,art,zweck_text,betrag_cent,status,erstellt_am)
|
|
VALUES ('z1','Z-2608-0001','k1','anzahlung','Anzahlung Website',30000,'offen',?)`).run(jetzt);
|
|
|
|
L("\n1. UEBER ALLE BEREICHE HINWEG");
|
|
let r = await such("Muster");
|
|
pruefe("die Suche antwortet", r && r.ok === true, JSON.stringify(r).slice(0, 100));
|
|
const arten = new Set(r.treffer.map((t) => t.art));
|
|
pruefe("sie findet die Anfrage", arten.has("anfrage"));
|
|
pruefe("sie findet den Kunden", arten.has("kunde"));
|
|
pruefe("sie findet das Projekt", arten.has("projekt"));
|
|
pruefe("sie findet die Zahlung ueber den KUNDENNAMEN", arten.has("zahlung"));
|
|
/* Vier Bereiche in einem Aufruf. Die erste Fassung erwartete drei und
|
|
uebersah, dass die Zahlung ueber den Namen des Kunden mitgefunden wird
|
|
-- was genau richtig ist: Wer "Muster" sucht, will auch dessen offene
|
|
Rechnung sehen, ohne sie ueber die Zahlungsnummer zu kennen. */
|
|
pruefe("alle vier Bereiche in EINEM Aufruf", arten.size === 4, Array.from(arten).join(","));
|
|
|
|
L("\n2. JEDER TREFFER TRAEGT SEINEN ZUSAMMENHANG");
|
|
const projektTreffer = r.treffer.find((t) => t.art === "projekt");
|
|
pruefe("das Projekt nennt Nummer und Kunde",
|
|
/P-2608-0001/.test(projektTreffer.unten) && /Anna Muster/.test(projektTreffer.unten),
|
|
projektTreffer.unten);
|
|
pruefe("und den Liefertermin", /2026-10-15/.test(projektTreffer.unten), projektTreffer.unten);
|
|
pruefe("jeder Treffer weiss, wohin er fuehrt",
|
|
r.treffer.every((t) => !!t.ziel), JSON.stringify(r.treffer.map((t) => t.ziel)));
|
|
pruefe("jeder Treffer hat eine Marke fuer seinen Stand",
|
|
r.treffer.every((t) => !!t.marke), JSON.stringify(r.treffer.map((t) => t.marke)));
|
|
|
|
L("\n3. SUCHE UEBER DEN KUNDENNAMEN FINDET SEIN PROJEKT");
|
|
/* Man merkt sich den Menschen, nicht die Projektnummer. */
|
|
r = await such("Musterbau");
|
|
pruefe("die Firma des Kunden fuehrt zum Kunden",
|
|
r.treffer.some((t) => t.art === "kunde" && t.titel === "Anna Muster"));
|
|
|
|
L("\n4. ZAHLUNGEN");
|
|
r = await such("Anzahlung");
|
|
pruefe("die Zahlung wird gefunden", r.treffer.some((t) => t.art === "zahlung"));
|
|
const z = r.treffer.find((t) => t.art === "zahlung");
|
|
pruefe("mit lesbarem Betrag", /300,00/.test(z.unten), z.unten);
|
|
|
|
L("\n5. LIKE-SONDERZEICHEN — hier entstehen falsche Treffer");
|
|
/* Ohne Behandlung waere "%" ein Platzhalter und faende ALLES. */
|
|
r = await such("%");
|
|
pruefe("ein einzelnes Prozentzeichen ist zu kurz und sucht gar nicht",
|
|
r.zuKurz === true, JSON.stringify(r).slice(0, 80));
|
|
r = await such("50%");
|
|
pruefe("nach '50%' wird woertlich gesucht",
|
|
r.treffer.length === 1 && /Dora/.test(r.treffer[0].titel),
|
|
r.treffer.length + " Treffer: " + r.treffer.map((t) => t.titel).join(", "));
|
|
/* Ohne Behandlung waere "_" ein beliebiges Zeichen: "bert_meier" faende
|
|
dann auch "bertXmeier". Geprueft wird umgekehrt -- ein Muster mit
|
|
Unterstrich darf NICHT auf etwas anderes passen. */
|
|
r = await such("bert_meier");
|
|
pruefe("ein Unterstrich wird woertlich genommen",
|
|
r.treffer.length === 1 && r.treffer[0].art === "kunde",
|
|
r.treffer.length + " Treffer");
|
|
r = await such("bertxmeier");
|
|
pruefe("und passt nicht auf ein beliebiges Zeichen",
|
|
r.treffer.length === 0, r.treffer.length + " Treffer");
|
|
|
|
L("\n6. ZU KURZ WIRD GAR NICHT GESUCHT");
|
|
r = await such("a");
|
|
pruefe("ein einzelner Buchstabe sucht nicht", r.zuKurz === true && r.treffer.length === 0);
|
|
r = await such("");
|
|
pruefe("leer ebenfalls nicht", r.zuKurz === true);
|
|
r = await such(" ");
|
|
pruefe("nur Leerzeichen ebenfalls nicht", r.zuKurz === true);
|
|
|
|
L("\n7. NICHTS GEFUNDEN IST EIN EIGENER ZUSTAND");
|
|
r = await such("gibtesnichtxyz");
|
|
pruefe("die Antwort ist gueltig", r.ok === true);
|
|
pruefe("mit null Treffern und NICHT zuKurz",
|
|
r.anzahl === 0 && r.zuKurz === false, JSON.stringify(r).slice(0, 90));
|
|
|
|
L("\n8. GESPERRTE KUNDEN WERDEN GEZEIGT, ABER GEKENNZEICHNET");
|
|
/* Verstecken waere falsch: Man sucht sie ja gerade, WEIL etwas nicht
|
|
stimmt. Ohne Kennzeichnung wuerde man sich aber wundern. */
|
|
r = await such("Sperr");
|
|
pruefe("der gesperrte Kunde erscheint", r.treffer.some((t) => t.art === "kunde"));
|
|
pruefe("und ist als gesperrt gekennzeichnet",
|
|
r.treffer.find((t) => t.art === "kunde").marke === "gesperrt",
|
|
r.treffer.find((t) => t.art === "kunde").marke);
|
|
|
|
L("\n9. ARCHIVIERTES TAUCHT NICHT AUF");
|
|
db.prepare(`UPDATE wd_anfragen SET archiviert = 1 WHERE id = 'a1'`).run();
|
|
r = await such("A-2608-0001");
|
|
pruefe("eine archivierte Anfrage wird nicht mehr gefunden",
|
|
!r.treffer.some((t) => t.art === "anfrage"), JSON.stringify(r.treffer));
|
|
|
|
L("\n10. DIE RECHTEPRUEFUNG GREIFT");
|
|
for (const [name, kopf] of [
|
|
["ohne Ausweis kein Ergebnis", {}],
|
|
["mit gefaelschter Unterschrift kein Ergebnis",
|
|
{ authorization: "Bearer " + ausweis().split(".")[0] + ".XXXX" }],
|
|
["mit abgelaufenem Ausweis kein Ergebnis",
|
|
{ authorization: "Bearer " + ausweis(-1000) }],
|
|
]) {
|
|
const a = await such("Muster", kopf);
|
|
pruefe(name, a && a.ok === false, JSON.stringify(a).slice(0, 70));
|
|
}
|
|
|
|
L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
|
|
process.exitCode = fehler ? 1 : 0;
|
|
rmSync(tmp, { recursive: true, force: true });
|