Public Access
feat(pwa): make the app installable
Manifest, gamepad icon (also the new favicon, instead of the Vite logo), iOS meta tags and the tab title ToBePlayed. No service worker, so a deploy never meets an old cached bundle. The title bar color follows light and dark mode. The README also lists adding a game by name.
This commit is contained in:
1 parent
444b03acee
commit
f8e6c2c970
12 files changed
+117
-3
No files matched your search
@@ -11,6 +11,8 @@ import {
|
||||
} from '../utils/appearance.js';
|
||||
|
||||
const DARK_MODE_STORAGE_KEY = 'tbp_dark_mode';
|
||||
// The --background of style.css; the installed app paints its title bar in this color.
|
||||
const THEME_COLORS = {dark: '#09090b', light: '#fafafa'};
|
||||
|
||||
const PreferencesContext = createContext(null);
|
||||
|
||||
@@ -28,6 +30,8 @@ export function PreferencesProvider({children}) {
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
document.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', isDarkMode ? THEME_COLORS.dark : THEME_COLORS.light);
|
||||
localStorage.setItem(DARK_MODE_STORAGE_KEY, String(isDarkMode));
|
||||
}, [isDarkMode]);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {act, cleanup, render} from '@testing-library/react';
|
||||
import {PreferencesProvider, usePreferences} from './PreferencesContext.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
let preferences;
|
||||
function Spy() {
|
||||
preferences = usePreferences();
|
||||
return null;
|
||||
}
|
||||
|
||||
const themeColor = () => document.querySelector('meta[name="theme-color"]').content;
|
||||
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = '<meta name="theme-color" content="#09090b">';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('PreferencesProvider theme color', () => {
|
||||
it('gives the installed app a light title bar in light mode and a dark one again in dark mode', () => {
|
||||
localStorage.setItem('tbp_dark_mode', 'false');
|
||||
render(<PreferencesProvider><Spy/></PreferencesProvider>);
|
||||
|
||||
expect(themeColor()).toBe('#fafafa');
|
||||
|
||||
act(() => preferences.setDarkMode(true));
|
||||
expect(themeColor()).toBe('#09090b');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import {readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const read = (file) => readFileSync(path.join(root, file));
|
||||
const manifest = JSON.parse(read('public/manifest.webmanifest'));
|
||||
|
||||
// Width and height sit at bytes 16-23 of every PNG.
|
||||
const pngSize = (file) => {
|
||||
const png = read(path.join('public', file));
|
||||
return `${png.readUInt32BE(16)}x${png.readUInt32BE(20)}`;
|
||||
};
|
||||
|
||||
describe('PWA manifest', () => {
|
||||
it('links every icon to a PNG of the size it claims', () => {
|
||||
for (const icon of manifest.icons) {
|
||||
expect(pngSize(icon.src)).toBe(icon.sizes);
|
||||
}
|
||||
});
|
||||
|
||||
it('has the icons Chrome needs to offer the install', () => {
|
||||
const sizes = manifest.icons.filter((icon) => icon.purpose === 'any').map((icon) => icon.sizes);
|
||||
expect(sizes).toEqual(expect.arrayContaining(['192x192', '512x512']));
|
||||
expect(manifest.display).toBe('standalone');
|
||||
expect(manifest.icons.some((icon) => icon.purpose === 'maskable')).toBe(true);
|
||||
});
|
||||
|
||||
it('is linked from index.html together with the iOS icon', () => {
|
||||
const html = read('index.html').toString();
|
||||
expect(html).toContain('<link rel="manifest" href="/manifest.webmanifest" />');
|
||||
expect(html).toContain('<link rel="apple-touch-icon" href="/apple-touch-icon.png" />');
|
||||
expect(pngSize('apple-touch-icon.png')).toBe('180x180');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user