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);
});
});