const test = require('node:test'); const assert = require('node:assert'); const { buildTrayTemplate, trayTooltip } = require('../src/features/trayMenu.js'); const noop = () => {}; const base = { version: '2.2.1', onOpen: noop, onHistory: noop, onCancel: noop, onAutostart: noop, onQuit: noop }; const labels = (tpl) => tpl.filter(i => i.label).map(i => i.label); const find = (tpl, label) => tpl.find(i => i.label === label); test('Tooltip nennt Version und zeigt eine laufende Übertragung', () => { assert.strictEqual(trayTooltip('2.2.1', 0), 'Git Manager Explorer Pro v2.2.1'); assert.match(trayTooltip('2.2.1', 2), /Übertragung läuft/); assert.match(trayTooltip('2.2.1', 1, true), /Übertragung pausiert/); assert.strictEqual(trayTooltip('2.2.1', 0, true), 'Git Manager Explorer Pro v2.2.1'); }); test('Menü im Ruhezustand: Kopfzeile, Status, Aktionen; Abbrechen gesperrt', () => { const tpl = buildTrayTemplate(base); assert.deepStrictEqual(labels(tpl), [ 'Git Manager Explorer Pro v2.2.1', 'Keine laufende Übertragung', 'Fenster öffnen', 'Upload-Verlauf', 'Übertragung pausieren', 'Laufende Übertragung abbrechen', 'Mit Windows starten', 'Beenden' ]); assert.strictEqual(find(tpl, 'Laufende Übertragung abbrechen').enabled, false); assert.strictEqual(tpl[0].enabled, false); }); test('Menü bei laufender Übertragung: Zähler, Abbrechen freigeschaltet', () => { const one = buildTrayTemplate({ ...base, active: 1 }); assert.ok(labels(one).includes('1 Übertragung läuft')); assert.strictEqual(find(one, 'Laufende Übertragung abbrechen').enabled, true); assert.ok(labels(buildTrayTemplate({ ...base, active: 3 })).includes('3 Übertragungen laufen')); }); test('Retry-Queue erscheint nur, wenn etwas wartet', () => { assert.ok(!labels(buildTrayTemplate(base)).some(l => l.startsWith('Retry-Queue'))); assert.ok(labels(buildTrayTemplate({ ...base, queued: 4 })).includes('Retry-Queue: 4 wartend')); }); test('Autostart ist ein Häkchen und spiegelt den Zustand', () => { assert.strictEqual(find(buildTrayTemplate({ ...base, autostart: true }), 'Mit Windows starten').checked, true); const off = find(buildTrayTemplate(base), 'Mit Windows starten'); assert.strictEqual(off.type, 'checkbox'); assert.strictEqual(off.checked, false); }); test('Einträge rufen die übergebenen Aktionen auf', () => { const calls = []; const tpl = buildTrayTemplate({ ...base, active: 1, onOpen: () => calls.push('open'), onHistory: () => calls.push('history'), onCancel: () => calls.push('cancel'), onQuit: () => calls.push('quit') }); for (const l of ['Fenster öffnen', 'Upload-Verlauf', 'Laufende Übertragung abbrechen', 'Beenden']) find(tpl, l).click(); assert.deepStrictEqual(calls, ['open', 'history', 'cancel', 'quit']); }); const { serializeTemplate, runAction } = require('../src/features/trayMenu.js'); const { computePosition } = require('../src/features/trayPopup.js'); test('serializeTemplate: nur Daten, keine Funktionen, Trenner bleiben erhalten', () => { const plain = serializeTemplate(buildTrayTemplate({ ...base, active: 1, autostart: true })); assert.ok(plain.every(i => Object.values(i).every(v => typeof v !== 'function'))); assert.strictEqual(plain.filter(i => i.type === 'separator').length, 3); const cancel = plain.find(i => i.id === 'cancel'); assert.deepStrictEqual(cancel, { id: 'cancel', label: 'Laufende Übertragung abbrechen', type: 'normal', enabled: true, checked: false }); assert.strictEqual(plain.find(i => i.id === 'autostart').checked, true); assert.doesNotThrow(() => JSON.stringify(plain)); }); test('runAction: führt Aktionen per id aus, Häkchen wird umgeschaltet', () => { const calls = []; const tpl = buildTrayTemplate({ ...base, active: 1, autostart: false, onOpen: () => calls.push('open'), onAutostart: (item) => calls.push(`autostart:${item.checked}`) }); assert.strictEqual(runAction(tpl, 'open'), true); assert.strictEqual(runAction(tpl, 'autostart'), true); assert.deepStrictEqual(calls, ['open', 'autostart:true']); }); test('runAction: gesperrte oder unbekannte Einträge tun nichts', () => { let cancelled = false; const tpl = buildTrayTemplate({ ...base, active: 0, onCancel: () => { cancelled = true; } }); assert.strictEqual(runAction(tpl, 'cancel'), false); assert.strictEqual(runAction(tpl, 'gibt-es-nicht'), false); assert.strictEqual(runAction(tpl, 'header'), false); assert.strictEqual(cancelled, false); }); test('computePosition: Menü bleibt im sichtbaren Bereich', () => { const wa = { x: 0, y: 0, width: 1920, height: 1040 }; // Zeiger in der Taskleiste unten rechts: Menü sitzt direkt über der Taskleiste, nicht außerhalb rechts const bottomRight = computePosition({ x: 1900, y: 1060 }, wa, 286, 300); assert.strictEqual(bottomRight.y, 1040 - 300 - 8); assert.ok(bottomRight.x + 286 <= 1920 - 8); // Zeiger weit links: nicht links aus dem Bildschirm assert.ok(computePosition({ x: 2, y: 1060 }, wa, 286, 300).x >= 8); // Taskleiste oben assert.strictEqual(computePosition({ x: 500, y: 10 }, { ...wa, y: 40, height: 1000 }, 286, 300).y, 48); }); test('Pause: Eintrag wechselt zwischen pausieren und fortsetzen, nur bei laufender Übertragung wählbar', () => { const idle = buildTrayTemplate(base); assert.strictEqual(find(idle, 'Übertragung pausieren').enabled, false); const running = buildTrayTemplate({ ...base, active: 1 }); assert.strictEqual(find(running, 'Übertragung pausieren').enabled, true); const paused = buildTrayTemplate({ ...base, active: 2, paused: true }); assert.ok(labels(paused).includes('Übertragung fortsetzen')); assert.ok(labels(paused).includes('2 Übertragungen pausiert')); assert.ok(!labels(paused).includes('Übertragung pausieren')); assert.ok(labels(buildTrayTemplate({ ...base, active: 1, paused: true })).includes('1 Übertragung pausiert')); }); test('Pause-Eintrag ruft onPause auf', () => { const calls = []; const tpl = buildTrayTemplate({ ...base, active: 1, onPause: () => calls.push('pause') }); assert.strictEqual(runAction(tpl, 'pause'), true); assert.deepStrictEqual(calls, ['pause']); });