// Probe für Shell-Phase: native-App-Verhalten, Marquee-Auswahl, i18n. // Headless, dpr 2, swiftshader. Schreibt PNGs nach scripts/ und meldet je // Test (a)-(e) ein Ergebnis als JSON auf stdout (letzte Zeile). import puppeteer from "puppeteer"; const TARGET = "http://localhost:5173/"; const OUT = new URL("./", import.meta.url).pathname; const results = {}; const log = (k, v) => { results[k] = v; console.log(`[${k}]`, JSON.stringify(v)); }; const browser = await puppeteer.launch({ headless: "new", args: [ "--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader", "--disable-dev-shm-usage", ], }); try { const page = await browser.newPage(); await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 2 }); page.on("pageerror", (e) => console.log("[pageerror]", e.message)); page.on("console", (m) => { const t = m.text(); if (m.type() === "error") console.log("[console.error]", t); }); try { await page.goto(TARGET, { waitUntil: "networkidle0", timeout: 8000 }); } catch { // networkidle-Timeout ignorieren (Dev-Server hält Verbindungen offen). } await page.waitForSelector(".plan-svg", { timeout: 15000 }); await new Promise((r) => setTimeout(r, 800)); // ── (a) Deutsch: Sichtbarer Text unverändert ──────────────────────────── const ger = await page.evaluate(() => { const body = document.body.innerText; return { grundriss: body.includes("Grundriss"), perspektive: body.includes("Perspektive"), auswahl: body.includes("Auswahl"), lang: window.__i18n?.getLanguage?.() ?? null, }; }); await page.screenshot({ path: OUT + "probe-shell-a-de.png" }); log("a_german", { ...ger, ok: ger.grundriss && ger.perspektive && ger.lang === "de", }); // ── (b) Native-App: contextmenu unterdrückt, user-select ──────────────── const native = await page.evaluate(() => { const ev = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, }); const prevented = !document.body.dispatchEvent(ev); const bodySel = getComputedStyle(document.body).userSelect || getComputedStyle(document.body).webkitUserSelect; return { defaultPrevented: prevented, bodyUserSelect: bodySel }; }); log("b_native_contextmenu", native); // Ressourcen-Fenster öffnen, dann ein auf user-select:text prüfen. // Die Ressourcen-Aktion in der Topbar anklicken. const openedResources = await page.evaluate(() => { const btns = [...document.querySelectorAll("button, [role=button], .nav-tab, .topbar *")]; const hit = btns.find((b) => /Ressourcen/i.test(b.textContent || "")); if (hit) { hit.click(); return true; } return false; }); await new Promise((r) => setTimeout(r, 500)); // Falls noch kein input sichtbar, einen Manager-Tab öffnen (Bauteile etc.). const inputSel = await page.evaluate(() => { const inp = document.querySelector("input"); if (!inp) return { found: false }; const cs = getComputedStyle(inp); return { found: true, userSelect: cs.userSelect || cs.webkitUserSelect, }; }); await page.screenshot({ path: OUT + "probe-shell-b-resources.png" }); log("b_input_userselect", { openedResources, ...inputSel, ok: native.defaultPrevented === true && native.bodyUserSelect === "none" && inputSel.userSelect === "text", }); // Ressourcen-Fenster wieder schließen, damit der Plan frei liegt (ESC). await page.keyboard.press("Escape"); await new Promise((r) => setTimeout(r, 300)); // ── (c) Marquee: Links-Drag über den Raum wählt mehrere Wände ─────────── const planBox = await page.evaluate(() => { const svg = document.querySelector(".plan-svg"); const r = svg.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; }); // Crossing-Marquee (rechts→rechts unten nach links oben): ein großes Rechteck, // das den ganzen Raum kreuzt und damit ALLE vier Wände berührt. Rechts→links // = crossing window (berührend), robust gegen exakte Wandlage. const cx = planBox.x + planBox.w / 2; const cy = planBox.y + planBox.h / 2; const x0 = cx + planBox.w * 0.42; const y0 = cy + planBox.h * 0.4; const x1 = cx - planBox.w * 0.42; const y1 = cy - planBox.h * 0.4; await page.mouse.move(x0, y0); await page.mouse.down({ button: "left" }); // Mehrere Zwischenschritte, damit pointermove + Schwelle greifen. for (let i = 1; i <= 8; i++) { await page.mouse.move( x0 + ((x1 - x0) * i) / 8, y0 + ((y1 - y0) * i) / 8, ); await new Promise((r) => setTimeout(r, 20)); } await page.mouse.up({ button: "left" }); await new Promise((r) => setTimeout(r, 400)); const marquee = await page.evaluate(() => { const highlights = document.querySelectorAll(".plan-selected").length; const footer = document.body.innerText; const m = footer.match(/Auswahl:\s*(\d+)\s*(Wand|Wände)/); return { highlightPolys: highlights, footerMatch: m ? m[0] : null, footerN: m ? Number(m[1]) : 0, }; }); await page.screenshot({ path: OUT + "probe-shell-c-marquee.png" }); log("c_marquee", { ...marquee, ok: marquee.footerN >= 2 && marquee.highlightPolys >= 1, }); // Einzel-Klick wählt genau eine Wand. Eine echte Wandfläche im DOM finden // (gefülltes innerhalb der SVG, das nicht Hintergrund/Auswahl ist), // dann auf ihren Schwerpunkt in Client-Koordinaten klicken — deterministisch. const wallPoint = await page.evaluate(() => { const svg = document.querySelector(".plan-svg"); const polys = [...svg.querySelectorAll("polygon")].filter( (p) => !p.classList.contains("plan-selected") && !p.classList.contains("plan-bg"), ); const ctm = svg.getScreenCTM(); let best = null; let bestArea = 0; for (const poly of polys) { const pts = poly.getAttribute("points"); if (!pts) continue; const nums = pts.trim().split(/[\s,]+/).map(Number); let cxv = 0, cyv = 0, n = 0; let area = 0; for (let i = 0; i + 1 < nums.length; i += 2) { cxv += nums[i]; cyv += nums[i + 1]; n++; } if (n < 3) continue; // grobe Bounding-Box-Fläche als Größenmaß const xs = nums.filter((_, i) => i % 2 === 0); const ys = nums.filter((_, i) => i % 2 === 1); area = (Math.max(...xs) - Math.min(...xs)) * (Math.max(...ys) - Math.min(...ys)); if (area > bestArea) { bestArea = area; // SVG-User-Space → Client über CTM transformieren. const ptObj = svg.createSVGPoint(); ptObj.x = cxv / n; ptObj.y = cyv / n; const sp = ptObj.matrixTransform(ctm); best = { x: sp.x, y: sp.y }; } } return best; }); if (wallPoint) { await page.mouse.click(wallPoint.x, wallPoint.y, { button: "left" }); await new Promise((r) => setTimeout(r, 350)); } const single = await page.evaluate(() => { const footer = document.body.innerText; const m = footer.match(/Auswahl:\s*(\d+)\s*(Wand|Wände)/); return { footerMatch: m ? m[0] : null, footerN: m ? Number(m[1]) : 0, highlightPolys: document.querySelectorAll(".plan-selected").length, }; }); await page.screenshot({ path: OUT + "probe-shell-c2-single.png" }); log("c_single_click", { ...single, ok: single.footerN === 1 }); // ── (d) Rechtsklick auf den Plan öffnet UNSER ContextMenu (.ctx-menu) ──── // Erst sicherstellen, dass eine Wand gewählt ist (Einzelklick oben hat das // getan). Rechtsklick auf den Plan. await page.evaluate( ({ x, y }) => { const svg = document.querySelector(".plan-svg"); const ev = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 2, }); svg.dispatchEvent(ev); }, { x: cx, y: planBox.y + planBox.h * 0.22 }, ); await new Promise((r) => setTimeout(r, 300)); const planCtx = await page.evaluate(() => ({ ctxMenu: document.querySelectorAll(".ctx-menu").length, })); await page.screenshot({ path: OUT + "probe-shell-d-plan-context.png" }); log("d_plan_context", { ...planCtx, ok: planCtx.ctxMenu >= 1 }); // Menü schliessen. await page.mouse.click(planBox.x + 4, planBox.y + 4, { button: "left" }); await new Promise((r) => setTimeout(r, 200)); // Rechtsklick auf eine Ebenen-Zeile (linke Liste) öffnet ebenfalls .ctx-menu. const layerCtx = await page.evaluate(() => { const row = document.querySelector(".nav-list .cat-name")?.closest("div") || document.querySelector(".nav-list > *"); if (!row) return { found: false, ctxMenu: 0 }; const r = row.getBoundingClientRect(); const ev = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: r.x + 10, clientY: r.y + r.height / 2, button: 2, }); row.dispatchEvent(ev); return { found: true }; }); await new Promise((r) => setTimeout(r, 300)); const layerCtxCount = await page.evaluate(() => ({ ctxMenu: document.querySelectorAll(".ctx-menu").length, })); await page.screenshot({ path: OUT + "probe-shell-d2-layer-context.png" }); log("d_layer_context", { ...layerCtx, ...layerCtxCount, ok: layerCtx.found && layerCtxCount.ctxMenu >= 1, }); await page.mouse.click(planBox.x + 4, planBox.y + 4, { button: "left" }); await new Promise((r) => setTimeout(r, 200)); // ── (e) i18n auf Englisch schalten + Screenshot ───────────────────────── const enFlip = await page.evaluate(() => { if (!window.__i18n?.setLanguage) return { exposed: false }; window.__i18n.setLanguage("en"); return { exposed: true, lang: window.__i18n.getLanguage() }; }); await new Promise((r) => setTimeout(r, 400)); const enText = await page.evaluate(() => { const body = document.body.innerText; return { plan: body.includes("Plan"), perspective: body.includes("Perspective"), stillGerman: body.includes("Grundriss"), }; }); await page.screenshot({ path: OUT + "probe-shell-e-en.png" }); log("e_english", { ...enFlip, ...enText, ok: enFlip.exposed === true && enFlip.lang === "en" && enText.perspective && !enText.stillGerman, }); // Zurück auf Deutsch. await page.evaluate(() => window.__i18n?.setLanguage("de")); const allOk = Object.values(results).every((r) => r.ok !== false); console.log("\nRESULT " + JSON.stringify({ allOk, results })); } finally { await browser.close(); }