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>
This commit is contained in:
1 parent
af05e5f1f1
commit
0bcdfe258d
3 files changed
+74
-11
No files matched your search
@@ -133,6 +133,21 @@ describe('Sprachwechsel', () => {
|
|||||||
expect(storeAfter.get().cardName).toBe('Mein Entwurf');
|
expect(storeAfter.get().cardName).toBe('Mein Entwurf');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ein geschlossenes Spenden-Banner kommt beim Sprachwechsel nicht zurück, auch wenn der Speicher gesperrt ist', async () => {
|
||||||
|
await start('/');
|
||||||
|
const denied = () => {
|
||||||
|
throw new DOMException('blockiert', 'SecurityError');
|
||||||
|
};
|
||||||
|
vi.stubGlobal('localStorage', { getItem: denied, setItem: denied });
|
||||||
|
document.querySelector<HTMLButtonElement>('.donate-banner-close')!.click();
|
||||||
|
expect(document.querySelector('.donate-banner')).toBeNull();
|
||||||
|
|
||||||
|
langButton().click();
|
||||||
|
|
||||||
|
expect(document.documentElement.lang).toBe('en');
|
||||||
|
expect(document.querySelector('.donate-banner')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('das Konto-Menü wird neu gebaut und zeigt sich sofort, wenn der Anmeldestatus schon bekannt ist', async () => {
|
it('das Konto-Menü wird neu gebaut und zeigt sich sofort, wenn der Anmeldestatus schon bekannt ist', async () => {
|
||||||
await start('/');
|
await start('/');
|
||||||
await vi.waitFor(() => expect(signIn()).toBeDefined());
|
await vi.waitFor(() => expect(signIn()).toBeDefined());
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { setLang } from '../i18n/i18n';
|
|
||||||
import { mountDonateBanner } from './donate-banner';
|
|
||||||
|
|
||||||
const KEY = 'aircard.donateBannerClosed';
|
const KEY = 'aircard.donateBannerClosed';
|
||||||
const URL_ = 'https://buymeacoffee.com/aircard';
|
const URL_ = 'https://buymeacoffee.com/aircard';
|
||||||
const TEXT = '☕ Gefällt dir AirCard-IOS? Spendier uns einen Kaffee – damit das Projekt weiterläuft.';
|
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;
|
let slot: HTMLElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
({ mountDonateBanner } = await import('./donate-banner'));
|
||||||
|
({ setLang } = await import('../i18n/i18n'));
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
document.body.innerHTML = '<div id="donate-banner"></div>';
|
document.body.innerHTML = '<div id="donate-banner"></div>';
|
||||||
slot = document.getElementById('donate-banner') as HTMLElement;
|
slot = document.getElementById('donate-banner') as HTMLElement;
|
||||||
@@ -16,8 +21,26 @@ beforeEach(() => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
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', () => {
|
describe('mountDonateBanner', () => {
|
||||||
it('leere URL: nichts wird gerendert', () => {
|
it('leere URL: nichts wird gerendert', () => {
|
||||||
mountDonateBanner(slot, '');
|
mountDonateBanner(slot, '');
|
||||||
@@ -68,19 +91,39 @@ describe('mountDonateBanner', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('localStorage wirft beim Lesen: Banner wird gezeigt', () => {
|
it('localStorage wirft beim Lesen: Banner wird gezeigt', () => {
|
||||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
blockStorage({ read: true });
|
||||||
throw new DOMException('blockiert', 'SecurityError');
|
|
||||||
});
|
|
||||||
expect(() => mountDonateBanner(slot, URL_)).not.toThrow();
|
expect(() => mountDonateBanner(slot, URL_)).not.toThrow();
|
||||||
expect(slot.querySelectorAll('.donate-banner')).toHaveLength(1);
|
expect(slot.querySelectorAll('.donate-banner')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('localStorage wirft beim Schreiben: Banner verschwindet trotzdem, kein Fehler', () => {
|
it('localStorage wirft beim Schreiben: Banner verschwindet trotzdem, kein Fehler', () => {
|
||||||
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
blockStorage({ write: true });
|
||||||
throw new DOMException('voll', 'QuotaExceededError');
|
|
||||||
});
|
|
||||||
mountDonateBanner(slot, URL_);
|
mountDonateBanner(slot, URL_);
|
||||||
expect(() => slot.querySelector('button')!.click()).not.toThrow();
|
expect(() => slot.querySelector('button')!.click()).not.toThrow();
|
||||||
expect(slot.childElementCount).toBe(0);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -5,19 +5,24 @@ import { t } from '../i18n/i18n';
|
|||||||
|
|
||||||
const CLOSED_KEY = 'aircard.donateBannerClosed';
|
const CLOSED_KEY = 'aircard.donateBannerClosed';
|
||||||
|
|
||||||
|
// Auch im Speicher des Moduls gemerkt: bei gesperrtem localStorage bringt sonst der Neuaufbau des Rahmens (Sprachwechsel) das Banner zurück.
|
||||||
|
let closedThisSession = false;
|
||||||
|
|
||||||
function isClosed(): boolean {
|
function isClosed(): boolean {
|
||||||
|
if (closedThisSession) return true;
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(CLOSED_KEY) !== null;
|
return localStorage.getItem(CLOSED_KEY) !== null;
|
||||||
} catch {
|
} catch {
|
||||||
return false; // Speicher gesperrt: das Banner bleibt sichtbar, bis die Seite neu geladen wird
|
return false; // Speicher gesperrt: das Banner bleibt sichtbar, bis es jemand schließt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberClosed(): void {
|
function rememberClosed(): void {
|
||||||
|
closedThisSession = true;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(CLOSED_KEY, '1');
|
localStorage.setItem(CLOSED_KEY, '1');
|
||||||
} catch {
|
} catch {
|
||||||
// gilt dann nur für diese Sitzung
|
// gilt dann nur, solange die Seite offen ist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user