/** * Das Fenster muss **sichtbar** werden — auch mit gemerkter Lage. * * ## Der Vorfall, dem diese Datei ihr Dasein verdankt * * Seit 0.23.0 merkt sich die Anwendung Größe und Lage des Fensters. Am * 29.08.2026 zeigte sich: Sobald eine Position gespeichert war und beim Start * wieder angelegt wurde, starb auf einem Windows-11-Rechner der GPU-Prozess * (`GPU process exited unexpectedly: exit_code=-1`), danach der Renderer. * `ready-to-show` feuerte deshalb nie — und weil das Fenster erst dort * gezeigt wurde, blieb es für immer unsichtbar. * * Für den Anwender: Die Anwendung startet einmal, er verschiebt das Fenster, * und danach lässt sie sich nie wieder öffnen. Kein Fenster, keine Meldung, * vier laufende Prozesse. An die Einstellung kommt er nicht heran, weil dafür * ein Fenster nötig wäre. * * ## Warum keine der 189 vorhandenen Prüfungen das gefunden hat * * Playwright greift das Fenster über die Entwicklerschnittstelle, nicht über * den Bildschirm. `firstWindow()` liefert eine Seite, sobald der Renderer da * ist — **ob das Fenster jemals gezeigt wurde, fragt niemand.** Die ganze * Prüfkette lief grün gegen eine Anwendung, die kein Mensch hätte benutzen * können. * * Zweitens legt jeder andere Lauf ein **frisches** Profil an. Ohne * gespeicherte Lage tritt der Fehler nicht auf. Diese Datei ist deshalb die * einzige, die ein Profil **vorbelegt**. * * Geprüft wird darum genau das, was fehlte: `isVisible()` am echten Fenster. */ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { BrowserWindow } from 'electron'; import { _electron as electron, type ElectronApplication, type JSHandle, type Page, } from '@playwright/test'; import { paketstand } from '../tools/paketstand.mjs'; import { projektWurzel, weiterAmZuschnitt } from './electron-hilfe'; import { expect, konsoleErwartet, konsolenwacheAnhaengen, test } from './konsolenwache'; const EXE = join(projektWurzel, 'release', 'win-unpacked', 'Waffensachkunde Lernsoftware.exe'); /** * Eine gespeicherte Lage, wie sie nach dem ersten Verschieben entsteht. * * Die Zahlen stammen aus dem Profil, an dem der Fehler auftrat. Sie liegen * mitten auf einem gewöhnlichen Bildschirm — es ist keine abwegige Lage, * sondern die alltäglichste, die es gibt. */ const GEMERKTE_LAGE = { breite: 1182, hoehe: 823, x: 507, y: 183 }; /** * So lange darf es dauern, bis das Fenster steht. * * Großzügig über der Notbremse in `fenster.ts` (4 s): Der Test soll den * Rückfall noch als Erfolg werten — gezeigt ist gezeigt —, aber nicht * unbegrenzt warten. */ const GEDULD_MS = 20_000; /** * Wie weit die gemeldete Groesse von der gesetzten abweichen darf. * * Windows legt um ein veränderbares Fenster einen unsichtbaren Greifrahmen. * `getNormalBounds()` zählt ihn mit, die Angabe im Konstruktor nicht — gemessen * sind es sieben Bildpunkte in der Breite. Der Spielraum ist bewusst knapp: * Er soll den Rahmen durchlassen und ein Fenster, das bei jedem Start weiter * wächst, trotzdem auffallen lassen. */ const RAHMEN_SPIELRAUM = 16; let app: ElectronApplication | undefined; let profil: string; /** * Der Griff auf das echte Fenster, mit Typ. * * `browserWindow()` liefert einen untypisierten Griff; ohne diese Zusage * arbeitete jede Abfrage darauf mit `any`, und der Linter hätte recht damit, * das zu bemängeln. */ async function fensterGriff(seite: Page): Promise> { return (await app!.browserWindow(seite)) as JSHandle; } test.beforeAll(async () => { const paket = paketstand(projektWurzel); test.skip(!paket.vorhanden || paket.veraltet, paket.grund); profil = mkdtempSync(join(tmpdir(), 'wsk-fensterlage-')); /* Das Profil wird vorbelegt, bevor die Anwendung es zum ersten Mal sieht. Genau diese Ausgangslage hat kein anderer Lauf. */ writeFileSync( join(profil, 'einstellungen.json'), `${JSON.stringify({ fenster: GEMERKTE_LAGE }, null, 2)}\n`, 'utf8', ); app = await electron.launch({ executablePath: EXE, args: [`--user-data-dir=${profil}`] }); konsolenwacheAnhaengen(app); }); test.beforeEach(() => { /* Die Notbremse darf hier melden — sie ist der Gegenstand der Prüfung. Auf der Maschine, an der der Fehler auftrat, stirbt mit gemerkter Lage der GPU-Prozess, `ready-to-show` bleibt aus, und die Notbremse zeigt das Fenster nach der Frist. Genau dann schreibt sie diese Warnung. Sie stillzulegen hieße, die einzige Spur zu löschen, die den Vorfall im Fehlerprotokoll des Anwenders sichtbar macht. Auf einer Maschine, auf der `ready-to-show` normal feuert, bleibt die Meldung aus — angemeldet zu sein schadet dort nichts. */ konsoleErwartet( /Fenster ohne ersten Frame gezeigt/u, 'Die Notbremse ist hier der Prüfgegenstand; sie meldet, wenn sie greift.', ); }); test.afterAll(async () => { await app?.close(); if (profil) { rmSync(profil, { recursive: true, force: true }); } }); test('zeigt das Fenster, obwohl eine Lage gespeichert ist', async () => { /* Die eigentliche Zusage dieser Datei. `firstWindow()` allein genügt nicht — sie war beim Vorfall erfolgreich, während auf dem Bildschirm nichts stand. Gefragt wird deshalb das Fenster selbst. */ const seite = await app!.firstWindow({ timeout: GEDULD_MS }); await seite.waitForLoadState('domcontentloaded'); const fenster = await fensterGriff(seite); await expect .poll(async () => fenster.evaluate((w) => w.isVisible()), { timeout: GEDULD_MS, message: 'Das Fenster ist nie sichtbar geworden. Die Anwendung läuft dann als Prozess ' + 'ohne Oberfläche – für den Anwender startet sie einfach nicht.', }) .toBe(true); }); test('behält die gespeicherte Lage bei', async () => { /* Wäre die Antwort auf den Fehler gewesen, die gemerkte Lage einfach nicht mehr anzuwenden, liefe der Test oben ebenfalls grün — und die Funktion wäre still verschwunden. Deshalb wird auch geprüft, dass sie noch wirkt. */ const seite = await app!.firstWindow(); const fenster = await fensterGriff(seite); const lage = await fenster.evaluate((w) => w.getNormalBounds()); /* Die Position muss auf den Punkt stimmen — sie ist das, woran der Anwender merkt, dass sich die Anwendung etwas gemerkt hat. Bei der Größe zählt Windows den unsichtbaren Greifrahmen mit; verglichen wird deshalb mit Spielraum. */ expect(lage.x, 'Die gemerkte Position wurde nicht angewandt.').toBe(GEMERKTE_LAGE.x); expect(lage.y, 'Die gemerkte Position wurde nicht angewandt.').toBe(GEMERKTE_LAGE.y); expect(Math.abs(lage.width - GEMERKTE_LAGE.breite)).toBeLessThanOrEqual(RAHMEN_SPIELRAUM); expect(Math.abs(lage.height - GEMERKTE_LAGE.hoehe)).toBeLessThanOrEqual(RAHMEN_SPIELRAUM); }); test('ist mit gemerkter Lage auch bedienbar', async () => { /* Ein sichtbares, aber leeres Fenster wäre keine Abhilfe. */ const seite = await app!.firstWindow(); await weiterAmZuschnitt(seite); await expect(seite.getByRole('heading', { level: 1 })).toBeVisible({ timeout: GEDULD_MS }); });