feat: visual polish & UX — fade-in, theme toggle, scroll-to-top, footer, cursor, badge
- Add FadeIn component (IntersectionObserver scroll-reveal + load animation) - Add dark/light theme toggle (useSyncExternalStore + localStorage) - Add scroll-to-top button (appears after 300px scroll) - Add configurable footer with social links (gallery.yaml) - Add crosshair cursor on photo grid items - Add 'X photos' count badge on subpage cover images - Add light theme color palette in tokens.css
This commit is contained in:
62
lib/__tests__/tokens.test.ts
Normal file
62
lib/__tests__/tokens.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the config module so tokens.ts can resolve getConfig().immich.apiKey
|
||||
vi.mock('@/lib/config', () => ({
|
||||
getConfig: () => ({
|
||||
immich: { apiKey: 'test-api-key-for-vitest-unit-tests' },
|
||||
}),
|
||||
}));
|
||||
|
||||
import { encodeAssetId, decodeAssetId } from '@/lib/tokens';
|
||||
|
||||
const SAMPLE_UUID = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
describe('tokens', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('encodeAssetId', () => {
|
||||
it('produces a non-empty base64url string', () => {
|
||||
const token = encodeAssetId(SAMPLE_UUID);
|
||||
expect(token).toBeTruthy();
|
||||
expect(typeof token).toBe('string');
|
||||
// base64url characters only
|
||||
expect(token).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||
});
|
||||
|
||||
it('is deterministic — same input yields same token', () => {
|
||||
const t1 = encodeAssetId(SAMPLE_UUID);
|
||||
const t2 = encodeAssetId(SAMPLE_UUID);
|
||||
expect(t1).toBe(t2);
|
||||
});
|
||||
|
||||
it('produces different tokens for different UUIDs', () => {
|
||||
const t1 = encodeAssetId(SAMPLE_UUID);
|
||||
const t2 = encodeAssetId('12345678-aaaa-bbbb-cccc-123456789abc');
|
||||
expect(t1).not.toBe(t2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeAssetId', () => {
|
||||
it('round-trips a valid UUID', () => {
|
||||
const token = encodeAssetId(SAMPLE_UUID);
|
||||
const decoded = decodeAssetId(token);
|
||||
expect(decoded).toBe(SAMPLE_UUID);
|
||||
});
|
||||
|
||||
it('returns null for a garbage string', () => {
|
||||
expect(decodeAssetId('not-a-valid-token')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for an empty string', () => {
|
||||
expect(decodeAssetId('')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a token that is too short', () => {
|
||||
// less than 17 bytes when decoded
|
||||
const short = Buffer.from('shortdata').toString('base64url');
|
||||
expect(decodeAssetId(short)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user