Upload via GUI (85 Dateien)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// Teil des UI-Testlaufs (scripts/ui-smoke.js): prüft die Startanimation in echtem Electron mit einem
|
||||
// Ersatz-Hauptfenster. Das Startfenster muss genau über dem Hauptfenster liegen, wirklich durchsichtig sein
|
||||
// (kein weißes Feld), das Hauptfenster bis zum Aufbau verborgen bleiben, das Bild von der Mitte aus
|
||||
// aufgezogen werden und am Ende das Hauptfenster sichtbar sein. Gibt „✓ …“ und „✗ …“ Zeilen aus.
|
||||
//
|
||||
// Die Durchsichtigkeit wird über eine Bildschirmaufnahme geprüft: An einer leeren Stelle des Startfensters
|
||||
// muss dieselbe Farbe stehen wie vorher auf dem Desktop. Dafür darf die Hardware-Beschleunigung nicht
|
||||
// abgeschaltet sein, sonst verhalten sich durchsichtige Fenster anders als im echten Programm.
|
||||
|
||||
const { app, BrowserWindow, desktopCapturer, screen } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const { begin, play, MIN_SPIN_MS } = require(path.join(ROOT, 'src', 'features', 'splash.js'));
|
||||
|
||||
let failed = 0;
|
||||
function check(name, ok, detail = '') {
|
||||
if (!ok) failed++;
|
||||
console.log(`${ok ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
app.on('window-all-closed', (e) => e.preventDefault());
|
||||
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// Farbe des Bildschirms an einer Stelle (Koordinaten des Hauptbildschirms) als [r, g, b]
|
||||
async function pixelAt(x, y) {
|
||||
const d = screen.getPrimaryDisplay();
|
||||
const sf = d.scaleFactor;
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: Math.round(d.size.width * sf), height: Math.round(d.size.height * sf) }
|
||||
});
|
||||
const src = sources.find((s) => String(s.display_id) === String(d.id)) || sources[0];
|
||||
const size = src.thumbnail.getSize();
|
||||
const bmp = src.thumbnail.toBitmap(); // BGRA
|
||||
const i = (Math.round(y * sf) * size.width + Math.round(x * sf)) * 4;
|
||||
return [bmp[i + 2], bmp[i + 1], bmp[i]];
|
||||
}
|
||||
|
||||
const close = (a, b) => a.every((v, k) => Math.abs(v - b[k]) <= 6);
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const page = 'data:text/html;charset=utf-8,' + encodeURIComponent(
|
||||
'<body style="margin:0;background:#123"><div id="explorerGrid"><div>Repo</div></div></body>');
|
||||
|
||||
const main = new BrowserWindow({ width: 900, height: 600, frame: false, show: false, webPreferences: { sandbox: true } });
|
||||
main.loadURL(page);
|
||||
const loaded = new Promise((resolve) => main.webContents.once('did-finish-load', resolve));
|
||||
await loaded;
|
||||
|
||||
const mainBounds = main.getBounds();
|
||||
// Leere Stelle nahe der linken oberen Ecke des Fensters (das Logo sitzt in der Mitte)
|
||||
const sx = mainBounds.x + 24;
|
||||
const sy = mainBounds.y + 24;
|
||||
const desktopColor = await pixelAt(sx, sy);
|
||||
|
||||
let sawSplashWindow = false;
|
||||
let boundsMatch = false;
|
||||
let hiddenWhileSpinning = true;
|
||||
let sawGrow = false;
|
||||
|
||||
const startedAt = Date.now();
|
||||
const handle = begin({ htmlPath: path.join(ROOT, 'renderer', 'splash.html'), bounds: mainBounds });
|
||||
// Die Oberfläche gilt erst später als bereit, damit das Logo lange genug dreht, um es zu messen
|
||||
const slowReady = wait(3500);
|
||||
const playing = play(main, slowReady, handle);
|
||||
|
||||
// Durchsichtigkeit messen, solange das Logo dreht (nach dem Zeigen und der Verzögerung für die Mausereignisse)
|
||||
const duringColor = (async () => {
|
||||
await handle.shownAt;
|
||||
await wait(1400);
|
||||
return pixelAt(sx, sy);
|
||||
})();
|
||||
|
||||
// Während der Animation beobachten
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const others = BrowserWindow.getAllWindows().filter((w) => w !== main);
|
||||
if (others.length) {
|
||||
sawSplashWindow = true;
|
||||
const b = others[0].getBounds();
|
||||
if (b.x === mainBounds.x && b.y === mainBounds.y && b.width === mainBounds.width && b.height === mainBounds.height) boundsMatch = true;
|
||||
if (Date.now() - startedAt < MIN_SPIN_MS - 250 && main.isVisible() && main.getOpacity() > 0) hiddenWhileSpinning = false;
|
||||
try {
|
||||
if (await others[0].webContents.executeJavaScript("document.body.classList.contains('aufbau')")) sawGrow = true;
|
||||
} catch (_) { /* Fenster schon geschlossen */ }
|
||||
}
|
||||
if (sawSplashWindow && others.length === 0) break;
|
||||
await wait(40);
|
||||
}
|
||||
await playing;
|
||||
const duration = Date.now() - startedAt;
|
||||
const measured = await duringColor;
|
||||
|
||||
check('Startfenster erscheint über dem Hauptfenster', sawSplashWindow && boundsMatch);
|
||||
check('Startfenster ist durchsichtig (kein weißes Feld)', close(measured, desktopColor) && !close(measured, [255, 255, 255]),
|
||||
`Desktop ${desktopColor.join(',')} / Startfenster ${measured.join(',')}`);
|
||||
check('Hauptfenster bleibt während des Drehens unsichtbar', hiddenWhileSpinning);
|
||||
check(`Logo dreht sich mindestens ${MIN_SPIN_MS} ms`, duration >= MIN_SPIN_MS, `${duration} ms`);
|
||||
check('Animation endet in angemessener Zeit', duration < 12000, `${duration} ms`);
|
||||
check('Fenster wird von der Mitte aus aufgezogen', sawGrow);
|
||||
check('Danach ist das Hauptfenster sichtbar und deckend', main.isVisible() && main.getOpacity() === 1);
|
||||
await wait(300);
|
||||
check('Startfenster ist danach geschlossen', BrowserWindow.getAllWindows().filter((w) => w !== main).length === 0);
|
||||
|
||||
app.exit(failed ? 1 : 0);
|
||||
}).catch((e) => {
|
||||
console.log(`✗ Startanimation ohne Ausnahme — ${e.message}`);
|
||||
app.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
// Teil des UI-Testlaufs (scripts/ui-smoke.js): prüft das Tray-Menüfenster in echtem Electron, ohne das
|
||||
// Tray-Symbol selbst. Zeigt das Fenster, fährt mit der Maus darüber, klickt, drückt Esc.
|
||||
// Gibt „✓ …“ und „✗ …“ Zeilen aus; Aufruf durch ui-smoke.js.
|
||||
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const { createTrayPopup } = require(path.join(ROOT, 'src', 'features', 'trayPopup.js'));
|
||||
const trayMenu = require(path.join(ROOT, 'src', 'features', 'trayMenu.js'));
|
||||
|
||||
let failed = 0;
|
||||
function check(name, ok, detail = '') {
|
||||
if (!ok) failed++;
|
||||
console.log(`${ok ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
app.on('window-all-closed', (e) => e.preventDefault());
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const log = [];
|
||||
let active = 1;
|
||||
const template = () => trayMenu.buildTrayTemplate({
|
||||
version: '0.0.0', active, queued: 2, paused: false, autostart: true,
|
||||
onOpen: () => log.push('open'), onHistory: () => log.push('history'), onPause: () => log.push('pause'),
|
||||
onCancel: () => log.push('cancel'), onAutostart: () => log.push('autostart'), onQuit: () => log.push('quit')
|
||||
});
|
||||
const popup = createTrayPopup({
|
||||
htmlPath: path.join(ROOT, 'renderer', 'tray-menu.html'),
|
||||
preloadPath: path.join(ROOT, 'tray-menu-preload.js'),
|
||||
getModel: () => trayMenu.serializeTemplate(template()),
|
||||
onAction: (id) => trayMenu.runAction(template(), id)
|
||||
});
|
||||
|
||||
const js = (code) => popup.getWindow().webContents.executeJavaScript(code);
|
||||
const items = async () => JSON.parse(await js(`JSON.stringify([...document.querySelectorAll('.item')].map(b => { const r = b.getBoundingClientRect(); return { text: b.innerText.trim(), disabled: b.disabled, x: r.x, y: r.y, h: r.height }; }))`));
|
||||
const mid = (i) => ({ x: Math.round(i.x + 40), y: Math.round(i.y + i.h / 2) });
|
||||
const bg = (label) => js(`(() => { const b = [...document.querySelectorAll('.item')].find(x => x.innerText.includes(${JSON.stringify(label)})); return getComputedStyle(b).backgroundColor; })()`);
|
||||
const mouse = (type, p, extra = {}) => popup.getWindow().webContents.sendInputEvent({ type, x: p.x, y: p.y, ...extra });
|
||||
|
||||
try {
|
||||
popup.warmUp();
|
||||
await wait(6000); // erster Renderer-Start dauert mehrere Sekunden
|
||||
popup.show();
|
||||
await wait(800);
|
||||
check('Menüfenster wird sichtbar', popup.isVisible());
|
||||
|
||||
let list = await items();
|
||||
check('Menü zeigt Einträge mit Pausieren und Abbrechen',
|
||||
['Fenster öffnen', 'Upload-Verlauf', 'Übertragung pausieren', 'Laufende Übertragung abbrechen', 'Beenden'].every((t) => list.some((i) => i.text.includes(t))));
|
||||
|
||||
// Hover folgt der Maus
|
||||
const history = list.find((i) => i.text.includes('Upload-Verlauf'));
|
||||
// Das frisch geöffnete Fenster braucht einen Moment, bis es sein erstes Bild gezeichnet hat und Hover annimmt.
|
||||
let hovered = 'rgba(0, 0, 0, 0)';
|
||||
for (let i = 0; i < 12 && hovered === 'rgba(0, 0, 0, 0)'; i++) {
|
||||
mouse('mouseMove', { x: mid(history).x + (i % 2), y: mid(history).y });
|
||||
await wait(250);
|
||||
hovered = await bg('Upload-Verlauf');
|
||||
}
|
||||
const other = await bg('Fenster öffnen');
|
||||
check('Hover markiert den Eintrag unter der Maus', hovered !== 'rgba(0, 0, 0, 0)' && other === 'rgba(0, 0, 0, 0)', `${hovered} / ${other}`);
|
||||
|
||||
// Gesperrter Eintrag bleibt unmarkiert und löst nichts aus
|
||||
active = 0; popup.refresh(); await wait(400);
|
||||
list = await items();
|
||||
const cancel = list.find((i) => i.text.includes('abbrechen'));
|
||||
mouse('mouseMove', mid(cancel));
|
||||
await wait(300);
|
||||
check('Gesperrter Eintrag wird nicht markiert', cancel.disabled && (await bg('abbrechen')) === 'rgba(0, 0, 0, 0)');
|
||||
mouse('mouseDown', mid(cancel), { button: 'left', clickCount: 1 });
|
||||
mouse('mouseUp', mid(cancel), { button: 'left', clickCount: 1 });
|
||||
await wait(400);
|
||||
check('Klick auf gesperrten Eintrag löst keine Aktion aus', log.length === 0 && popup.isVisible());
|
||||
|
||||
// Klick führt die Aktion aus und schließt das Fenster
|
||||
const open = list.find((i) => i.text.includes('Fenster öffnen'));
|
||||
mouse('mouseMove', mid(open));
|
||||
mouse('mouseDown', mid(open), { button: 'left', clickCount: 1 });
|
||||
mouse('mouseUp', mid(open), { button: 'left', clickCount: 1 });
|
||||
await wait(600);
|
||||
check('Klick führt die Aktion aus und schließt das Menü', log.join() === 'open' && !popup.isVisible());
|
||||
|
||||
// Esc schließt
|
||||
popup.show();
|
||||
await wait(900);
|
||||
const reopened = popup.isVisible();
|
||||
const win = popup.getWindow();
|
||||
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
|
||||
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' });
|
||||
await wait(500);
|
||||
check('Esc schließt das Menü', reopened && !popup.isVisible());
|
||||
} catch (e) {
|
||||
check('Menüfenster-Test ohne Ausnahme', false, e.message);
|
||||
}
|
||||
|
||||
popup.destroy();
|
||||
app.exit(failed ? 1 : 0);
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
// Oberflächen-Testlauf: startet die App mit einem leeren, eigenen Datenordner (APPDATA), steuert sie
|
||||
// über das Chrome-DevTools-Protokoll und prüft Layout, Knöpfe und die neuen Funktionen.
|
||||
// Berührt weder echte Zugangsdaten noch einen Server. Aufruf: npm run test:ui
|
||||
//
|
||||
// Prüft nur, was ohne Anmeldung sichtbar ist. Die Repo-Ansicht und echte Uploads sind bewusst nicht dabei.
|
||||
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const electronPath = require(path.join(ROOT, 'node_modules', 'electron'));
|
||||
const PORT = 9300 + Math.floor(Math.random() * 500);
|
||||
|
||||
const results = [];
|
||||
function check(name, ok, detail = '') {
|
||||
results.push({ name, ok });
|
||||
console.log(`${ok ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
function killTree(pid) {
|
||||
if (!pid) return;
|
||||
if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
||||
else { try { process.kill(-pid, 'SIGKILL'); } catch (_) { try { process.kill(pid, 'SIGKILL'); } catch (__) {} } }
|
||||
}
|
||||
|
||||
async function waitForPage(timeoutMs, state = {}) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const targets = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json();
|
||||
if (targets.some((t) => /renderer[\\/]splash\.html/.test(t.url))) state.sawSplash = true;
|
||||
const page = targets.find((t) => t.type === 'page' && /renderer[\\/]index\.html/.test(t.url));
|
||||
if (page) return page;
|
||||
} catch (_) { /* App startet noch */ }
|
||||
await sleep(500);
|
||||
}
|
||||
throw new Error('Die App hat sich nicht innerhalb von ' + Math.round(timeoutMs / 1000) + ' s gemeldet');
|
||||
}
|
||||
|
||||
async function connect(page) {
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.addEventListener('open', resolve); ws.addEventListener('error', reject); });
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
ws.addEventListener('message', (m) => {
|
||||
const d = JSON.parse(m.data);
|
||||
if (d.id && pending.has(d.id)) { pending.get(d.id)(d); pending.delete(d.id); }
|
||||
});
|
||||
const send = (method, params = {}) => new Promise((resolve) => {
|
||||
const i = ++id;
|
||||
pending.set(i, resolve);
|
||||
ws.send(JSON.stringify({ id: i, method, params }));
|
||||
});
|
||||
const ev = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.result.exceptionDetails) throw new Error(r.result.exceptionDetails.exception?.description || r.result.exceptionDetails.text);
|
||||
return r.result.result.value;
|
||||
};
|
||||
return { ev, close: () => ws.close() };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ui-smoke-'));
|
||||
const appData = path.join(tmp, 'appdata');
|
||||
fs.mkdirSync(appData, { recursive: true });
|
||||
|
||||
const app = spawn(electronPath, ['.', `--remote-debugging-port=${PORT}`], {
|
||||
cwd: ROOT,
|
||||
env: { ...process.env, APPDATA: appData },
|
||||
stdio: 'ignore',
|
||||
detached: process.platform !== 'win32'
|
||||
});
|
||||
let page;
|
||||
try {
|
||||
const startup = { sawSplash: false };
|
||||
page = await waitForPage(60000, startup);
|
||||
const cdp = await connect(page);
|
||||
const { ev } = cdp;
|
||||
|
||||
// Warten, bis die Seite fertig geladen ist und die Renderer-Skripte laufen (im frischen Datenordner dauert das länger)
|
||||
const splashOpen = async () => {
|
||||
try {
|
||||
const targets = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json();
|
||||
return targets.some((t) => /renderer[\\/]splash\.html/.test(t.url));
|
||||
} catch (_) { return false; }
|
||||
};
|
||||
let ready = false;
|
||||
for (let i = 0; i < 60 && !ready; i++) {
|
||||
if (await splashOpen()) startup.sawSplash = true;
|
||||
ready = await ev("document.readyState === 'complete' && typeof showProgress === 'function' && typeof showUploadHistory === 'function' && !!document.getElementById('toolbar')").catch(() => false);
|
||||
if (!ready) await sleep(300);
|
||||
}
|
||||
if (!ready) throw new Error('Die Oberfläche wurde nicht fertig geladen');
|
||||
|
||||
// 0. Startbildschirm: erscheint, verschwindet nach dem Laden, danach ist das Hauptfenster sichtbar
|
||||
let splashGone = false;
|
||||
for (let i = 0; i < 25 && !splashGone; i++) {
|
||||
splashGone = !(await splashOpen());
|
||||
if (!splashGone) await sleep(400);
|
||||
}
|
||||
check('Startanimation erscheint beim Start', startup.sawSplash);
|
||||
check('Startanimation endet, sobald die Oberfläche geladen ist', splashGone);
|
||||
check('Hauptfenster ist nach der Startanimation sichtbar', (await ev("document.visibilityState")) === 'visible');
|
||||
await sleep(1000);
|
||||
|
||||
// 1. Start und Schnittstellen
|
||||
const title = await ev('document.title');
|
||||
check('App startet, Titel stimmt', title === 'Git Manager Explorer Pro', 'Titel: ' + JSON.stringify(title));
|
||||
const apis = ['cancelUpload', 'pauseUpload', 'resumeUpload', 'getUploadHistory', 'exportUploadHistoryCsv', 'getGitignoreTemplates',
|
||||
'inspectReleaseFile', 'prepareRelease', 'onTransferPauseState', 'onOpenUploadHistory', 'previewUploadPaths'];
|
||||
const missing = await ev(`${JSON.stringify(apis)}.filter(k => typeof window.electronAPI[k] !== 'function')`);
|
||||
check('Alle neuen Schnittstellen sind vorhanden', missing.length === 0, missing.join(', '));
|
||||
|
||||
// 2. Toolbar bricht bei keiner Fensterbreite um
|
||||
for (const width of [1216, 960, 720]) {
|
||||
await ev(`window.resizeTo(${width}, 780); 1`);
|
||||
await sleep(900);
|
||||
const r = await ev(`(() => {
|
||||
const bad = [];
|
||||
for (const b of document.querySelectorAll('#toolbar button')) {
|
||||
const rect = b.getBoundingClientRect();
|
||||
if (rect.width === 0) continue;
|
||||
if (rect.height > 40) bad.push(b.id || b.textContent.trim());
|
||||
}
|
||||
return { iw: innerWidth, bad, overflowX: document.documentElement.scrollWidth > innerWidth };
|
||||
})()`);
|
||||
check(`Toolbar bei ${width} px: keine umgebrochenen Knöpfe, kein seitliches Scrollen`, r.iw === width && r.bad.length === 0 && !r.overflowX,
|
||||
r.iw !== width ? `Fensterbreite ${r.iw}` : (r.bad.join(', ') || (r.overflowX ? 'seitlicher Überlauf' : '')));
|
||||
}
|
||||
await ev('window.resizeTo(1216, 828); 1');
|
||||
await sleep(600);
|
||||
|
||||
// 3. Repo-Leiste ohne Repo ausgeblendet
|
||||
check('Repo-Leiste ist in der Startansicht ausgeblendet', (await ev("getComputedStyle(document.querySelector('.toolbar-row--context')).display")) === 'none');
|
||||
|
||||
// 4. Fortschrittsbalken mit Pause und Abbrechen
|
||||
const prog = await ev(`(async () => {
|
||||
showProgress(40, describeTransferProgress('Upload', { processed: 48, total: 120, percent: 40, totalBytes: 48 * 1024 * 1024 }), { cancellable: true });
|
||||
const cancel = document.getElementById('folderProgressCancel');
|
||||
const pause = document.getElementById('folderProgressPause');
|
||||
const shown = { cancel: cancel.style.display, pause: pause.style.display, text: document.getElementById('folderProgressText').innerText };
|
||||
applyPauseState(true);
|
||||
const paused = { label: pause.textContent, text: document.getElementById('folderProgressText').innerText };
|
||||
applyPauseState(false);
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const afterEsc = { label: cancel.textContent, disabled: cancel.disabled };
|
||||
hideProgress();
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
return { shown, paused, afterEsc, reset: { cancelDisabled: cancel.disabled, display: cancel.style.display, pauseDisplay: pause.style.display } };
|
||||
})()`);
|
||||
check('Fortschritt zeigt Pausieren und Abbrechen', prog.shown.cancel !== 'none' && prog.shown.pause !== 'none');
|
||||
check('Fortschritt nennt MB in deutscher Schreibweise', /19,2 MB \/ 48,0 MB/.test(prog.shown.text), prog.shown.text);
|
||||
check('Pause schaltet die Beschriftung um und vermerkt „pausiert“', prog.paused.label === 'Fortsetzen' && /pausiert/.test(prog.paused.text));
|
||||
check('Esc löst Abbrechen aus', prog.afterEsc.disabled && /abgebrochen/.test(prog.afterEsc.label));
|
||||
check('Knöpfe werden nach dem Ausblenden zurückgesetzt', !prog.reset.cancelDisabled && prog.reset.display === 'none' && prog.reset.pauseDisplay === 'none');
|
||||
|
||||
// 5. Pause/Abbrechen ohne laufende Übertragung
|
||||
const idle = await ev(`(async () => ({ pause: await window.electronAPI.pauseUpload(), state: await window.electronAPI.getPauseState(), cancel: await window.electronAPI.cancelUpload() }))()`);
|
||||
check('Ohne laufende Übertragung lassen sich Pause und Abbrechen nicht auslösen', idle.pause.ok === false && idle.cancel.ok === false && idle.state.paused === false);
|
||||
|
||||
// 6. Upload-Verlauf: Aufzeichnung, Filter, Dialog
|
||||
await ev('window.electronAPI.clearUploadHistory()');
|
||||
await ev("window.electronAPI.uploadPathsToGitea({ owner: 'smoke', repo: 'alpha', paths: [] })");
|
||||
await ev("window.electronAPI.uploadPathsToGitea({ owner: 'smoke', repo: 'beta', paths: [] })");
|
||||
const hist = await ev(`(async () => {
|
||||
const all = (await window.electronAPI.getUploadHistory()).history.length;
|
||||
const ok = (await window.electronAPI.getUploadHistory({ status: 'ok' })).history.length;
|
||||
const alpha = (await window.electronAPI.getUploadHistory({ query: 'ALPHA' })).history.map(e => e.repo);
|
||||
showUploadHistory();
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
const rows = document.querySelectorAll('.upload-history-row').length;
|
||||
const exportEnabled = !document.querySelector('.confirm-modal button:nth-of-type(2)').disabled;
|
||||
const btns = [...document.querySelectorAll('.confirm-modal button')].map(b => b.textContent);
|
||||
document.querySelector('.confirm-modal button').click();
|
||||
return { all, ok, alpha, rows, btns, exportEnabled, offen: document.querySelectorAll('.confirm-modal').length };
|
||||
})()`);
|
||||
check('Verlauf zeichnet Uploads auf', hist.all === 2);
|
||||
check('Verlauf filtert nach Status und Repo', hist.ok === 0 && hist.alpha.length === 1 && hist.alpha[0] === 'smoke/alpha');
|
||||
check('Verlauf-Dialog zeigt klickbare Zeilen, Export und Leeren', hist.rows === 2 && hist.btns.includes('CSV exportieren') && hist.btns.includes('Verlauf leeren'));
|
||||
check('Verlauf-Dialog schließt sich', hist.offen === 0);
|
||||
await ev('window.electronAPI.clearUploadHistory()');
|
||||
|
||||
// 7. Sperre für Zugangsdaten (Vorschau nutzt dieselbe Dateiliste wie der Upload)
|
||||
const folder = path.join(tmp, 'projekt');
|
||||
fs.mkdirSync(path.join(folder, 'data'), { recursive: true });
|
||||
fs.writeFileSync(path.join(folder, 'main.js'), '// ok\n');
|
||||
fs.writeFileSync(path.join(folder, 'README.md'), '# ok\n');
|
||||
fs.writeFileSync(path.join(folder, '.env'), 'TOKEN=geheim\n');
|
||||
fs.writeFileSync(path.join(folder, 'data', 'credentials.json'), '{}\n');
|
||||
fs.writeFileSync(path.join(folder, '.env.example'), 'TOKEN=\n');
|
||||
const prev = await ev(`window.electronAPI.previewUploadPaths({ paths: [${JSON.stringify(folder)}], destPath: '' })`);
|
||||
const names = (prev.files || []).map((f) => path.basename(f.localFile)).sort();
|
||||
const blocked = (prev.blocked || []).map((b) => path.basename(b.path)).sort();
|
||||
check('Sperre nimmt .env und credentials.json aus der Dateiliste', !names.includes('.env') && !names.includes('credentials.json'), names.join(', '));
|
||||
check('Sperre meldet die ausgelassenen Dateien', JSON.stringify(blocked) === JSON.stringify(['.env', 'credentials.json']), blocked.join(', '));
|
||||
check('Normale Dateien und Vorlagen (.env.example) bleiben erlaubt', names.includes('main.js') && names.includes('README.md') && names.includes('.env.example'));
|
||||
|
||||
// 8. Release-Datei prüfen (Prüfsumme, Abgleich, Version)
|
||||
const exe = path.join(tmp, 'Git-Manager-GUI-Setup-9.8.7.exe');
|
||||
fs.writeFileSync(exe, 'Testinhalt');
|
||||
const hash = crypto.createHash('sha256').update('Testinhalt').digest('hex');
|
||||
fs.writeFileSync(`${exe}.sha256`, `${hash} Git-Manager-GUI-Setup-9.8.7.exe\n`);
|
||||
const good = await ev(`window.electronAPI.inspectReleaseFile({ filePath: ${JSON.stringify(exe)} })`);
|
||||
check('Release-Datei: Version und Prüfsumme werden erkannt', good.ok && good.version === '9.8.7' && good.sha256 === hash);
|
||||
check('Release-Datei: .sha256 stimmt überein', good.sidecarMatches === true);
|
||||
fs.writeFileSync(`${exe}.sha256`, `${'0'.repeat(64)} Git-Manager-GUI-Setup-9.8.7.exe\n`);
|
||||
const bad = await ev(`window.electronAPI.inspectReleaseFile({ filePath: ${JSON.stringify(exe)} })`);
|
||||
check('Release-Datei: falsche .sha256 wird erkannt', bad.sidecarMatches === false);
|
||||
|
||||
// 9. .gitignore-Vorlagen
|
||||
const tpl = await ev(`(async () => ({ list: (await window.electronAPI.getGitignoreTemplates()).templates.map(t => t.key), built: (await window.electronAPI.buildGitignore({ keys: ['java'] })).content }))()`);
|
||||
check('.gitignore-Vorlagen: sieben Stück, Java enthält target/', tpl.list.length === 7 && /target\//.test(tpl.built));
|
||||
|
||||
// 10. Einstellungen: neue Schalter vorhanden
|
||||
const toggles = await ev(`['prefBlockSecrets', 'prefNotifyTransfers'].map(id => !!document.getElementById(id))`);
|
||||
check('Einstellungen: Sperre und Benachrichtigung sind einstellbar', toggles.every(Boolean));
|
||||
|
||||
cdp.close();
|
||||
} finally {
|
||||
killTree(app.pid);
|
||||
await sleep(500);
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) { /* Datei noch gesperrt */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Führt eine eigenständige Electron-Prüfung aus (gibt ✓/✗-Zeilen aus) und übernimmt ihre Ergebnisse.
|
||||
function runHarness(title, script) {
|
||||
console.log(`\n${title}:`);
|
||||
const res = spawnSync(electronPath, [path.join(__dirname, script)], { cwd: ROOT, encoding: 'utf8', timeout: 120000 });
|
||||
const lines = String(res.stdout || '').split(/\r?\n/).filter((l) => /^(✓|✗) /.test(l));
|
||||
for (const l of lines) {
|
||||
console.log(l);
|
||||
results.push({ name: l.slice(2), ok: l.startsWith('✓') });
|
||||
}
|
||||
if (lines.length === 0) check(`${title} liefert Ergebnisse`, false, String(res.stderr || res.error || '').slice(0, 200));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`UI-Testlauf (Debug-Port ${PORT})\n`);
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
check('Testlauf ohne Ausnahme', false, e.message);
|
||||
}
|
||||
runHarness('Tray-Menüfenster', 'ui-smoke-tray.js');
|
||||
runHarness('Startanimation', 'ui-smoke-splash.js');
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log(`\n${results.length - failed.length} von ${results.length} Prüfungen bestanden.`);
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
})();
|
||||
Reference in New Issue
Block a user