From 3970e8a00c97b7e56fee234c0e5e7042aee2328f Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Fri, 20 Feb 2026 09:04:38 +0000 Subject: [PATCH] test: add unit tests for hooks (useBookmarks, useUpdateCheck) - Export loadBookmarks/saveBookmarks and isNewer for testability - Add 12 tests covering bookmark persistence and semver comparison - Total: 165 tests passing --- src/hooks/__tests__/useBookmarks.test.ts | 56 ++++++++++++++++++++++ src/hooks/__tests__/useUpdateCheck.test.ts | 33 +++++++++++++ src/hooks/useBookmarks.ts | 4 +- src/hooks/useUpdateCheck.ts | 2 +- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/hooks/__tests__/useBookmarks.test.ts create mode 100644 src/hooks/__tests__/useUpdateCheck.test.ts diff --git a/src/hooks/__tests__/useBookmarks.test.ts b/src/hooks/__tests__/useBookmarks.test.ts new file mode 100644 index 0000000..cddf996 --- /dev/null +++ b/src/hooks/__tests__/useBookmarks.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { loadBookmarks, saveBookmarks, type Bookmark } from '../useBookmarks'; + +const STORAGE_KEY = 'pinchchat-bookmarks'; + +// Mock localStorage +const store: Record = {}; +const localStorageMock = { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { store[key] = value; }), + removeItem: vi.fn((key: string) => { delete store[key]; }), +}; + +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock }); + +beforeEach(() => { + for (const key of Object.keys(store)) delete store[key]; + vi.clearAllMocks(); +}); + +describe('loadBookmarks', () => { + it('returns empty array when nothing stored', () => { + expect(loadBookmarks()).toEqual([]); + }); + + it('returns parsed bookmarks from localStorage', () => { + const bookmarks: Bookmark[] = [ + { messageId: 'msg1', sessionKey: 'sess1', preview: 'Hello', timestamp: 1000, bookmarkedAt: 2000 }, + ]; + store[STORAGE_KEY] = JSON.stringify(bookmarks); + expect(loadBookmarks()).toEqual(bookmarks); + }); + + it('returns empty array on corrupt JSON', () => { + store[STORAGE_KEY] = '{broken'; + expect(loadBookmarks()).toEqual([]); + }); +}); + +describe('saveBookmarks', () => { + it('persists bookmarks to localStorage', () => { + const bookmarks: Bookmark[] = [ + { messageId: 'msg1', sessionKey: 'sess1', preview: 'Test', timestamp: 1000, bookmarkedAt: 2000 }, + ]; + saveBookmarks(bookmarks); + expect(JSON.parse(store[STORAGE_KEY]!)).toEqual(bookmarks); + }); + + it('overwrites existing bookmarks', () => { + saveBookmarks([{ messageId: 'old', sessionKey: 's', preview: '', timestamp: 0, bookmarkedAt: 0 }]); + saveBookmarks([{ messageId: 'new', sessionKey: 's', preview: '', timestamp: 0, bookmarkedAt: 0 }]); + const stored = JSON.parse(store[STORAGE_KEY]!); + expect(stored).toHaveLength(1); + expect(stored[0].messageId).toBe('new'); + }); +}); diff --git a/src/hooks/__tests__/useUpdateCheck.test.ts b/src/hooks/__tests__/useUpdateCheck.test.ts new file mode 100644 index 0000000..ff68035 --- /dev/null +++ b/src/hooks/__tests__/useUpdateCheck.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { isNewer } from '../useUpdateCheck'; + +describe('isNewer', () => { + it('returns true when remote major is higher', () => { + expect(isNewer('2.0.0', '1.0.0')).toBe(true); + }); + + it('returns true when remote minor is higher', () => { + expect(isNewer('1.5.0', '1.4.0')).toBe(true); + }); + + it('returns true when remote patch is higher', () => { + expect(isNewer('1.4.2', '1.4.1')).toBe(true); + }); + + it('returns false when versions are equal', () => { + expect(isNewer('1.4.1', '1.4.1')).toBe(false); + }); + + it('returns false when remote is older', () => { + expect(isNewer('1.3.9', '1.4.0')).toBe(false); + }); + + it('handles missing patch segments', () => { + expect(isNewer('1.1', '1.0.9')).toBe(true); + }); + + it('handles large version numbers', () => { + expect(isNewer('1.66.1', '1.66.0')).toBe(true); + expect(isNewer('1.66.0', '1.66.1')).toBe(false); + }); +}); diff --git a/src/hooks/useBookmarks.ts b/src/hooks/useBookmarks.ts index bf05ab6..f144711 100644 --- a/src/hooks/useBookmarks.ts +++ b/src/hooks/useBookmarks.ts @@ -10,7 +10,7 @@ export interface Bookmark { bookmarkedAt: number; } -function loadBookmarks(): Bookmark[] { +export function loadBookmarks(): Bookmark[] { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw) as Bookmark[]; @@ -18,7 +18,7 @@ function loadBookmarks(): Bookmark[] { return []; } -function saveBookmarks(bookmarks: Bookmark[]) { +export function saveBookmarks(bookmarks: Bookmark[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks)); } catch { /* noop */ } diff --git a/src/hooks/useUpdateCheck.ts b/src/hooks/useUpdateCheck.ts index 5b1239b..2bea687 100644 --- a/src/hooks/useUpdateCheck.ts +++ b/src/hooks/useUpdateCheck.ts @@ -54,7 +54,7 @@ export function useUpdateCheck(currentVersion: string): UpdateInfo { } /** True if remote is newer than local (semver compare) */ -function isNewer(remote: string, local: string): boolean { +export function isNewer(remote: string, local: string): boolean { const r = remote.split('.').map(Number); const l = local.split('.').map(Number); for (let i = 0; i < 3; i++) {