Files
AirCardIOS/web/src/branding.test.ts
T

155 lines
6.6 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 { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { runInNewContext } from 'node:vm';
import { describe, expect, it } from 'vitest';
import indexHtml from '../index.html?raw';
import { initialLang } from './i18n/i18n';
import { initialTheme } from './shell/theme';
const publicFile = (name: string) => readFileSync(resolve(import.meta.dirname, '../public', name));
/** Breite, Höhe und Farbtyp aus dem IHDR-Block eines PNG (Farbtyp 2 = RGB ohne Alphakanal, 6 = RGBA). */
function pngInfo(buf: Buffer) {
expect(buf.subarray(0, 8).toString('hex')).toBe('89504e470d0a1a0a');
expect(buf.subarray(12, 16).toString('ascii')).toBe('IHDR');
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), colorType: buf[25] };
}
describe('Name und Kopfbereich', () => {
it('Titel und Meta-Angaben nennen AirCard-IOS', () => {
// Statisch englisch, damit Link-Vorschauen englisch sind; der Router setzt danach den Titel in der gewählten Sprache.
expect(indexHtml).toContain('<title>AirCard-IOS – Card designs for Apple Wallet</title>');
expect(indexHtml).toContain(
'<meta name="description" content="Explore community card designs for AirCard-iOS, create your own and download it as PNG." />',
);
expect(indexHtml).toContain('<meta name="theme-color" content="#0F1828" />');
expect(indexHtml).toContain('<meta name="apple-mobile-web-app-title" content="AirCard-IOS" />');
});
it('Icon-Links: SVG, 32-px-PNG, Apple-Touch-Icon', () => {
expect(indexHtml).toContain('<link rel="icon" href="/favicon.svg?v=2" type="image/svg+xml" />');
expect(indexHtml).toContain('<link rel="icon" href="/favicon-32.png?v=2" sizes="32x32" type="image/png" />');
expect(indexHtml).toContain('<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=2" />');
});
it('Kopfzeile: dekoratives Logo (28 x 28) links vom Namen, beides im Markenlink', () => {
expect(indexHtml).toContain(
'<a class="brand" href="/" data-link><img src="/favicon.svg?v=2" alt="" width="28" height="28" /><span>AirCard-IOS</span></a>',
);
});
it('Fußzeile ist leer: ihren Text setzt mountFooter in der gewählten Sprache', () => {
expect(indexHtml).toContain('<footer id="app-footer"></footer>');
expect(indexHtml).not.toContain('Inoffizielles Hobbyprojekt');
});
});
describe('Icon-Dateien', () => {
it('favicon-32.png ist 32 x 32', () => {
expect(pngInfo(publicFile('favicon-32.png'))).toMatchObject({ width: 32, height: 32 });
});
it('apple-touch-icon.png ist 180 x 180 und deckend (RGB ohne Alphakanal, kein transparenter Rand)', () => {
expect(pngInfo(publicFile('apple-touch-icon.png'))).toEqual({ width: 180, height: 180, colorType: 2 });
});
});
describe('Inline-Skript für Hell/Dunkel und Sprache in index.html', () => {
const script = /<script>([\s\S]*?)<\/script>/.exec(indexHtml)?.[1] ?? '';
/** Was der Browser zur Sprache hergibt: der gespeicherte Wert von `aircard.lang`, `navigator.languages` und `navigator.language`. */
interface BrowserLang {
stored?: string | null;
languages?: readonly string[];
language?: string;
}
/**
* Führt das Skript in einer leeren Umgebung aus (kein Browser) und liefert, was es gesetzt hat.
* `stored` ist der Wert von `aircard.theme`; die Sprache steht in `browser`.
*/
function run(stored: string | null, prefersDark: boolean, storageThrows = false, browser: BrowserLang = {}) {
const attributes: Record<string, string> = {};
const sandbox: Record<string, unknown> = {
localStorage: {
getItem: (key: string) => {
if (storageThrows) throw new Error('gesperrt');
return key === 'aircard.lang' ? (browser.stored ?? null) : stored;
},
},
window: {
matchMedia: () => ({ matches: prefersDark }),
navigator: { languages: browser.languages, language: browser.language },
},
document: { documentElement: { setAttribute: (name: string, value: string) => (attributes[name] = value) } },
};
runInNewContext(script, sandbox);
return { attributes, sandbox };
}
it('ist da und läuft ohne Browser-Umgebung durch', () => {
expect(script).toContain('data-theme');
expect(() => run(null, true)).not.toThrow();
});
it('legt keine globale Variable an (Skript steckt in einer sofort aufgerufenen Funktion)', () => {
const { sandbox } = run('dark', false);
expect(Object.keys(sandbox).sort()).toEqual(['document', 'localStorage', 'window']);
expect('theme' in sandbox).toBe(false);
expect('lang' in sandbox).toBe(false);
});
it.each([
['dark', false],
['light', true],
['dark', true],
['light', false],
['blau', true],
['blau', false],
['', true],
[null, true],
[null, false],
] as const)('gleiche Regel wie initialTheme(): gespeichert %j, Systemvorgabe dunkel %s', (stored, prefersDark) => {
expect(run(stored, prefersDark).attributes['data-theme']).toBe(initialTheme(stored, prefersDark));
});
it('gesperrter localStorage: es gilt die Systemvorgabe', () => {
expect(run(null, true, true).attributes['data-theme']).toBe('dark');
expect(run(null, false, true).attributes['data-theme']).toBe('light');
});
it.each([
[null, ['de-DE', 'en']],
[null, ['en-US', 'de']],
['en', ['de-DE']],
['de', ['en-US']],
['xx', ['de-AT']],
['xx', ['fr-FR']],
['', ['de']],
[null, ['DE']],
[null, ['de_CH']],
[null, []],
] as const)('setzt <html lang> nach derselben Regel wie initialLang(): gespeichert %j, Browsersprachen %j', (stored, languages) => {
const { attributes } = run(null, true, false, { stored, languages });
expect(attributes.lang).toBe(initialLang(stored, languages));
});
it('ohne navigator.languages gilt navigator.language (wie currentLang), ohne beides Englisch', () => {
expect(run(null, true, false, { language: 'de-CH' }).attributes.lang).toBe('de');
expect(run(null, true, false, { language: 'en-GB' }).attributes.lang).toBe('en');
expect(run(null, true, false, { languages: [], language: 'de' }).attributes.lang).toBe('de');
expect(run(null, true, false, {}).attributes.lang).toBe('en');
});
it('gesperrter localStorage: es gilt die Browsersprache', () => {
expect(run(null, true, true, { stored: 'en', languages: ['de-DE'] }).attributes.lang).toBe('de');
expect(run(null, true, true, { stored: 'de', languages: ['en-US'] }).attributes.lang).toBe('en');
});
it('die gespeicherte Sprache hat mit dem Theme nichts zu tun (und umgekehrt)', () => {
const { attributes } = run('light', false, false, { stored: 'en', languages: ['de'] });
expect(attributes['data-theme']).toBe('light');
expect(attributes.lang).toBe('en');
});
});