Files
AirCardIOS/web/src/shell/donate-banner.test.ts
T
ClaudeandClaude Opus 5.5 0bcdfe258d fix(shell): geschlossenes Spenden-Banner kommt beim Sprachwechsel auch ohne localStorage nicht zurück
Das Banner merkte sich „geschlossen“ nur in localStorage. Bei gesperrtem Speicher (privates Fenster, gesperrte Website-Daten) baute der Neuaufbau
des Rahmens beim Sprachwechsel (main.ts) es wieder auf.

- donate-banner.ts: zusätzlich ein Merker im Speicher des Moduls (closedThisSession); er gilt neben localStorage.
- Tests: donate-banner.test.ts lädt je Test eine frische Modulkopie (der Merker lebt im Modul); neue Fälle: gesperrter Speicher, erneutes Einhängen
  nach dem Schließen (bleibt weg) und ohne Schließen (kommt wieder); main.test.ts: Sprachwechsel nach dem Schließen bei gesperrtem Speicher.
  Die beiden schon vorhandenen Fälle zum gesperrten Speicher sperren jetzt wirklich (vi.stubGlobal): vi.spyOn(Storage.prototype, …) greift in
  happy-dom nicht, die Fälle waren leer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:31:38 +00:00

130 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '<div id="donate-banner"></div>';
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<HTMLAnchorElement>('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<HTMLAnchorElement>('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);
});
});