import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const KEY = 'aircard.donateBannerClosed'; const URL_ = 'https://buymeacoffee.com/aircard'; const TEXT = '☕ Gefällt dir AirCard-IOS? Spendier uns einen Kaffee – damit das Projekt weiterläuft.'; // Das Modul merkt sich im Speicher, dass das Banner geschlossen wurde (siehe unten): jeder Test bekommt darum eine frische Kopie von ihm // und von i18n.ts (die Sprache wird an dieser Kopie gesetzt). let mountDonateBanner: typeof import('./donate-banner').mountDonateBanner; let setLang: typeof import('../i18n/i18n').setLang; let slot: HTMLElement; beforeEach(async () => { vi.resetModules(); ({ mountDonateBanner } = await import('./donate-banner')); ({ setLang } = await import('../i18n/i18n')); localStorage.clear(); document.body.innerHTML = ''; slot = document.getElementById('donate-banner') as HTMLElement; }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); /** * Ein Speicher, der den Zugriff verweigert (privates Fenster, gesperrte Website-Daten). `vi.spyOn(Storage.prototype, …)` greift in * happy-dom nicht, darum wird `localStorage` selbst ersetzt. */ function blockStorage(block: { read?: boolean; write?: boolean }): void { const denied = () => new DOMException('blockiert', 'SecurityError'); vi.stubGlobal('localStorage', { getItem: () => { if (block.read) throw denied(); return null; }, setItem: () => { if (block.write) throw denied(); }, }); } describe('mountDonateBanner', () => { it('leere URL: nichts wird gerendert', () => { mountDonateBanner(slot, ''); expect(slot.childElementCount).toBe(0); mountDonateBanner(slot, ' '); expect(slot.childElementCount).toBe(0); }); it('zeigt Text, Link „Unterstützen →“ (neuer Tab) und Schließen-Knopf', () => { mountDonateBanner(slot, URL_); expect(slot.querySelector('.donate-banner-text')?.textContent).toBe(TEXT); const link = slot.querySelector('a.donate-banner-link')!; expect(link.textContent).toBe('Unterstützen →'); expect(link.getAttribute('href')).toBe(URL_); expect(link.target).toBe('_blank'); expect(link.rel).toBe('noopener noreferrer'); expect(link.hasAttribute('data-link')).toBe(false); expect(slot.querySelector('button')?.getAttribute('aria-label')).toBe('Banner schließen'); }); it('Englisch: Text, Link und Schließen-Knopf übersetzt, Zeichen (☕, →) und Link unverändert', () => { setLang('en'); mountDonateBanner(slot, URL_); expect(slot.querySelector('.donate-banner-text')?.textContent).toBe('☕ Enjoying AirCard-IOS? Buy us a coffee – it keeps the project running.'); const link = slot.querySelector('a.donate-banner-link')!; expect(link.textContent).toBe('Support us →'); expect(link.getAttribute('href')).toBe(URL_); expect(slot.querySelector('button')?.getAttribute('aria-label')).toBe('Close banner'); }); it('Schließen setzt das Flag und entfernt das Banner', () => { mountDonateBanner(slot, URL_); slot.querySelector('button')!.click(); expect(localStorage.getItem(KEY)).toBe('1'); expect(slot.childElementCount).toBe(0); }); it('Flag gesetzt: nichts wird gerendert', () => { localStorage.setItem(KEY, '1'); mountDonateBanner(slot, URL_); expect(slot.childElementCount).toBe(0); }); it('zweimal eingehängt gibt es nur ein Banner', () => { mountDonateBanner(slot, URL_); mountDonateBanner(slot, URL_); expect(slot.querySelectorAll('.donate-banner')).toHaveLength(1); }); it('localStorage wirft beim Lesen: Banner wird gezeigt', () => { blockStorage({ read: true }); expect(() => mountDonateBanner(slot, URL_)).not.toThrow(); expect(slot.querySelectorAll('.donate-banner')).toHaveLength(1); }); it('localStorage wirft beim Schreiben: Banner verschwindet trotzdem, kein Fehler', () => { blockStorage({ write: true }); mountDonateBanner(slot, URL_); expect(() => slot.querySelector('button')!.click()).not.toThrow(); expect(slot.childElementCount).toBe(0); }); it('localStorage gesperrt: das geschlossene Banner kommt beim erneuten Einhängen (Sprachwechsel) nicht zurück', () => { blockStorage({ read: true, write: true }); mountDonateBanner(slot, URL_); slot.querySelector('button')!.click(); expect(slot.childElementCount).toBe(0); mountDonateBanner(slot, URL_); // so baut main.ts den Rahmen bei einem Sprachwechsel neu auf expect(slot.childElementCount).toBe(0); }); it('ohne Schließen kommt das Banner beim erneuten Einhängen wieder, auch bei gesperrtem Speicher', () => { blockStorage({ read: true, write: true }); mountDonateBanner(slot, URL_); mountDonateBanner(slot, URL_); // neu aufgebaut, ohne dass jemand es geschlossen hat expect(slot.querySelectorAll('.donate-banner')).toHaveLength(1); }); it('der Speicher lässt sich schreiben, aber nicht lesen: nach dem Schließen bleibt das Banner ebenfalls weg', () => { blockStorage({ read: true }); mountDonateBanner(slot, URL_); slot.querySelector('button')!.click(); mountDonateBanner(slot, URL_); expect(slot.childElementCount).toBe(0); }); });